I was pair programming over Zoom, sharing my terminal, and my partner asked me to scroll back to see a previous error. I couldn't. Terminal history doesn't scroll in screen share. That small friction made me finally learn tmux session management properly.

A few days later I watched a video by DDH about his tmux workflow with Opencode and Claude Code. Editor and AI assistant side by side, both aware of each other's state. I wanted that same flow, but with Neovim and Opencode instead.

What I ended up with is a developer productivity workflow I call tdev. It's a single command that spins up a complete development environment with Neovim and Opencode side by side, connected via a local port for context sharing. Honestly, the whole point is keeping editor and AI assistant in sync: when I ask Opencode to "fix this function," Neovim has already told it which file and line I'm looking at. No more juggling terminals. No more losing state when I disconnect.

My tmux development workflow showing Neovim with file tree on the left, Opencode AI panel on the right, and terminal window below

What tdev Actually Does

Running tdev in any project directory creates a tmux session with three components:

  1. A dev window with two panes: Neovim on the left (80% width) and Opencode on the right (20%)
  2. A terminal window for shell commands, git, tests, whatever doesn't fit in the editor
  3. Automatic port allocation so Neovim can send commands to Opencode's HTTP server

Here's the script that makes it happen:

#!/usr/bin/env bash
set -euo pipefail

TARGET_DIR="$PWD"
SESSION_NAME="${1:-$(basename "$TARGET_DIR")}"

# Attach to existing session if it exists
if tmux has-session -t "=$SESSION_NAME" 2>/dev/null; then
  echo "Session '$SESSION_NAME' already exists. Attaching..."
  exec tmux attach-session -t "=$SESSION_NAME"
fi

# Cleanup on error (only for new session creation)
trap 'if tmux has-session -t "=$SESSION_NAME" 2>/dev/null; then tmux kill-session -t "=$SESSION_NAME"; fi; exit 1' ERR

# Find next available port in the dynamic range (49152-65535 per IANA)
# This avoids conflicts with well-known ports (0-1023) and registered ports (1024-49151)
PORT=$(ss -tln | awk '{print $4}' | grep -oE '[0-9]+$' | sort -n | awk 'BEGIN{p=49152} {if($1==p) p++} END{print p}')

# Create session with nvim pane
NVIM_PANE=$(tmux new-session -d -s "$SESSION_NAME" -n dev -e "OPENCODE_PORT=$PORT" -P -F "#{pane_id}" "nvim .; exec $SHELL")
tmux select-pane -t "$NVIM_PANE" -T "Editor"

# Split and add opencode
OPENCODE_PANE=$(tmux split-window -h -p 20 -t "$NVIM_PANE" -P -F "#{pane_id}" "opencode --port $PORT; exec $SHELL")
tmux select-pane -t "$OPENCODE_PANE" -T "Opencode"

# Add a terminal window
tmux new-window -t "$SESSION_NAME" -n terminal -c "$TARGET_DIR"

# Remove error trap - session created successfully
trap - ERR

# Switch to dev window and attach
tmux select-window -t "$SESSION_NAME:dev"
tmux select-pane -t "$NVIM_PANE"
tmux attach-session -t "$SESSION_NAME"#!/usr/bin/env bash
set -euo pipefail

TARGET_DIR="$PWD"
SESSION_NAME="${1:-$(basename "$TARGET_DIR")}"

# Attach to existing session if it exists
if tmux has-session -t "=$SESSION_NAME" 2>/dev/null; then
  echo "Session '$SESSION_NAME' already exists. Attaching..."
  exec tmux attach-session -t "=$SESSION_NAME"
fi

# Cleanup on error (only for new session creation)
trap 'if tmux has-session -t "=$SESSION_NAME" 2>/dev/null; then tmux kill-session -t "=$SESSION_NAME"; fi; exit 1' ERR

# Find next available port in the dynamic range (49152-65535 per IANA)
# This avoids conflicts with well-known ports (0-1023) and registered ports (1024-49151)
PORT=$(ss -tln | awk '{print $4}' | grep -oE '[0-9]+$' | sort -n | awk 'BEGIN{p=49152} {if($1==p) p++} END{print p}')

# Create session with nvim pane
NVIM_PANE=$(tmux new-session -d -s "$SESSION_NAME" -n dev -e "OPENCODE_PORT=$PORT" -P -F "#{pane_id}" "nvim .; exec $SHELL")
tmux select-pane -t "$NVIM_PANE" -T "Editor"

# Split and add opencode
OPENCODE_PANE=$(tmux split-window -h -p 20 -t "$NVIM_PANE" -P -F "#{pane_id}" "opencode --port $PORT; exec $SHELL")
tmux select-pane -t "$OPENCODE_PANE" -T "Opencode"

# Add a terminal window
tmux new-window -t "$SESSION_NAME" -n terminal -c "$TARGET_DIR"

# Remove error trap - session created successfully
trap - ERR

# Switch to dev window and attach
tmux select-window -t "$SESSION_NAME:dev"
tmux select-pane -t "$NVIM_PANE"
tmux attach-session -t "$SESSION_NAME"

The thing about OPENCODE_PORT is that when Opencode starts with --port $PORT, it exposes an HTTP server with endpoints that Neovim uses to send commands to Opencode. So when I ask Opencode to "fix the function in the current buffer," Neovim has already told it which file and line I'm looking at.

But having two panes is useless if switching between them is annoying. The Neovim tmux integration comes from vim-tmux-navigator so that Ctrl+h/j/k/l works identically whether I'm in a Vim split or a tmux pane.

In Neovim, the same keys navigate between editor splits. When I reach the edge of Vim and hit Ctrl+l again, it jumps to the tmux pane on the right. The mental model is simple: I'm moving between editing surfaces, not thinking about "application boundaries."

I changed my prefix to Ctrl-Space because Ctrl-b conflicts with readline's backward-char. The window switching with Alt+Number is what I actually use daily: no prefix key, just Alt+2 to jump to the terminal window. It's fast enough that I actually use multiple windows now instead of cramming everything into splits.

The full configuration is in my dotfiles.nix repo under modules/shell/. The tmux setup is in tmux.nix, the tdev script is in scripts/tdev.sh, and the Neovim navigator config is in modules/neovim/_tmux.nix. Everything is managed as a Nix flake, so the whole setup is reproducible across machines.

The Hidden Cost: Quick Edits Become a Production

The tdev workflow is great for deep work. But it's overkill when I just need to change one line in a config file. Spinning up a whole tmux session with two panes, port allocation, and window management for a 30-second edit feels like bringing a deployment pipeline to a knife fight.

Sometimes I just open Neovim directly in those cases. But then I lose the context isolation: my terminal history mixes with whatever I was doing before, and if I need the AI assistant halfway through, I have to either start it manually in a split or fire up tdev anyway.

This is the trade-off nobody talks about with fancy development environments: they optimize for the long session but add friction to the quick task. I still haven't found a clean way to collapse this spectrum into one command.

Why This Beat My Old Workflow

Before tdev, I'd open a terminal, start Neovim, forget to start Opencode, then realize I wanted AI help and have to open another terminal or split. Or I'd start Opencode first and it wouldn't have editor context.

Now it's one command. The environment is consistent every time. I can detach with Ctrl-Space d, go to lunch, and reattach from my laptop with tmux attach and everything is exactly where I left it.

The session per project approach also keeps context isolated. My "dotfiles.nix" session has its own terminal history, its own opencode conversation, its own editor state. Switching projects means switching sessions, not wiping mental context.

FAQ

Q Does this work without NixOS?
A Yes, the tdev script is just bash. You can adapt the tmux configuration to your package manager of choice.
Q What terminal emulator do you use?
A I use WezTerm, but this works with any terminal that supports 256 colors and proper key forwarding.
Q Can I use this with Claude Code instead of Opencode?
A Absolutely. Just replace the opencode command in the script with your preferred AI coding assistant.
Q Why Ctrl-Space as prefix instead of Ctrl-b?
A Ctrl-b is the default but it conflicts with readline (backward char). Ctrl-Space is unbound and easier to reach.
Q Do I need a Nerd Font?
A Recommended but not required. Tokyo Night theme works fine without it, though some status icons may not render.
Asaduzzaman Pavel

About the Author

Asaduzzaman Pavel is a Software Engineer who actually enjoys the friction of a well-architected system. He has over 15 years of experience building high-performance backends and infrastructure that can actually handle the real-world chaos of scale.

Currently looking for new opportunities to build something amazing.