Context Mode:用 tmux 与 Shell 脚本打造秒级切换的上下文工作流

发布时间:2026/10/8 5:29:26
Context Mode:用 tmux 与 Shell 脚本打造秒级切换的上下文工作流
开发者的效率瓶颈往往不在手速而在状态切换。我今天想聊的这个话题是我自己折腾了很久才稳定下来的一套工作流名字就叫 context-mode。一句话说清楚它是干什么的把你的工作状态打包成一个个可保存、可切换、可恢复的“模式”。它适合所有需要在多个项目、多个任务之间来回横跳的人——写代码的、写文档的、做运营的、做分析的都可以用上。别急着划走这套方案不是要用什么重型框架底层只是 shell 脚本、tmux 和编辑器配置的组合复制就能用。我最早意识到这个问题是在一次同时维护三个项目之后。每个项目有不同的分支、不同的环境变量、不同的服务端口甚至打开的文件都不一样。我每天的工作就是反复在三个项目之间切换切过去要重新打开文件、重新起服务、重新回忆之前做到哪一步切回来又忘了刚才那边的进展。一整天下来真正写代码的时间没多少大部分时间都花在“找回现场”上。后来我认真算过一笔账每次切换后的恢复时间平均要五到十分钟一天切换五六次等于每天浪费接近一个小时。这个成本太吓人了所以我决定彻底解决它。1. 整体设计与思路拆解context-mode 的核心思路是把“工作状态”当作一个完整对象来管理。大多数人管理多个任务时习惯把状态拆散在浏览器标签、终端窗口、便签纸、聊天记录里真到用的时候东拼西凑永远凑不齐。而我想做的是给每一个任务或项目建立一套独立的“档案”里面包含目录、环境变量、编辑器状态、窗口布局、临时笔记以及当时的任务描述。这样切换的时候不是“一个个找回”而是“一键整体换入”。1.1 为什么需要一套“上下文模式”先搞清楚一个概念切换上下文真正贵的是什么。举个例子你正在写一段涉及用户登录逻辑的代码脑子里装满了token、session、刷新策略这些细节忽然同事问你要一份上周的数据报告。你不得不从代码里抽离出来打开Excel表格回忆哪些指标、什么口径写完再切回代码。这时候你会发现刚才脑子里那些关于登录逻辑的细节已经凉了一半得重新读一遍代码才能回到状态。这就是所谓的“上下文切换损耗”。与其说是时间成本不如说是认知成本。工作记忆的容量是有限的一旦被新任务占据旧任务的状态就会退化。浏览器开一百个标签页、终端开十个窗口根本不能算“管理状态”那只是把状态散落一地。context-mode 想解决的正是这种“状态散落”的问题。我给它打个比方普通工作方式就像把所有文件摊在一个大桌面上东西多到连自己都找不到。context-mode 则像给每一类工作准备一个独立的工位——文档工位、代码工位、汇报工位每个工位上的东西都提前摆好走进去就能干活不用先花时间清理和布置。1.2 设计目标与方案选型在设计这套方案之前我先定了四条硬指标避免做出来一个花架子切换要快最好三秒以内完成整套环境的恢复保存要轻不能依赖网络、不能依赖某个特定软件的服务端可维护所有状态文件都是纯文本能看懂、能改、能备份可扩展后期要能接入AI工具、同步到其他机器不至于推倒重来。带着这些要求我对比过几条路线。用容器或者虚拟机来隔离每个项目的环境确实非常干净但对日常开发来说太重了镜像构建、端口映射、目录挂载都要花时间而且编辑器怎么接进去也是个麻烦。用 IDE 自带的工作区功能比如 VS Code 的 workspace能保存一部分状态但它管不了终端窗口也管不了环境变量。最后我选择了“轻量组合”的方案能力维度用到的工具负责什么会话管理tmux保存终端窗口、shell 会话、进程状态存储纯文本文件env.sh、notes.md保存环境变量、任务说明、随手记编辑器状态Neovim / VS Code session记录打开的文件、光标位置快速选择fzf上下文列表的模糊搜索与切换统一入口shell 函数 ctx所有操作的入口封装底层工具这套组合最大的好处是每一层都是成熟可靠的工具我没有重复造轮子只是把它们用一条主线串起来。而且每一层都可以单独替换比如你不用 Neovim换成 VSCode 也能跑。这也是为什么我一直推荐别人用“组合”而不是“自研”的思路工具会过时思路不会。2. 核心细节解析与实操要点方案搭起来了真正决定好用不好用的是细节。context-mode 里最容易被忽略却又最影响体验的就是“上下文里到底要存什么”以及“怎么组织这些存储”。我踩过不少坑下面展开说说。2.1 一个上下文到底包含什么很多人以为上下文就是“记住当前目录”其实远远不够。我实际设计的最小子集包含七个要素工作目录进入上下文后的默认位置shell 环境变量项目用的端口、API 地址、当前 Git 分支、Python 虚拟环境等运行中的进程比如本地开发服务器、watch 命令、日志监听编辑器状态打开的文件、光标位置、未保存的会话任务说明这个上下文现在要做什么一句话写清楚随手笔记联想到的细节、待办、坑随时想到随时记参考资料常用链接、文档路径、设计稿位置。前四类属于“环境状态”负责把一个空壳子恢复成你离开前的样子后三类属于“思维状态”负责告诉你当时为什么在这里、接下来要做什么。我后来发现环境状态只解决了一半问题另一半要靠思维状态来补。没有任务说明和笔记即使所有窗口都恢复了你依然可能想不起来那个 localStorage 的临时修改是为了验证什么。2.2 上下文目录设计与状态存储我的方案里每个上下文对应 ~/.contexts/ 下的一个目录目录名就是上下文的名称。举例来说~/.contexts/ ├── blog-post/ │ ├── workdir # 唯一一行工作目录路径 │ ├── env.sh # 环境变量导出脚本 │ ├── prompt.md # 任务说明写给未来的自己 │ ├── notes.md # 随手笔记 │ └── session.vim # 编辑器会话 ├── demo-project/ ├── report-summary/全部用纯文本是刻意为之。这个设计有三个好处第一任何时候你把任意一个文件拖进编辑器都能直接看内容不用启动任何解析器第二文件之间互相独立某个文件坏了不影响整个上下文第三可以放进 Git 仓库或者网盘同步路径变了也容易处理。存储格式上我放弃了 YAML、JSON 之类的结构化格式。原因很简单上下文状态里任务说明和笔记的灵活度远大于结构化字段硬塞给 JSON 只会让转义字符满天飞。env.sh 本身已经是 shell 可执行的脚本两边统一用“纯文本 轻约定”复杂度和灵活性之间取了一个比较舒服的平衡点。2.3 命名与索引规则命名这件事看起来小实际影响非常大。早期我的上下文名称五花八门什么 project_a_final_v2、blog 0825、运维那个过两周自己都不知道谁是谁切换到错误上下文反而更浪费时间。后来我定了三条规则用“项目/任务”命名不用“技术栈”命名。比如叫 api-redesign不叫 go-service因为前者表达意图后者只表达技术选型有短期任务的在 notes.md 里写清楚截止日期和目标避免上下文越积越多每次 load 一个上下文就自动更新它的 last_used 字段。索引和搜索是另一个关键。我不可能每次用菜单方式去选上下文所以我把 fzf 接到了 ctx 命令上。ctx list 的管道输出直接作为 fzf 的输入键盘一按模糊匹配文件名、标签、甚至 prompt.md 里的关键词基本上一眨眼的功夫就能定位目标。整个过程不用鼠标不需要退出当前终端这是 context-mode 用起来“顺滑感”的重要来源。3. 实操过程与核心环节实现前面讲了设计这里进入真正的实现环节。我先把整个方案拆成六个执行步骤从零开始搭一套可以用的 context-mode。你可以直接复制这整套配置也可以只挑其中一部分参考。3.1 初始化创建上下文目录与命令入口第一步先建目录结构并定义一组 shell 函数。我习惯把这些代码放在 ~/.config/context-mode/ctx.sh 里然后在 .bashrc 或 .zshrc 中 source 它。export CONTEXT_DIR$HOME/.contexts mkdir -p $CONTEXT_DIR # 保存当前状态到指定上下文 ctx_save() { local name$1 [ -z $name ] { echo usage: ctx save name; return 1; } local dir$CONTEXT_DIR/$name mkdir -p $dir # 1. 记录工作目录 pwd $dir/workdir # 2. 保存关键环境变量避免PATH等高敏感变量干扰 env | grep -E ^(PORT|API_HOST|PYTHON_ENV|GIT_BRANCH|NODE_ENV|APP_ENV|MY_FLAG) $dir/env.sh # 3. 保存编辑器状态Neovim示例 if command -v nvim /dev/null [ -n $NVIM_SERVER ]; then nvim --server $NVIM_SERVER --remote-send :mksession! $dir/session.vimCR fi # 4. 写入当前 tmux 布局快照 tmux list-windows -t $name /dev/null 21 tmux list-panes -t $name -F #{window_index} #{pane_index} #{pane_current_path} $dir/tmux_layout echo context saved: $name } # 加载指定上下文 ctx_load() { local name$1 [ -z $name ] { echo usage: ctx load name; return 1; } local dir$CONTEXT_DIR/$name [ ! -d $dir ] { echo context not found: $name; return 1; } # 先恢复 tmux 会话存在则附加不存在则创建 if ! tmux has-session -t $name 2/dev/null; then tmux new-session -d -s $name -c $(cat $dir/workdir 2/dev/null || echo $HOME) fi # 恢复环境变量 [ -f $dir/env.sh ] source $dir/env.sh # 恢复编辑器会话 [ -f $dir/session.vim ] nvim -S $dir/session.vim # 切换到对应的 tmux 会话 tmux switch-client -t $name # 更新最近使用时间 touch $dir echo context loaded: $name } # 列出所有上下文 ctx_list() { for dir in $CONTEXT_DIR/*; do [ -d $dir ] || continue echo $(basename $dir) done } # 删除上下文 ctx_rm() { local name$1 [ -z $name ] { echo usage: ctx rm name; return 1; } rm -rf $CONTEXT_DIR/$name echo context removed: $name } # 模糊查找并切换 ctx_fzf() { local name name$(ctx_list | fzf --preview cat $CONTEXT_DIR/{}/prompt.md 2/dev/null) || return ctx_load $name } alias ctxctx_load这里有一个很重要的细节ctx_load 里的 source 顺序和环境变量范围。默认情况下 source 只影响当前 shell不会污染其他会话这正好满足我们要的隔离性。如果你希望环境变量能传递给 tmux 的新窗口必须在 tmux 会话创建之前 source否则新窗口拿不到。3.2 关键实现保存与恢复环境变量环境变量是上下文里最容易被忽略、也最容易出错的部分。我的经验是只保存“项目的关键变量”不要把整个环境变量表 dump 出来。刚开始我图省事直接 env env.sh结果恢复的时候把 PATH、SHLVL、PPID、_ 这些系统变量也恢复了shell 行为直接异常连命令都找不到折腾了大半天才排查出来。正确的做法是维护一个“白名单”像下面这样env | grep -E ^(PORT|API_HOST|PYTHON_ENV|GIT_BRANCH|NODE_ENV|APP_ENV|MY_FLAG) $dir/env.sh白名单正则可以根据自己的项目随意扩充。恢复的时候source 一下即可。这里需要解释一个原理source 一个包含 export 语句的文件等于在当前 shell 进程内执行这些赋值语句。因为它是在当前进程执行的所以能够影响后续命令而执行子脚本则不行子脚本的环境变量变化不会传回父进程。这也是为什么 ctx_load 必须用 source 来恢复 env.sh而不是直接 bash env.sh。还有一个要避开的坑不要在恢复时 source 整个 .bashrc。因为 .bashrc 里往往有历史记录、别名设置、提示符配置这些东西跟当前上下文没关系source 之后轻则重定义一堆别名重则触发递归加载。环境变量保存文件的意义就是“只带必要状态不带无关杂物”。3.3 关键实现编辑器会话保存编辑器状态的恢复是整个体验中最能感觉到“卧槽原来可以这么顺”的部分。用 Neovim 的朋友直接利用内建会话机制:mksession! ~/.contexts/blog-post/session.vim然后用 nvim -S ~/.contexts/blog-post/session.vim 就能一键恢复当时打开的文件列表、窗口分割、光标位置。如果你用 VS Code也可以把 .code-workspace 文件作为上下文的一部分一个文件打包所有工作区配置效果类似。编辑器会话这块有几个坑。第一个Neovim 的会话文件默认路径写死的话换机器会找不到文件。所以我建议在保存命令里用动态变量拼接路径而不是手写完整路径。第二个mksession 保存的窗口布局和当前机器分辨率有关换大屏或者小屏打开布局会乱。这个暂时没有完美解法我一般只保存文件列表不保存窗口布局。第三个如果有 Fugitive、NvimTree 这类插件打开的辅助窗口会话恢复时偶尔会冒出重复窗口解决办法是保存前先执行一个清理函数关掉非必要插件窗口。3.4 关键实现tmux 布局切换tmux 是 context-mode 里最像“项目管理器”的部分。我的做法是一个上下文对应一个 tmux sessionsession 内部再按功能分成几个 window窗口用途1主编辑器必然是全屏的重点窗口2终端执行git、构建、测试3日志/调试dev server 输出、tail -f切换上下文的时候tmux switch-client 直接切到对应 session整个终端布局瞬间变成上一个状态的样貌。这里用到了 tmux 的一个特性session 是后台实体即使你不在这个 session 里它内部的进程也一直在跑。这是我故意保留的因为我不希望切走上下文就把开发服务器给停了。真要停的时候手动执行 ctx_stop 就行否则它跑着也不占多少资源。恢复布局的方式我推荐先用一个基础脚本创建窗口再根据快照文件调整。直接在 tmux 里保存精确布局很麻烦而且不同版本兼容性一般。我的妥协方案是保存每个 window 的工作目录和一个自定义布局字符串比如 main-vertical加载时先创建窗口再应用布局。够用就行不用追求像素级还原。3.5 完整演示创建一个“写博客”上下文下面我走一遍完整流程帮你直观感受平时是怎么操作的。先建上下文并进入工作状态ctx new 等价于 mkdir -p $HOME/.contexts/blog-post cd ~/sites/my-blog git checkout -b feat/ctx-mode-demo export PORT4000开始写作后可能打开几个文件文章草稿、图片目录、示例代码。在 Neovim 里编辑一会儿生成 session:mksession! ~/.contexts/blog-post/session.vim离开去吃午饭前执行一行命令ctx save blog-post下午回来或者次日继续执行ctx load blog-posttmux 会话恢复、环境变量 PORT4000 自动生效、Neovim 打开草稿文件、光标还在昨天停的位置。全程不到三秒打开终端都是昨天离开的模样。这个体验一旦习惯就再也回不去了。3.6 进阶把上下文喂给 AI 辅助工具这个扩展是我最近几个月觉得增益最大的。既然上下文里已经有任务说明、代码相关的背景信息能不能把这些信息自动打包成 prompt送给 AI 辅助工具让它不用重新解释背景实现很简单ctx_prompt 命令把当前上下文里的 prompt.md、notes.md、目录结构、关键环境变量全部拼接成一段文本然后复制到剪贴板ctx_prompt() { local dir$CONTEXT_DIR/$(basename $PWD) [ ! -d $dir ] { echo current dir is not a context; return 1; } { echo TASK cat $dir/prompt.md 2/dev/null echo NOTES cat $dir/notes.md 2/dev/null echo WORKDIR cat $dir/workdir 2/dev/null echo ENV cat $dir/env.sh 2/dev/null } | xclip -selection clipboard echo prompt copied to clipboard }然后在 AI 对话框里直接粘贴三秒钟把背景交代得清清楚楚相当于给 AI 也做了一个上下文切换。这个能力在看别人代码、写周报、给项目做复盘的时候意外地好用。4. 常见问题与排查技巧实录任何方案用久了都会遇到问题。context-mode 也不例外这里把我在实际使用中踩过的坑和排查思路整理成几个高频问题。以后遇到类似状况直接对着排查就行。4.1 环境变量恢复后总是不对症状ctx load 之后echo $PORT 是空的或者和预期不一致。排查思路分两步先看文件本身再看出问题的地方是不是在白名单外。我之前遇到过一个典型问题把环境变量文件写成了 CRLF 行尾在 Windows 下编辑过拿到 Linux 的 shell 里 source行尾的 \r 导致变量名被污染。解决方案很简单dos2unix 转一下或者以后别在混用跨平台编辑器时保存 Windows 行尾。另一个常见问题是变量值里有空格或者特殊字符grep 保存时被丢掉了。解决办法是保存时不要用 env 的无差别导出改用 declare -p 或者手动维护一个保存脚本把变量值用引号包严实。提示排查环境变量问题时用起来最顺手的一句话是source env.sh env | grep -E ^(PORT|API)能立刻看到当前进程里实际生效的值比凭感觉猜快得多。4.2 tmux 窗口布局恢复失败症状切换上下文后 tmux 只有一个窗口或者布局变回默认的左右分屏没有按预期恢复。我排查询问之后发现最常见的原因是目标上下文对应的 tmux session 不存在了。ctx_load 会先判断 if ! tmux has-session不存在就新建但新建的时候只创建了一个默认窗口。如果之前保存的上下文有几个 window需要提前用脚本创建这些基础窗口再按快照恢复目录。还有一个隐藏问题session 名称里带了点号或者斜杠tmux 对名称有特殊语法处理切换时一直报 no such session。我的规避手段是统一用短横线命名上下文比如 blog-post、demo-project坚决不用点号和空格。4.3 编辑器状态保存不完整症状恢复 Neovim 会话后打开的 buffer 变多了或者某些窗口被插件窗口替代。排查方向是会话生成插件的影响。我自己最后采用了“保存前关插件窗口保存后再重新打开”的策略写了一个函数function! SaveCleanSession() NvimTreeClose :mksession! ~/.contexts/blog-post/session.vim NvimTreeOpen endfunction这不算完美但实测里能解决百分之八十的布局错乱问题。如果你用的是 VS Code 的 workspace那就没有这个烦恼不过 VS Code 的开源性能和终端联动又会弱一些取舍问题而已。4.4 多台机器同步的坑很多人想把 ~/.contexts 同步到家里的电脑。我的建议是同步纯文本文件不要同步 session.vim 和 tmux_layout。原因很实在session.vim 里的路径是绝对路径换机器就失效tmux 布局涉及终端尺寸笔记本和台式机的宽高比例完全不同直接同步只会得到一个乱掉的界面。我的同步策略是~/.contexts 里的 prompt.md、notes.md、workdir、env.sh 进 Git 仓库其余文件用 .gitignore 忽略。这样才能保证换机器后至少任务说明和思维状态是完整的重新恢复环境也就几十秒的事。4.5 自动化定时快照避免状态丢失最怕的情况是上下文里存了大量笔记但哪天忘了执行 ctx save结果什么都没保存下来。我的解法是设置了一个定时任务每十五分钟自动把当前上下文里改动过的笔记文件 cp 到 ~/.contexts 对应目录。用最简单的方式*/15 * * * * cp $HOME/.auto-notes.md $HOME/.contexts/$(date %Y%m%d)-snapshot.md这里的关键是不要频繁自动保存完整 session 文件尤其是大型项目session 文件动辄几 MBsave 一次都有延迟。临时笔记这种小文本文件随便存大文件用 Git 提交或者手动 save 更合适。用 context-mode 小半年之后我自己最直观的感受是敢同时开多个任务了。以前因为切换成本高手头任务一多就想砍掉几个现在每个任务都有独立的“工位”切来切去不用付额外的认知税反而敢接一些短期的辅助性工作。最后再分享一个小技巧给 ctx list 绑一个快捷键比如 CtrlN任何时候按一下就能看到自己到底有多少个半拉子上下文这种感觉很爽偶尔也会逼自己清理掉那些过期的旧上下文。这套方案目前还在迭代比如后面准备在 notes.md 里增加一个 checklist 模板让每个上下文新建时自动带上任务分解结构有兴趣的朋友可以自己试试。