Turn a comment into a command by pressing TAB.
It feels like talking to your terminal. You type what you want as a quick note to yourself, hit TAB, and your prompt fills with a real command you can review. Sometimes you tweak it. Sometimes you just run it. Either way, you stay in control.
This is a zsh plugin, and it plays nicely with Oh My Zsh. Most importantly: it keeps your normal TAB completion. It only steps in when you ask for it.
Type a comment and press TAB:
# list commits between 869b1373 and f1b8edd0, oldest firstAfter TAB, your prompt keeps your request and adds command(s) below:
# list commits between 869b1373 and f1b8edd0, oldest first
git rev-list --reverse 869b1373..f1b8edd0Safety line (worth repeating): it never runs anything. It only inserts text into your prompt.
- If the current line starts with
#, TAB treats it as a request and generates zsh command(s). - If it does not start with
#, TAB behaves exactly like it did before.
Pick a prefix:
# <request><TAB>generate command(s)#? <question><TAB>explanation mode; prints an answer to your terminal (does not edit your prompt)
The persistence behavior is what makes iteration feel nice: keep your draft in the prompt and press TAB again.
When you use persistence, your prompt becomes a tiny scratchpad. You're not "chatting". You're drafting a command.
- First TAB gets you a rough draft.
- Next TAB is a revision pass: you tweak your notes (add another
# ...line, or edit an existing one) and press TAB again. - Because the old draft stays in the buffer, the agent can refine it instead of reinventing it.
That means you can do this:
- Ask for a rough draft:
# list all .py files under this folder, one per line<TAB>- Refine the request and press TAB again (add another
# ...line anywhere you like):
# but exclude files with _test_ in the filename<TAB>- Add the action you actually want:
# now show line counts for each file and sort biggest first<TAB>If you ever want to override the default behavior:
- Use
#+to force persistence for a single request. - Use
#-to force a "commands only" reply (no echoed# ...prompt lines).
You can keep iterating until it looks right, then run it. Still safe: it never runs anything for you.
Click to expand the TLDR section on how it works internally
- ZLE widget: intercepts TAB and triggers only on
# ...lines. - Controller (
src/controller.zsh):- starts the worker process
- shows the Knight Rider spinner while the worker runs
- updates
BUFFERwith the generated result on success
- Worker (
src/opencode_generate_command.py):- runs
opencode run --format jsonand parses NDJSON events - returns
sessionID<US>repro_cmd<US>agent_reply(US = ASCII Unit Separator, 0x1f) so the controller can split it
- runs
- Spinner (
src/spinner.zsh): rendering-only; draws viaBUFFER+region_highlight.
- zsh >= 5.1
python3opencodeCLI inPATH- (Optional) an opencode server running on your machine or on your premises for attach/warm-start mode
Note: this plugin targets macOS and Linux. If you use Windows, run it under WSL.
Note: the export configurations shown below are just examples. For the full list, see Configuration.
Important
If you use other plugins that customize TAB completion (especially fzf-tab), you have two options:
- Load
zsh-opencode-tablast (afterfzf-taband anything else that re-binds TAB). - Or keep TAB fully owned by your completion plugin and bind this plugin to another key via
Z_OC_TAB_BINDKEY.
Non-technical explanation (TL;DR)
In zsh, a key press can only call one thing.
This plugin respects what is already bound to the chosen key (Z_OC_TAB_BINDKEY): it steps in only when the prompt line begins with # ...; in all other cases, it calls the original widget that was bound to that key.
If another plugin grabs the same key after this one loads (common with fzf-tab), zsh will call that newer binding directly and this plugin will never see the key press.
On this basis, we recommend option 1) because the binding of this plugin is triggered on a very specific criterion and is unlikely to interfere with any other plugins already installed on your system.
Oh My Zsh:
- Clone this repo into your custom plugins directory:
git clone https://github.com/alberti42/zsh-opencode-tab.git "$ZSH_CUSTOM/plugins/zsh-opencode-tab"- Add it to your
.zshrc:
export \
Z_OC_TAB_OPENCODE_MODEL="anthropic/claude-3-5-haiku-latest" \
Z_OC_TAB_EXPLAIN_PRINT_CMD='bat --plain --color=always --decorations=always --language=markdown --paging=never {}'
# IMPORTANT: keep this after plugins like fzf-tab that re-bind TAB.
plugins+=(zsh-opencode-tab)
# Alternative: bind to a different key (example: Ctrl-G) to avoid TAB conflicts.
# export Z_OC_TAB_BINDKEY='^G'- Reload your shell:
exec zshDo-it-yourself:
-
Clone this repo into your desired location (e.g., "$HOME/local/share/my-zsh-plugins")
-
Source it from your
.zshrc:
Z_OC_TAB_OPENCODE_MODEL="anthropic/claude-3-5-haiku-latest"
# Optional: bind to a different key than TAB (example: Ctrl-G)
# export Z_OC_TAB_BINDKEY='^G'
source "$HOME/local/share/my-zsh-plugins/zsh-opencode-tab/zsh-opencode-tab.plugin.zsh"zinit:
Optional: if you want to bind this plugin to a different key than TAB, add e.g. Z_OC_TAB_BINDKEY="^G" to the atinit export list.
zinit lucid wait depth=1 from'gh' compile for \
wait'0c' atinit'export Z_OC_TAB_OPENCODE_MODEL="anthropic/claude-3-5-haiku-latest" \
Z_OC_TAB_SPINNER_BG_HEX="#24273A" \
Z_OC_TAB_OPENCODE_RUN_MODE="cold" \
Z_OC_TAB_EXPLAIN_PRINT_CMD="bat --plain --color=always --decorations=always --language markdown --paging=never {}"' \
@alberti42/zsh-opencode-tabWrite a request preceded by # and press TAB (or your configured key via Z_OC_TAB_BINDKEY). The plugin updates your prompt with generated command(s), ready to edit/run.
- If the line does not start with
#, TAB behaves as usual (your original widget is preserved). - When you press TAB, the generator agent receives your whole prompt buffer (including any previous draft you kept there). This is what makes iteration work: you can refine the request without losing context.
- Magic prefixes:
# <request><TAB>: generate command(s).- By default, your
# ...prompt stays in the buffer above the output.
- By default, your
#? <request><TAB>: explanation mode; prints the explanation to the terminal viaZ_OC_TAB_EXPLAIN_PRINT_CMD(default:cat).- It does not insert the explanation into the buffer.
- If you configure it to use
bat, make surebatis installed and inPATH.
One small convention that makes iteration nice:
- Lines that start with a single
#are treated as your prompt notes. - If the agent needs to add notes, it uses
## ...(double hash). That makes it easy to tell what you wrote vs what the agent added.
Type each request line (starting with #) and press TAB.
The plugin inserts text into your prompt so you can review it; it does not execute anything.
Demo clip:
zsh-opencode-tab-demo.mov
Example: list commits in a SHA range (in chronological order):
# give me the git command to list (using rev-list) the commits between 869b1373 and f1b8edd0, oldest first
git rev-list --reverse 869b1373..f1b8edd0Example: iterate over fd results and print resolved paths:
# give me a for-loop to iterate over `fd -e zsh`; print the resolved path for each file
for file in $(fd -e zsh); do print "$(realpath "$file")"; doneAll settings are resolved once when the plugin is loaded.
To change them, update your .zshrc and reload your shell (exec zsh).
For the best-looking "Knight Rider" fade effect, set the spinner background color to match your terminal background.
# Default behavior for plain `# ...` requests.
# 1: keep the request line above the generated command(s) (default)
# 0: replace the buffer with generated command(s)
export Z_OC_TAB_PERSIST_DEFAULT='1'
# Backend server URL (optional).
# Used for:
# - attach mode (warm-start), if you want it
# - deleting sessions in attach mode (cold mode deletes locally)
export Z_OC_TAB_OPENCODE_BACKEND_URL=''
# How to run opencode:
# - cold (default): run `opencode run` locally (most reliable)
# - attach: run against the backend server (faster if your server is warm)
export Z_OC_TAB_OPENCODE_RUN_MODE='cold'
# Debug: bypass opencode and return a dummy reply immediately.
# Useful to iterate on UI/integration without making remote calls.
export Z_OC_TAB_OPENCODE_DUMMY=0
# Optional: what to insert into the prompt when dummy mode is enabled.
export Z_OC_TAB_OPENCODE_DUMMY_TEXT="echo 'hello from dummy mode'"
# Speed (seconds per frame)
export Z_OC_TAB_SPINNER_INTERVAL='0.03'
# Message shown after the bar
export Z_OC_TAB_SPINNER_MESSAGE='Please wait for the agent ...'
# IMPORTANT: set this to your terminal background color.
# Tip: use a color picker / eyedropper to measure the hex color of your terminal background.
export Z_OC_TAB_SPINNER_BG_HEX='#24273A'
# Explanation mode output command (printed to the terminal).
# Use '{}' as the placeholder for the temporary file path.
export Z_OC_TAB_EXPLAIN_PRINT_CMD='bat --plain --color=always --decorations=always --language=markdown --paging=never {}'How to pick the right color:
- Use any "eyedropper" / color picker tool, click your terminal background, and copy the hex value (like
#1e1e1e). - On macOS, the built-in "Digital Color Meter" app can do this.
Most people never need these. They are here if you want to fine-tune the feel of the spinner (speed, colors, fading), or control how opencode is invoked (model, logging).
Click to expand the full list
The plugin reads these environment variables at load time:
Z_OC_TAB_DEBUG(default:0)- Enable debug behavior (internal).
Z_OC_TAB_DEBUG_LOG(default:/tmp/zsh-opencode-tab.log)- Path to append debug logs to when
Z_OC_TAB_DEBUG=1.
- Path to append debug logs to when
Z_OC_TAB_BINDKEY(default:^I)- Which key sequence triggers this plugin (bindkey notation).
- Default is TAB (i.e.,
^I). Example alternative:^G(Ctrl-G). - If another plugin re-binds TAB (e.g.
fzf-tab), set this to a different key to avoid conflicts.
Z_OC_TAB_PERSIST_DEFAULT(default:1)- Default persistence for plain
# ...requests. 1: keep the request line above the generated output.0: replace the buffer with the generated output.
- Default persistence for plain
Z_OC_TAB_EXPLAIN_PRINT_CMD(default:cat)- Command used to print
#?explanation output to the terminal. - Use
{}as a placeholder for the temporary file path. - Keep it simple: the value is split on spaces.
- Command used to print
-
Z_OC_TAB_SPINNER_MESSAGE(default:AI agent is busy ...)- Message shown after the spinner bar.
-
Z_OC_TAB_SPINNER_MESSAGE_FG(default: empty)- Truecolor hex foreground for the message, e.g.
#cfcfcf.
- Truecolor hex foreground for the message, e.g.
-
Z_OC_TAB_SPINNER_HUE(default:280)- Base hue (0..360).
-
Z_OC_TAB_SPINNER_SATURATION(default:0.30)- Base saturation (0..1).
-
Z_OC_TAB_SPINNER_VALUE(default:1.0)- Base brightness (0..1).
-
Z_OC_TAB_SPINNER_INACTIVE_FACTOR(default:0.4)- Dim factor for inactive dots (0..1). This is "alpha-like" via background blending.
-
Z_OC_TAB_SPINNER_ENABLE_FADING(default:1)- Enable global dot fading (1/0).
-
Z_OC_TAB_SPINNER_MIN_ALPHA(default:0.0)- Minimum fade factor for dots (0..1).
-
Z_OC_TAB_SPINNER_BG_HEX(default:#24273A)- Background used for blending and as the bar background. Set it to your terminal background for best results.
-
Z_OC_TAB_SPINNER_INTERVAL(default:0.03)- Seconds per frame (single speed knob).
-
Z_OC_TAB_SPINNER_POLL_S(default:0.005)- Poll interval (seconds) for reading the worker status FIFO.
-
Z_OC_TAB_OPENCODE_BACKEND_URL(default: empty)- URL of your opencode server.
- Used for attach mode.
-
Z_OC_TAB_OPENCODE_WORKDIR(default:$XDG_DATA_HOME/zsh-opencode-tab)- Working directory used for the
opencodesubprocess. - If
XDG_DATA_HOMEis empty, it falls back to${TMPDIR:-/tmp}/zsh-opencode-tab. - The plugin keeps sessions in the global workspace (not inside whatever git repo you happen to be in).
- The plugin writes its two bundled agents into
${Z_OC_TAB_OPENCODE_WORKDIR}/.opencode/agents/.
- Working directory used for the
-
Z_OC_TAB_OPENCODE_RUN_MODE(default:cold)cold: runopencode runlocally (no server attach).attach: runopencode run --attach <backend_url>.- If you set
attachbut leaveZ_OC_TAB_OPENCODE_BACKEND_URLempty, the plugin falls back tocoldand shows a warning.
-
Z_OC_TAB_OPENCODE_MODEL(default: empty)- Model in
provider/modelform. - Comprehensive list of providers/models: https://models.dev/
- Recommended: first try the model in a regular
opencodesession (outside this plugin) to confirm your provider credentials are set up and your account has credits/billing to use it. - This sets the model for both generation and explanation.
- Model in
-
Z_OC_TAB_OPENCODE_MODEL_GENERATOR(default: empty)- Optional: model override for command generation.
-
Z_OC_TAB_OPENCODE_MODEL_EXPLAINER(default: empty)- Optional: model override for explanation mode.
-
Z_OC_TAB_OPENCODE_AGENT_GENERATOR(default:shell_cmd_generator)- Agent name for command generation.
-
Z_OC_TAB_OPENCODE_AGENT_EXPLAINER(default:shell_cmd_explainer)- Agent name for explanation mode.
-
Z_OC_TAB_OPENCODE_VARIANT(default: empty)- Optional model variant.
-
Z_OC_TAB_OPENCODE_TITLE(default:zsh shell assistant)- Session title.
-
Z_OC_TAB_OPENCODE_LOG_LEVEL(default: empty)- Passes
--log-levelto opencode (DEBUG,INFO,WARN,ERROR).
- Passes
-
Z_OC_TAB_OPENCODE_PRINT_LOGS(default:0)- If set to
1, passes--print-logs.
- If set to
-
Z_OC_TAB_OPENCODE_DELETE_SESSION(default:1)- If set to
1, deletes the created session after receiving the answer. - In
attachmode: deletes via the server API (requiresZ_OC_TAB_OPENCODE_BACKEND_URL). - In
coldmode: deletes locally on disk (no server needed).
- If set to
-
Z_OC_TAB_OPENCODE_GNU(0or1; default:1)- Passed to the agent whether to prefer GNU tools over macOS/freeBSD.
This plugin bundles two agent definitions (prompt files) for opencode:
-
One tuned for generating zsh command(s).
-
One tuned for explaining commands and shell workflows.
-
Default generator agent:
shell_cmd_generator(definition:opencode/agents/shell_cmd_generator.md). -
Default explainer agent:
shell_cmd_explainer(definition:opencode/agents/shell_cmd_explainer.md). -
Custom agents: set
Z_OC_TAB_OPENCODE_AGENT_GENERATORand/orZ_OC_TAB_OPENCODE_AGENT_EXPLAINER. -
Custom prompts: copy the agent file under
${Z_OC_TAB_OPENCODE_WORKDIR}/.opencode/agents/to a new filename and select it usingZ_OC_TAB_OPENCODE_AGENT_GENERATORandZ_OC_TAB_OPENCODE_AGENT_EXPLAINER. Note that the plugin overwrites its own bundled filenamesshell_cmd_generator.mdandshell_cmd_explainer.mdto keep upgrades deterministic; thus, avoid modifying the bundled agents.
Tip: when you are iterating on the agent prompt, use cold start (Z_OC_TAB_OPENCODE_RUN_MODE=cold). It's the least confusing setup: you edit a file, reload your shell, and the next TAB uses it.
-
Cold start (default): simplest and most reliable.
- You do not run/attach to any server.
- Each TAB request runs
opencodefromZ_OC_TAB_OPENCODE_WORKDIR, which keeps sessions in the global workspace.
-
Attach mode (optional): same experience, less startup overhead.
- Run an opencode server and set:
Z_OC_TAB_OPENCODE_RUN_MODE='attach'Z_OC_TAB_OPENCODE_BACKEND_URL='http://127.0.0.1:4096'
- The plugin already keeps its two agent files under
${Z_OC_TAB_OPENCODE_WORKDIR}/.opencode/agents/, and attach mode can use those too.
- Run an opencode server and set:
Warning
Known upstream rough edges (so you're not surprised):
- Attach mode may ignore which helper you asked for.
- Under the hood, opencode is supposed to honor
--agent shell_cmd_generatoror--agent shell_cmd_explainerwhen you attach to a server. - Upstream status: broken right now; a fix exists but is not merged yet: anomalyco/opencode#11812
- For now, we recommend using cold start (default).
- Under the hood, opencode is supposed to honor
- Password-protected server can be buggy upstream ("Unauthorized" on attach even with the right password): anomalyco/opencode#9095
- Nothing happens on TAB:
- The plugin only triggers when the line starts with
#. - If you use
fzf-tab(or any plugin that re-binds TAB), loadzsh-opencode-tablast or setZ_OC_TAB_BINDKEYto another key. - Quick check (default TAB):
bindkey -M emacs '^I'should point to_zsh_opencode_tab_or_fallback. - Quick check (example Ctrl-G):
bindkey -M emacs '^G'should point to_zsh_opencode_tab_or_fallback.
- The plugin only triggers when the line starts with
- The spinner runs but the buffer does not change:
- Ensure
opencodeis inPATH. - If using attach mode, ensure the opencode server is running at
Z_OC_TAB_OPENCODE_BACKEND_URL. - Temporarily set
Z_OC_TAB_OPENCODE_LOG_LEVEL=DEBUGandZ_OC_TAB_OPENCODE_PRINT_LOGS=1.
- Ensure
Idea inspired by https://github.com/verlihirsh/zsh-opencode-plugin.
- Author: Andrea Alberti
- GitHub Profile: alberti42
- Donations:
Feel free to contribute to the development of this plugin or report any issues in the GitHub repository.
This project is licensed under the MIT License. See the LICENSE file for details.
If you want to get a feel for it in 10 seconds: install it, open a new terminal, type a line starting with #, and press TAB.
If it clicks, consider starring the repo: https://github.com/alberti42/zsh-opencode-tab
