Featured image of post 逃离 IDE:我的纯终端 AI 编程工作流实践

逃离 IDE:我的纯终端 AI 编程工作流实践

在 AI Coding 时代,我反而比以前更频繁地使用终端。这听起来有点反直觉——既然有 Cursor 这类把 AI 深度集成进 IDE 的工具,为什么还要"回到"终端?如果你以为我讲终端就是聊 Claude Code 等 CLI,其实并不是!我们开发者还有很多要解决的体验问题,本文尝试给一整套可照抄的纯终端开发解决方案,希望对你有启发。

前言

很多年前,我是个彻底的 Vimer,所有编辑都在终端里完成,随便精进着 Vim 的技巧,运指如飞,手不离键,并且很享受这种状态。我的博客陆续分享过多次相关经验,但随着 VSCode + Copilot、Cursor 这些 AI 加持的 IDE 出现,效率提升明显,我别无他法只能改换门庭 —— 投入了 VSCode / Cursor 的怀抱。直到最近一年,当我开始频繁使用 Codex、Claude Code、OpenCode 这类终端 Agent,我们甚至不再需要去手工编辑代码,程序员的工作模式发生了根本的变化,突然我发现,是时候”回家”了。

回家不是因为恋旧,是因为当前模式有一些痛,比如我受不了每天要将 N 多个 Cursor(VSCode)的界面重连一遍,受不了在无数个窗口中来回寻找,受不了合上盖子它就停止工作。而这些痛点,其实都有解法。

AI 如是说:

终端不是退化,而是一个更稳定、可恢复、可长期运行的工作台。

我说:“你说得对!”。

我的终端开发工作流总览

我主要使用 macOS 系统,我的方案主要是基于 iTerm2 + tmux + Agent 开发定制功能实现的全流程开发工作流。

简单示意大概是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
iTerm2 Profile(本地入口)
SSH / 本地 Shell
tmux session(每个 session 对应一个项目)
   ├── window: agent   ← Codex / Claude Code 在跑
   ├── window: test    ← 跑测试 / 看输出
   ├── window: logs    ← tail -f 一类的常驻日志
   ├── window: lazygit ← 提交前驾驶舱
   └── popup: temp shell(临时命令,关掉即销毁)

效果图大概是这样的: tmux纯终端开发效果

几个关键组件的分工:

  • iTerm2:它是我爱用的终端,当然你可以选择自己喜欢的。作为本地入口,我设定了 profile 区分不同机器、不同环境。
  • tmux:提供了会话复用,我再基于它的理念设计了一套工作编排方式,并且在 tmux 上进行了比较丰富的设定与扩展,下文会详细道来。
  • AI Agent(Codex / Claude Code):选个你喜欢的 Agent cli 跑,它们总是跑在 tmux window 里,提供了任务的连续性。

基于 iTerm2 的多终端区分与管理

iTerm2 我用得比较克制,比如我知道它有类似 Tmux 的分屏(pane)等功能,但是我不用,因为我们并不想被绑死在某一个终端软件上,不过我还是利用它的 Profile 机制做"环境区分":

  • 每个常用机器(家里设备、办公电脑、腾讯云 dev等)一个 profile;
  • 不同 profile 用不同的背景色——这是一个非常便宜但有效的"误操作防护"。比如生产机用偏红的背景,开发机用偏绿的背景,眼睛一扫就知道当前 shell 在哪。
  • 每个 profile 的 “Login Shell” 配置成 SSH 到目标机器后自动 attach 到一个固定名字的 tmux session

最后一条很关键。比如我在 iTerm2 的 Profile 的 Command 部分像这样:

1
ssh -t dev 'exec "${SHELL:-/bin/bash}" -lc "exec tmux new-session -A -s dev"'

我们确保一进入便是在 Tmux 管理之下,再也不怕会话丢失了。昨天那个打开的窗口在哪里的问题,或许一去不复返了。iTerm2 这边的配置就这么多,接下来才是真正精彩的部分。不过看这个之前,为了后续你更好的利用 Agent,我建议你不要忘了看我另一篇文章:Vibe Coding 远程开发时,如何优雅地贴图?,它会让后面你的开发更加原生和爽快一些。

基于 Tmux 的终端复用

Tmux 是这套工作流真正的”地基”,有人说:这么一款神器软件,它居然是完全免费可随意使用的,不用真的很亏。我觉得他说得很对。

它的几个核心抽象——session、window、pane——刚好对应我们开发中的三个层次:

  • session = 项目。每个项目一个 session,互不干扰;加上我们各种自主命名和可定制化的界面管理,让你”身轻如燕”般穿梭于各个项目。
  • window = 这个项目下的一个工作任务:跑 Agent、跑测试、看日志、操作 git,对于这个项目的各种场景,随便开个跑;我的习惯专事专办,用完随便关掉。
  • pane = 临时分屏:有时跑个测试、编译啥的要盯着可以。以前我爱用 pane,慢慢的其实分层清晰后,它用得更少了,真到要临时分屏时,我另有招数(下文会讲)。

关于 Tmux 有太多文章介绍,我觉得抄什么功能介绍是毫无价值的,但我要说的有点不一样,我们谈点真实感受和技巧。相关配置我已经放到 GitHub中 dotfiles-example,你可以随时取用。

1. 快捷键设置

我个人长期使用过另一个窗口管理软件,Ubuntu 上带的 byobu。它底层可基于 Tmux或 Screen等。它定义了不少快捷键其实挺好用的。不过用久了后有几个问题,太多键位其实你也用不上,多了反而容易误按以及和自建的冲突。为了追求极简我重新设定了一些规则。我挑几个重点说下:

  • tmux 默认 prefix 是 C-b,按起来不顺。byobu 默认是 C-a 或者是 F12,这也不顺手,用 C-a 对快捷键党更不友好(会冲突跳行首)。我建议换成 C-s,反正你早就习惯 Ctrl + s 了:)
  • byobu 风格的直接快捷键以 Fn 功能键居多,其实挺好用,我也继承和改良了一些,同时去掉我较少用的。
1
2
3
4
5
6
7
8
bind-key -n F2 new-window -c "#{pane_current_path}"
bind-key -n F3 previous-window
bind-key -n F4 next-window
bind-key -n F6 detach
bind-key -n F7 copy-mode
bind-key -n F8 command-prompt -p "(rename-window) " "rename-window '%%'"
bind-key -n S-F11 resize-pane -Z
bind-key -n F5 source-file ~/.tmux.conf \; display-message "tmux config reloaded"
  • F2 新建 window 时一定要 -c "#{pane_current_path}"新 window 默认继承当前目录——包括 tmux 默认新建 window 的行为,我也建议都改成进入当前 pane 所在目录,这会让你开心不少;
  • F5 reload 配置,byobu就是这么设定的,特别是咱们调整 tmux 配置的时候一键生效还是很便捷的。

更多的设定看仓库中相关文档,有byobu风格的延续,也有 VIMER 喜欢的各种切换和跳转,不在这里赘述。

2. 关键且有用的几个配置项

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 鼠标模式:可以点击切 pane、拖边框调大小、滚轮翻历史
set -g mouse on

# 自动命名 window,但禁止应用程序通过 OSC 改名
set -g automatic-rename on
set -g allow-rename off

# 滚屏历史尽量大
set -g history-limit 100000

# 跟系统剪贴板打通
set -g set-clipboard external

allow-rename off 这一条是被 Claude Code 教育出来的——某些 CLI 工具会通过 OSC 转义序列把版本号、临时状态写到 window 名里,眼花缭乱。关掉之后,window 名只受 tmux 和我自己控制,整洁多了。

automatic-rename 的 format 我也做了一点定制:

1
set -g automatic-rename-format '#{?#{==:#{pane_current_command},zsh},#{b:pane_current_path},#{?#{m/r:^(node|python3?)$,#{pane_current_command}},#(~/.config/tmux/bin/tmux-window-name #{pane_pid} #{pane_current_command}),#{pane_current_command}}}'

逻辑很简单:

  • zsh 空闲时显示当前目录名;
  • node / python 解释器在跑脚本时,显示实际脚本名而不是干巴巴的 node
  • 其他进程直接显示进程名。

这样状态栏一眼就能看出每个 window 在干嘛。

3. 复制粘贴:tmux-yank + 选区按键

复制粘贴在 tmux 中我们可以借助 tmux-yank 插件解决了"跨平台复制到系统剪贴板"的问题:macOS 上自动调用 pbcopy,Linux 上自动选 xclip / xsel / wl-clipboard

然后剩下的选区控制我用了 vim 风格:

1
2
3
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi V send-keys -X select-line
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle

v / V / C-v 分别是字符选区、行选区、矩形选区——和 vim visual mode 一模一样。

我觉得这还不够,想起来 Cursor 这类 IDE 有个把终端内容一键发送到 AI 聊天框的功能,我也想实现类似,怎么办?

如下这样配置一键抓取 pane 最后 N 行写到 tmux 粘贴缓冲区,然后在另一个pane中 prefix + ] 贴上去。

1
2
3
bind-key 1 run-shell 'tmux capture-pane -pJ -S - | tail -n 10 | tmux load-buffer -'
bind-key 2 run-shell 'tmux capture-pane -pJ -S - | tail -n 20 | tmux load-buffer -'
bind-key 5 run-shell 'tmux capture-pane -pJ -S - | tail -n 50 | tmux load-buffer -'

使用时,大概估摸个数,用 prefix + 1/2/5 抓最近 X 行报错粘贴到比如你的 Agent window 里说:“看看这个错”,比手动选区快很多。

4. 弹出式终端:临时命令的好去处

我之前在用一套 Neovim 配置时,有个弹出式终端是我喜爱的功能,咱虽然不用再开 vim 了,但这个功能 tmux就可以提供:

1
bind-key t display-popup -E -w 90% -h 90% -d "#{pane_current_path}" "TMUX_POPUP=1 exec zsh -i"

按下 prefix + t,浮起一个铺满 90% 屏幕的临时 shell,关掉就销毁,完全不影响当前 window 的布局。我经常用它做临时查看或修改某个文件的内容等,这还是蛮舒适的。

添加这个功能时,我注意到起初它弹出终端感觉有点延迟,于是配置里我塞了一个 TMUX_POPUP=1 的环境变量。popup 里跳过 nvm、goenv 这类重型版本管理器初始化,把弹窗打开速度从 1 秒降到 0.1 秒以内,秒开的感觉真爽。如果要用到完整的各种环境参数,则创建独立的 windows即可。速度优先,职责清晰。

5. 会话保存与恢复

其实我本来不喜欢用会话保存这种功能的,感觉能少就少,直到几次莫名其妙的会话丢失。这里我使用了 tmux-resurrect + tmux-continuum 是 tmux 老用户的两大神器。

1
2
3
4
5
6
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @continuum-interval '15'

set -g @resurrect-save 'S'
set -g @resurrect-restore 'R'
  • continuum 每 15 分钟自动调用 resurrect 保存当前 session 布局;
  • 想要恢复时,手动按 prefix + R

或许除了异常丢失会话,重启机器时也能派上用场。这玩意会话快照也太省了,我还想着要不要定时清理,7 天可能都用不了几 MB,放心大胆的“瞎搞”吧。

基于 Lazygit 的审查

现在不兴提 Vibe Coding 了是吗?你们写的代码会不会自己再审查下呢?反正多数时候我是需要的。有时候 Agent 改完一堆文件,我心里是没底的。当前多数的编码 Agent 在修改内容的呈现上都不太理想。特别是 cli 模式的,靠那”惊鸿一瞥”根本不足以判断它有没有夹带私货。所以我需要比较直接的呈现修改的 Diff,这点 Codex App 以及 Cursor Agent (cli) 表现稍好,它们给 Diff 设计了独立的展示 UI。

而纯终端下,我们有 lazygit 不光比较好的呈现了修改面,而且把 Git 管理也集成了进来,所有操作收敛到一个 TUI 里,键盘党友好。

我把 lazygit 集成进 tmux 的方式有两种:

1
2
3
4
5
# 在当前目录打开 lazygit 浮窗
bind-key g display-popup -E -w 90% -h 90% -d "#{pane_current_path}" "lazygit"

# 在当前目录打开 lazygit 专用 window,退出后自动关闭
bind-key G new-window -n lazygit -c "#{pane_current_path}" "lazygit"

prefix + g 是"快进快出"的浮窗模式,适合扫一眼现状;prefix + G 是开一个专用 window,请好好审查你的改动吧。 lazygit 默认 diff 风格

这是lazygit的默认 Diff 方式,其实不太友好,我还是喜欢双栏对照着看,这也简单:

diff 渲染接上 delta 作为 lazygit 的 pager:

1
2
3
4
git:
  pagers:
    - pager: delta --paging=never
      colorArg: always

delta 的 diff 排版、语法高亮、行内差异显示都比原生 git diff 强一个量级,配上 lazygit 之后,我基本不再为了看 diff 而专门打开 VSCode 跑 Code Agent 了。

lazygit 接入 delta 后的 diff 风格

我发现 lazygit 除了各种样式可定制外,还支持自定义 commands,这就有意思了。比如当我需要填写 commit message 时,我可以按 C-a,让 AI 给我生成多个版本的 commit 文案,我还可以交互式的选择一个自己满意的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
customCommands:
  - key: "<c-a>"
    context: "files"
    description: "Pick AI commit message"
    command: "git commit -m {{ .Form.Msg | quote }}"
    loadingText: "Generating AI commit messages..."
    prompts:
      - type: "menuFromCommand"
        title: "AI Commits"
        key: "Msg"
        command: "ai-commit-candidates"
        filter: "^(?P<msg>.+)$"
        valueFormat: "{{ .msg }}"
        labelFormat: "{{ .msg | green }}"

这也就是一个实验功能,其实多数时候我是开着 Agent的,让它帮我提交生成 commit message往往更符合规范。但你多个选择,随手改了几行功能等,也不失为一个偷懒的方法。

10x 程序员:Agent 运行状态可视化

作为一个熟练驱使各种 Agent 干活的人,他们在做事时你肯定也不想闲着,AI 已经被你“奴役“了,它们进度怎样是你关心的一个事情吧。所以良好的通知机制(状态感知)就至关重要。他们都在想抢你的关注,每一个输入窗口都“嗷嗷待哺“。

一旦我们多任务并行处理,现在到底应该看哪个 window 便是一个问题:哪个正在干活,哪个已经来汇报工作,哪个正在等你指示。程序员最讨厌低效的轮询了,事件触发是必须的。于是,我们可以利用 Agent 本身的 hook 机制,让它主动通知 tmux 的 window,我选择以修改 window 名称(添加 emoji)来反映状态。

很多 Agent都提供了hook机制,像如下的 Claude Code 配置,可以在 prompt 提交、运行结束、需要输入时分别触发脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/notify-tmux.sh running"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/notify-tmux.sh done"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/notify-tmux.sh input-required"
          }
        ]
      }
    ]
  }
}

对应的脚本只做一件事:根据当前状态在 window 名前面打一个 emoji 前缀,太简单的脚本都不值得展示。但效果就是tmux 状态栏上的 window 名会变成 🔄 agent-foo / ✅ agent-foo / ❓ agent-foo

如前面所说,多个项目同时推进时,每个 session 内部的 Agent 状态就成了一个新问题。同样的思路——让状态在 session list 里直接呈现。这个细节你可能发现我第一张图就有这些信息啦~现在咱们可以安心喝茶、高效监工了:)

后记

用上这套系统后,解决了我原本的很多琐碎的小烦恼:

  • 下班前进行到一半的工作,第二天不知道窗口在哪了。而且我受够了不断确认VSCode的远程重连了,它还慢得要命,它还更新的频繁:(
  • 为了看代码不得不打开“内存杀手“ VSCode/Cursor之流,并且得接受被一些无用的 UI 占据了宝贵的窗口空间。我的 Macbook Pro也只有小小的面积。
  • 还有很多好处,看完全文的你或能有更多体会:)

更要命的是,我的整洁的毛病,分门别类的感觉真好!当然,或许在某些需要浏览大量代码的时候,我还是会再次打开 VSCode 们,但显然它们已经失去我的心了。

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:爱折腾的风

扫码关注公众号