diff --git a/docs/ai/index.mdx b/docs/ai/index.mdx
index a8f0087..709fcec 100644
--- a/docs/ai/index.mdx
+++ b/docs/ai/index.mdx
@@ -57,5 +57,5 @@ None of these files are role-gated: `.chezmoiignore.tmpl` applies them on every
## Shell entry points
- `ai` (fish function) opens a `gum choose` picker over `claude`, `opencode`, `codex`, `hermes`, and `fractal`. Picking `claude` runs `claude --dangerously-skip-permissions`; the others run the binary of the same name. See [fish](/shell/fish).
-- Ctrl A launches `omp` from the fish and zsh prompts.
+- Ctrl A opens the `agents` television cable from the fish prompt: every installed agent CLI (omp, claude, codex, opencode, …); enter starts it ([television](/tools/television#agents)). In zsh it still launches `omp`.
- `launch_omp_livediff_overlay` and `lc-elir` are omp helpers in `~/.local/bin`; see [omp](/ai/omp#shell-integration) and [scripts](/shell/scripts).
diff --git a/docs/ai/omp.mdx b/docs/ai/omp.mdx
index b3fa7ed..9475b92 100644
--- a/docs/ai/omp.mdx
+++ b/docs/ai/omp.mdx
@@ -168,7 +168,7 @@ Both count as done only when the full artifact is pasted in the chat.
| Entry point | Source | What it does |
| --- | --- | --- |
| fish completions | `dot_config/fish/completions/omp.fish` | `omp completions fish \| source` generates completions at load time |
-| Ctrl A | fish `fish_user_key_bindings`, zsh `keybinds.zsh` | Launches `omp` from the prompt |
+| Ctrl A | fish `fish_user_key_bindings` → `_agents_bind`, zsh `keybinds.zsh` | fish: the `agents` tv cable, with `omp` first; zsh: launches `omp` from the prompt |
| `launch_omp_livediff_overlay` | `dot_local/bin/executable_launch_omp_livediff_overlay`, fish function of the same name | Opens omp as a kitty overlay on the current window with a `livediff` vsplit beside it, focus kept on omp. Needs `KITTY_LISTEN_ON` (kitty remote control). |
| `lc-elir [root]` | `dot_local/bin/executable_lc-elir`, fish function | fzf multi-picks numbered LeetCode files (`1365.slug.js`/`.ts`) with a bat preview and runs `omp -p "/leetcode-eli5 "` on each |
diff --git a/docs/shell/bash-zsh.mdx b/docs/shell/bash-zsh.mdx
index 8799b8c..144574e 100644
--- a/docs/shell/bash-zsh.mdx
+++ b/docs/shell/bash-zsh.mdx
@@ -27,7 +27,7 @@ Interactive shells only (`[[ $- != *i* ]] && return`). In order:
1. **PATH.** Prepends `~/.local/bin` if missing, sources `~/.cargo/env` if present, and appends `~/.maestro/bin` if it exists. Exports `GHQ_ROOT` and `CODE_ROOT` as `~/Code`.
2. **Go.** `GOPATH=~/.local/share/go`, `GOBIN=$GOPATH/bin`, and `GOBIN` prepended to `PATH`. mise provides `GOROOT`.
-3. **Android SDK.** `ANDROID_HOME` and `ANDROID_SDK_ROOT` set to `~/.local/share/android`. `platform-tools`, `cmdline-tools/latest/bin`, and `emulator` are appended if they exist.
+3. **Android SDK.** `ANDROID_HOME` and `ANDROID_SDK_ROOT` set to `~/.local/share/android`. `platform-tools`, `cmdline-tools/latest/bin`, and `emulator` are appended if they exist. Exports `GITHUB_TOKEN` from `gh auth token` unless it is already set.
4. **Aliases.** Sources `~/.config/shell/aliases.sh`.
5. **Functions.** Sources every `~/.config/bash/functions/*.bash`.
6. **Flyline.** If `~/.local/lib/libflyline.so` exists, loads it with `enable -f … flyline` for richer line editing.
@@ -40,7 +40,7 @@ Interactive shells only (`[[ $- != *i* ]] && return`). In order:
1. **History.** `HISTFILE=~/.zsh_history`, `HISTSIZE=10000`, `SAVEHIST=10000`, `setopt SHARE_HISTORY HIST_IGNORE_DUPS`.
2. **Completion.** `autoload -Uz compinit && compinit`.
-3. The same PATH, Go, and Android blocks as `.bashrc`, including `GHQ_ROOT` and `CODE_ROOT`.
+3. The same PATH, Go, Android, and `GITHUB_TOKEN` blocks as `.bashrc`, including `GHQ_ROOT` and `CODE_ROOT`.
4. Sources `~/.config/shell/aliases.sh`.
5. Sources every `~/.config/zsh/functions/*.zsh`.
6. Sources `~/.config/zsh/keybinds.zsh`.
diff --git a/docs/shell/fish.mdx b/docs/shell/fish.mdx
index a752428..ec7a293 100644
--- a/docs/shell/fish.mdx
+++ b/docs/shell/fish.mdx
@@ -80,6 +80,7 @@ Runs for every shell, including non-interactive ones, so scripts get the same `P
| `ANDROID_HOME`, `ANDROID_SDK_ROOT` | `~/.local/share/android` |
| `GITEA_LOGIN` | `prad.codes` |
| `GITLAB_HOST` | `git.sonr.org` |
+| `GITHUB_TOKEN` | `gh auth token`, unless already set and only if `gh` is logged in |
| `EDITOR`, `VISUAL` | `nvim` |
| `BROWSER` | `/usr/bin/brave-origin` if that binary exists |
| `CHROME_EXECUTABLE` | `/usr/bin/chromium` if it exists |
@@ -185,7 +186,7 @@ The header comment says the file should stay empty, but it holds the following:
| ctrl-y | `clip-last`: re-run the last command and copy its output |
| ctrl-o | `_gho_bind`: `gho` (pick a ghq repo or Obsidian vault from the `ghq-repos` cable and cd into it), then repaint |
| ctrl-p | `_pj_bind`: `pj`, the task runner on the `pj` cable ([Scripts](/shell/scripts#pj)), then repaint |
-| ctrl-a | `omp` (the coding agent, see [AI agents](/ai)) |
+| ctrl-a | `_agents_bind`: pick an installed AI agent from the `agents` cable and start its TUI ([television](/tools/television#agents)), then repaint |
| ctrl-w | `atuin ai inline`: natural-language command overlay |
| ctrl-g | `lazygit` |
| down | `_pj_bind_down`: opens the `pj` cable on an idle, empty prompt, otherwise fish's `down-or-search` |
@@ -210,11 +211,12 @@ The pickers run `tv --inline`, which opens on the line below the prompt and leav
| `_tv_inline ARGS` | `tv ARGS --inline` | `\033[A` (cursor up), returns tv's status |
| `_gho_bind` | `gho` | `\033[A` if the cwd did not change; `\n` after a cd, so the prompt lands below onefetch |
| `_pj_bind [--quiet]` | `pj` with `PJ_STATE` set to a temp file | `\033[A` for `open` (cancelled) or empty (no task sources); `\n` for `ran` (the task printed output) |
+| `_agents_bind` | `tv agents --inline` with `AGENTS_STATE` set to a temp file | `\033[A` unless the cable's `launch` action wrote `ran` (an agent started) |
zsh and bash bind ctrl-o, ctrl-e, ctrl-p, down, and ctrl-t the same way ([bash and zsh](/shell/bash-zsh)). Inside television, ctrl-e runs the channel's `edit` action and ctrl-h its `goto_parent_dir` action (`television/config.toml`). The config and cables are on the [television](/tools/television) page.
:::note
-Under kitty, ctrl-g (lazygit overlay), ctrl-a (omp split), ctrl-y (yazi split), and ctrl-h/j/k/l (split navigation) are mapped in `kitty.conf` and never reach fish. The fish binds for those keys only apply in other terminals. See [kitty](/terminal/kitty).
+Under kitty, ctrl-g (lazygit overlay), ctrl-y (yazi split), and ctrl-h/j/k/l (split navigation) are mapped in `kitty.conf` and never reach fish. The fish binds for those keys only apply in other terminals. See [kitty](/terminal/kitty).
:::
The prompt shows `❮` in green in vi normal mode (starship's `vimcmd_symbol`).
diff --git a/docs/tools/television.mdx b/docs/tools/television.mdx
index 04dc829..7e2d659 100644
--- a/docs/tools/television.mdx
+++ b/docs/tools/television.mdx
@@ -62,7 +62,7 @@ Global keys added on top of the defaults:
## Sorting
-Every custom cable pipes its source through [`mtime-sort`](/shell/scripts#mtime-sort), which orders lines newest-first by the mtime of the path in the line's last tab-separated field (`--repo` stats `/.git/index` first). tv then ranks match quality and frecency on top of that order. All custom cables set `shell = "bash"`, because `$SHELL` is fish.
+Every custom cable except `agents` (a fixed list, see [agents](#agents)) pipes its source through [`mtime-sort`](/shell/scripts#mtime-sort), which orders lines newest-first by the mtime of the path in the line's last tab-separated field (`--repo` stats `/.git/index` first). tv then ranks match quality and frecency on top of that order. All custom cables set `shell = "bash"`, because `$SHELL` is fish.
## Custom cables
@@ -72,8 +72,10 @@ Every custom cable pipes its source through [`mtime-sort`](/shell/scripts#mtime-
| [`git-files`](#git-files) | `cable/git-files.toml` | ctrl-e |
| [`pj`](#pj) | `cable/pj.toml` | ctrl-p, down on an empty prompt (`pj`) |
| [`files`](#files) | `cable/files.toml` | `tv`, ctrl-t autocomplete |
+| [`obsidian`](#obsidian) | `cable/obsidian.toml` | `tv obsidian` |
+| [`agents`](#agents) | `cable/agents.toml` | ctrl-a in fish (`_agents_bind`) |
-The keys are bound in fish (`fish_user_key_bindings.fish`), zsh (`keybinds.zsh`), and bash (`keybinds.bash`). See [fish](/shell/fish#key-bindings) and [bash and zsh](/shell/bash-zsh).
+The keys are bound in fish (`fish_user_key_bindings.fish`), zsh (`keybinds.zsh`), and bash (`keybinds.bash`), except ctrl-a, which only fish binds to `agents` (zsh's ctrl-a still runs `omp`). See [fish](/shell/fish#key-bindings) and [bash and zsh](/shell/bash-zsh).
### ghq-repos
@@ -116,3 +118,30 @@ Source `pj --list`; tv shows the display column. The preview runs `pj --preview`
Overrides tv's stock `files` cable. Sources `Default` (`fd -t f`) and `Hidden` (`fd -t f -H`), both piped through `mtime-sort`. The `edit` action opens the selection in `nvim` on `/dev/tty`, so it also works from ctrl-t, where the shell captures tv's stdout.
**Needs:** `fd`, `bat`, `mtime-sort`.
+
+### obsidian
+
+Entries are `namepath` for every directory under `~/Documents/Obsidian` (depth 3) that contains `.obsidian`, named relative to that directory (nested vaults show as `Research/Sonr`) and sorted with `mtime-sort`. The preview is `eza --tree --level 2`.
+
+| Key | Action |
+| --- | --- |
+| enter | `open`: `xdg-open obsidian://open?path=`, detached with `setsid -f` because the generic `xdg-open` stays in the foreground |
+| ctrl-e | `edit`: `cd && nvim .` |
+
+`obsidian://open?path=` only resolves vaults listed in `~/.config/obsidian/obsidian.json`, and Obsidian reads that file once at startup and rewrites it from memory. So when the vault is not listed and Obsidian is closed, `open` adds `{path, ts}` under a random 16-hex id first. When Obsidian is running, it sends a notification and opens the vault switcher (`obsidian://choose-vault`) for **Open folder as vault**.
+
+**Needs:** `fd`, `jq`, `eza`, `obsidian`, `xdg-open`, `mtime-sort`.
+
+### agents
+
+The source is a table of known agent CLIs in the cable, `binary|launch command|description`, kept in table order (`omp` first). A row is listed only when `command -v ` resolves, so agents appear and disappear as they are installed. Entries are ``; the description is part of the matched text, so `openai` finds `codex`.
+
+| Agents in the table | Launch |
+| --- | --- |
+| `omp`, `claude`, `codex`, `opencode`, `crush`, `pi`, `cursor-agent`, `copilot`, `gemini`, `agy`, `grok`, `muse`, `ori`, `hermes`, `amp`, `droid`, `qwen`, `kimi`, `goose`, `aider`, `auggie`, `kiro-cli`, `cn`, `fractal` | The binary, except `claude --dangerously-skip-permissions` (same as `ai`) |
+
+The preview shows the launch command, the path on `PATH`, its `readlink -f` target, and, for the `~/.local/bin` wrappers that install the agent through mise on first run, the mise tool. It never runs the agent, since some wrappers take seconds to answer `--version`.
+
+enter runs `launch` in execute mode: tv exits and the launch command runs in its place, in the current directory. Before that it writes `ran` to `$AGENTS_STATE` when set, which is how `_agents_bind` tells a started agent from a cancel. Plain `tv agents` works the same way.
+
+**Needs:** nothing beyond the agents themselves.