DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Chat Git — DSH Plugin for DeepSeek Harness
← Plugins
C

dsh-chat-git

Chat Git

DeepSeek Harness conversation and code management enhancement plugin: Automatically creates Git checkpoints for each round of AI Coding conversations, tracks changes by round, and supports history viewing, conversation branching, edit-and-resend, and independent rollback of conversations and workspa

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:NOOB-P/dsh-chat-git#a69e004105d911cf13f1a2a42f3df0f4b38bed36
READMECompatibilityVersions

Description

DeepSeek Harness conversation and code management enhancement plugin: Automatically creates Git checkpoints for each round of AI Coding conversations, tracks changes by round, and supports history viewing, conversation branching, edit-and-resend, and independent rollback of conversations and workspaces. Try every idea with confidence, return your code to any previous version at any time, and avoid accidental changes, lost work, and confusing context. No runtime dependencies; ready to use after installation.

Compatibility and provenance

Chat Git is published as dsh-chat-git and currently resolves to version 0.6.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
9/17/2026

Versions

0.6.0stable
9/17/2026

Related plugins

Loading related plugins…

Latest
0.6.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 1
Weekly downloads
0
Last push
9/17/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in

Related plugins

More verified plugins in developer-tools.

Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)Sdk Minimal@deepseek-ai/dsh-sdk-minimalThe standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessionsSdk App@deepseek-ai/dsh-sdk-appThe dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-baseSubagent Codex@deepseek-ai/dsh-subagent-codexOne-shot Codex subagent provider over the official app-server protocol

README

dsh-chat-git

把每个对话变成一条 git 检查点链的 DeepSeek Harness 插件。

每轮对话结束自动提交一次工作区,随时回到任意一轮的代码状态,或者只改对话不碰代码 —— 两条线互不干扰,而且每一步都能找回。

DSH Desktop 插件市场

本插件通过 GitHub 仓库安装,安装后重启 DSH Desktop 即可加载:

dsh plugin --profile web add github:NOOB-P/dsh-chat-git

插件市场元信息位于 package.json 的 dsh 字段,profile bundle manifest 为 cordis.patch.yml。仓库使用 MIT 协议,并通过 GitHub 的 dsh-plugin Topic 参与社区插件索引。

权限与数据访问

  • 在当前会话工作目录中读取 Git 状态、提交历史和变更,并按设置执行 git init、git add、git commit 以及用户确认的工作区还原。
  • 将会话与检查点映射写入 $DSH_HOME/chat-git.json;不会把 Git 操作扩展到当前工作目录之外。
  • 在 DSH Web 界面注入回退按钮、历史面板和设置项,通过本地 /chat-git/* 路由与宿主通信。
  • 启用提交标题总结时,会把本轮提示词和暂存变更摘要交给当前 DSH 模型生成提交标题;关闭该设置即可不调用模型总结。
  • 插件没有独立的远程服务,也没有运行时 npm 依赖;Git 是可选的系统级依赖。

目录

  • 这是什么
  • 功能特性
  • 安装
  • 使用
  • 提交信息格式
  • 架构
  • Host 路由
  • 状态文件
  • 设计边界与取舍
  • 开发
  • 常见问题
  • 更新日志
  • 许可证

这是什么

dsh-chat-git 是 DeepSeek Harness 的一个 profile bundle 插件。它把「对话」和「工作区代码」这两件本来各自漂移的事情绑成一条可回溯的链:

  • 对话开始时,检查该会话的工作目录是不是一个 git 仓库,不是就 git init;
  • 每轮对话结束(agent 交付完成)时,git add -A -- . + git commit,提交信息形如 Ai-coding:修复设置页开关无法启用;
  • 提交描述默认由模型总结该轮的提示词与改动文件生成,而不是把冗长的原始提示词塞进标题;
  • 回合图标行里多一个回退按钮:一下待确认、一下确认,同时回滚代码与对话;
  • 标签栏里多一个 历史 标签页,左边管对话(打开新对话 / 回退 / 编辑并发送),右边管工作区 Git(查看提交、还原到任意提交),两栏互不读写。

整个插件零 npm 依赖、无构建步骤,lib/ 下的文件就是源码。


功能特性

自动检查点

能力说明
自动 git init工作目录不是仓库根时自动初始化,不会把提交打到上层仓库去
每轮自动提交默认每轮一次,可在设置里放宽到每 2 / 3 … 10 轮
模型生成标题读提示词 + git diff --cached 摘要,输出一句 ≤20 字标题
失败必留检查点模型不可用 / 超时 / 报错一律回退成提示词本身,绝不丢提交

回合回退

回合图标行(复制按钮旁边)的回退按钮:点一下进入待确认,再点一下才真的回滚。回滚走 sessions.fork,所以原始会话不会被销毁,随时可以找回。

历史标签页(左右两栏)

左栏 · 对话右栏 · 工作区 Git
按时间倒序列出每一轮(最新在最上)直接读仓库:分支 / HEAD / 是否干净
从这里打开新对话(fork) —— 新开一条线,原会话保留列出最近 100 条提交(含非本插件写的)
回退到这里(还原) —— 新开一条线,原会话归档每条提交可还原到这里
编辑并发送 —— 弹窗选「当前对话 / 新建对话」,把该轮请求(文字 + 图片)放进输入框标出当前位置并弹出确认对话框

每张卡片都带年月日 + 时分秒的时间戳(2026-09-13 14:05:32)—— 只写 14:05 答不出「这是今天、昨天还是上周」,而两个回合落在同一分钟里是常事。

带图片的那一轮会额外标一行 含 N 张图片 —— 只写数量、不铺缩略图:历史列表是列表,不是图库;句柄本身由唯一需要搬动它们的那一步去重读。

左栏的最上面就是当前所在的那一轮,它带一个 当前位置 标签,列表上方另有一行「当前位置:第 N 轮。」—— 列表一旦滚动,「最上面那行」就不再是答案。

两栏是两条独立的路由、两次独立的读取:仓库坏掉不会带走对话列表,反之亦然。

编辑并发送(预填输入区,不自动发送)

点历史卡片上的编辑并发送,先弹出对话框选去向(当前对话 / 新建对话 / 取消):

  1. 取回该轮完整的原始请求(不是卡片上被截断的展示文本),文字与图片一起;
  2. 重建对话,两条去向的差别只有一个 —— 你正站着的这条对话还留不留下:
    • 当前对话(还原):本窗口变成重建后的那条线(同样在「上一轮」处切开),原对话随即归档(可从归档恢复);
    • 新建对话(fork):在它旁边另开一条只到「上一轮」的会话并切换过去,原对话保持原样,两条线都能继续,新线带可区分的标题(… (1))。
  3. 不论哪条去向,请求都被放进新会话的输入框,光标在那里等你 —— 想改就改,想换模型就用 shell 自己的模型选择器,不想发就删掉。

它不会替你发送。 之前的行为是截断后立刻把提示词排队,模型在用户还没看清之前就开始回答了;现在这一步停在输入区,发送键本身就是确认。

只有 当前对话 和 回退到这里(还原) 会归档原会话;新建对话 是纯粹的分叉。读取请求与移动会话都发生在选完去向之后,所以它们的失败报在这个对话框里,就报在做出选择的地方。

工作区 Git 还原(含当前位置)

  • 点还原到这里会先弹出确认对话框,写明:要还原的提交 id 与主题、HEAD 不动、未跟踪文件不会被清理、以及 当前位置:… → 还原后变成 …;
  • 确认前不发任何请求,取消 是真正的一等出口;
  • 还原后,工作区当前所在的那一行会带上 当前位置 标签,按钮变成不可点的 已在此位置;列表上方另有一行「当前位置:xxxxxxx。」;
  • 还原只动工作区:不改对话、不移动 HEAD、不丢检查点。

设置页

设置项说明
自动检查点总开关;未安装 git 时无法开启
自动保存间隔每轮 / 每 2 轮 / … / 每 10 轮
历史标签页纯展示偏好,关掉只是撤下这个标签页,功能不受影响
总结模型关闭 / 使用当前模型 / 指定模型(provider + model)
检测执行 git --version
下载跳转 Git 官方下载页

安装

前置要求

  • Node.js ≥ 22(插件本身零依赖,这是宿主的要求);
  • DeepSeek Harness,并有一个可用的 profile(下面以 web 为例);
  • git(可选但强烈建议;未安装时插件会检测到并给出下载入口,只是无法提交检查点)。

方式一:从 GitHub 直接安装(推荐)

dsh plugin --profile web add github:NOOB-P/dsh-chat-git

pnpm 会把这个 GitHub 仓库拉进 profile 的依赖里,dsh 随后按已安装状态重建层列表 —— 不必先手动 git clone。

想改代码、随时 git pull 的话,先克隆再按本地路径安装:

git clone https://github.com/NOOB-P/dsh-chat-git.git
cd dsh-chat-git
dsh plugin --profile web add link:.

Windows 上用正斜杠或反斜杠都可以(在克隆出来的目录里执行):

dsh plugin --profile web add link:.

方式二:下载 release 里的 tarball 安装

从 Releases 下载 dsh-chat-git-<版本>.tgz,然后:

dsh plugin --profile web add ./dsh-chat-git-0.6.0.tgz

或者一条命令直接从 release 资产安装(无需先下载):

dsh plugin --profile web add https://github.com/NOOB-P/dsh-chat-git/releases/download/v0.6.0/dsh-chat-git-0.6.0.tgz

也可以自己从源码打包再装:

npm pack                    # 产出 dsh-chat-git-0.6.0.tgz
dsh plugin --profile web add ./dsh-chat-git-0.6.0.tgz

dsh plugin --profile <name> ... 是一条 pnpm 转发器:它在 profile 目录里执行 pnpm,然后按已安装状态重建 dsh.profile.bundles 层列表 —— 任何能解析到「声明了 dsh.bundle 的包」的依赖都会自动加入层栈,本包正是这种情况,所以不必手改 profile 的 package.json。

更新

按当初的安装方式二选一:

# 装的是 github: 或 release tarball
dsh plugin --profile web add github:NOOB-P/dsh-chat-git

# 装的是本地克隆(link:)
git pull
dsh plugin --profile web add link:.   # 重新链接

卸载

dsh plugin --profile web remove dsh-chat-git

装完 / 更新后需要重启 dsh web 才会加载。


使用

回合图标行的回退按钮

每轮 agent 回复下方的图标行里(紧挨复制按钮之后)多出一个回退图标:

  1. 第一次点击 —— 进入待确认状态,按钮文案变化,此时不会发生任何事;
  2. 第二次点击 —— 在该轮的结束序列处分叉出新会话并切换过去,原会话归档(可从归档恢复);
  3. 移动鼠标或点别处即取消。

历史标签页

标签栏里(对话 / 轨迹 旁边)点 历史,得到左右两栏:

  • 左栏按时间倒序列出该会话的每一轮(最新一轮在最上面,带 当前位置 标签、带年月日时分秒时间戳,带图片的轮次另标 含 N 张图片),每张卡片三个动作:从这里打开新对话(fork) / 回退到这里(还原) / 编辑并发送;
  • 右栏展示工作区仓库的分支、HEAD、是否干净,以及最近 100 条提交(含不是本插件写的提交),每条可以 还原到这里。

正在进行中、还没有结束序列的那一轮无法分叉,按钮置灰并说明原因 —— 猜一个边界会产出错误的 fork。

编辑并发送

  1. 在左栏点某张卡片的 编辑并发送,在弹窗里选 当前对话 或 新建对话(取消 是真正的一等出口,点它不读请求、不建会话、不归档);
  2. 会话切到重建后的新线,该轮的完整请求(文字 + 图片)已经在输入框里;
  3. 改好之后按 shell 自己的发送键。

结果:该轮及其之后的所有对话不在新线上(实现上是在「上一轮」处分叉出新会话),而这一轮的请求被交还给你。选 新建对话 时原会话不被归档,每一轮都还在、随时切得回去,新线另外带一个可区分的标题(… (1)),所以两条线在侧栏里不会读成同一个对话;选 当前对话 时原会话归档,本窗口就是重建后的那条线(标题沿用不变 —— 它替换了自己的来源,而不是并排新增一条)。第 1 轮没有上一轮,两种去向都会在同一工作区新建一个会话 —— 这才是「这一轮之前什么都不留」的诚实表达。

图片是重新读进来的:新线是在「这一轮之前」切开的,那张图的持久引用并不在新会话的日志里,所以字节是通过原会话自己的授权读回来、再注册成新会话的草稿图片。部署没挂 conversation 服务时,图片会被放弃,文字照常送达。

工作区 Git 栏

  1. 点某条提交的 还原到这里;
  2. 在确认对话框里核对提交主题、当前位置与还原后位置;
  3. 点 确认还原。

工作区被 git checkout <sha> -- . 覆盖,并清掉该提交中不存在的新增路径;HEAD 不动,提交历史与对话都不变。未跟踪文件不属于任何检查点,删掉就找不回来,因此不会被清理。

设置项

设置 → 仓库增强。


提交信息格式

每个检查点的提交主题由固定的 Ai-coding: 前缀(全角冒号)加一段简短描述组成:

Ai-coding:修复设置页开关无法启用
  • 描述优先由模型生成:把该轮的提示词与 git diff --cached 摘要交给 ctx.llm,要求输出一句不超过 20 字、与提示词同语言的标题。返回结果会被清洗(去掉引号、标题: 之类的标签、被回显的 Ai-coding: 前缀、结尾句号),并截到 40 字。
  • 整行(含前缀)不超过 72 字符,超出部分截断并以 … 结尾;
  • 模型不可用、超时、报错或返回内容不可用时,自动回退为提示词本身(空白折叠成单个空格),绝不会因此丢掉检查点;
  • 该轮没有记录到提示词时(会话在重启后恢复、或该轮没有用户消息),再回退为 Ai-coding:第 N 轮对话。
  • 设置里的 总结模型 有三态:关闭(描述直接取提示词,零模型调用)、使用当前模型(该对话自身的模型,读不到时退回默认模型)、指定模型(显式选一个 provider + model)。
  • 指定模型的选择器由 host 从实时模型注册表(llm.listProviders() + llm.listModels())填充,因此不会列出本部署无法调用的路由;没有已注册模型的服务商不会出现在列表里,而已存储但注册表不再列出的路由仍保持可选 —— 否则打开一次设置页就会让用户的配置变得不可达。

架构

组成

半边入口职责
Hostlib/index.js(exports ".")会话事件钩子、git 命令、/chat-git 路由、设置持久化
Clientlib/client.js(exports "./client")回退按钮、历史标签页、设置页
.
├── lib/
│   ├── index.js      # host 半边:事件钩子 + 路由 + 状态
│   ├── client.js     # client 半边:四个客户端席位
│   ├── git.js        # git 封装(全部走 argv 数组)
│   ├── store.js      # $DSH_HOME/chat-git.json 的读写与校验
│   ├── summarize.js  # 模型生成提交标题(含清洗与回退)
│   └── timeline.js   # 会话日志折叠成「轮次」与「整轮请求」(文字 + 图片句柄)
├── test/
│   ├── harness.mjs   # host 侧:真实 git + 真实 loopback HTTP
│   └── client.mjs    # client 侧:React 替身 + 假 host
├── cordis.patch.yml
└── package.json

为什么零依赖、无构建

lib/ 下的文件就是源码,没有构建步骤 —— 客户端半边直接写在 window.__ModuleLoader__.load({ id, factory }) 包装格式里(这正是 client-modules 期望被服务的文件形状),因此不需要 TypeScript、tsdown 或任何 npm 依赖。

Host 半边只 import node: 内置模块和同目录兄弟模块:profile 安装会把包软链过去,Node 会从真实项目路径向上解析裸模块名,那里并不存在依赖树,所以保持零依赖是刻意的。

四个客户端席位

Slot类型用途
conversation.chat.assistant-actionslist回退按钮。shell 把它渲染成该回合图标行的 extraActions,即紧跟在复制按钮之后
conversation.viewlist标签栏里的历史标签页,左右两栏:左栏是对话(每一轮带「从这里打开新对话(fork)」「回退到这里(还原)」「编辑并发送」,带图片的轮次另标 含 N 张图片),右栏是工作区 git(提交列表,每条带「还原到这里」并带 当前位置 标记,还原前弹出确认对话框)
settings.sectionlist设置页(自动检查点开关 / 自动保存间隔 / 历史标签页开关 / 总结模型三态 / 检测 / 下载)
conversation.input.docklist无渲染的转交席位:编辑并发送 把该轮的请求寄存在新会话 id 下,这个席位在该会话的输入区挂载时把它取走,写进草稿。它什么都不画,只占一个空节点

四个席位都是纯增量的 list:不与他人争抢任何 cell,因此本插件不会遮蔽、也不会顶掉任何既有 UI。

历史视图用 conversation.view 而不是自绘的浮层,是因为标签栏是从 slot 本身投影出来的:一个组件无法隐藏自己那一页,唯一能撤下标签的办法是退出注册表。因此设置里的开关先把偏好写进 host,再镜像到本地 store,由 store 的订阅者注册或注销这一席 —— 标签与开关不可能各说各话。这也是为什么开关状态必须持久化到磁盘:conversation.view 的注册发生在启动期,而不是标签被点开的那一刻。


Host 路由

全部 loopback-only、POST、JSON 信封 { ok, value } / { ok, error }。 路由只接受 sessionId,从不接受路径,因此无法被指向任意目录。

路由作用
POST /chat-git/state全部偏好(自动检查点 / 自动保存间隔 / 历史标签页)的状态、git 探测结果、该会话的检查点列表
POST /chat-git/detect执行 git --version
POST /chat-git/set-enabled切换自动检查点;git 不可用时拒绝开启
POST /chat-git/set-history切换历史标签页;纯展示偏好,不做任何能力检查
POST /chat-git/set-interval设置自动保存间隔(1–10 的整数);越界、非整数一律拒绝,不会存成"永不提交"
POST /chat-git/set-summary局部更新总结偏好(mode / provider / model);无路由的 custom 被拒绝
POST /chat-git/timeline折叠该会话的日志,返回按顺序的每一轮(含 fork 边界与对应检查点);提示词按展示需要截断
POST /chat-git/turn-prompt取某一轮完整的原始请求(不截断):{ turn, text, images },images 只带 { attachmentId } 句柄,字节由浏览器半边经原会话授权自行读回
POST /chat-git/repo直接读该会话工作区的仓库:root / 分支 / HEAD / 当前位置(position + positionKnown)/ 是否干净 / 最近 100 条提交(与轮次无关)
POST /chat-git/models实时模型目录,供设置页的总结模型选择器使用(带 60 秒缓存与每服务商 4 秒上限)
POST /chat-git/revert该回合图标按钮用:git checkout <sha> -- .、清理之后新增的路径,并丢掉该轮之后的检查点
POST /chat-git/restore右栏用:把工作区还原到仓库里任意一个提交,只动工作区,不动对话、不动 HEAD、不丢检查点
POST /chat-git/inherit回退分叉后,把剩余检查点交给新会话

状态文件

写在 $DSH_HOME/chat-git.json(默认 ~/.dsh/chat-git.json),内容为偏好与 sessionId -> { cwd, position, commits } 映射:

{
  "version": 1,
  "enabled": true,
  "history": true,
  "interval": 1,
  "summary": { "mode": "current", "provider": "", "model": "" },
  "sessions": { "<sessionId>": { "cwd": "...", "position": "", "commits": [] } }
}

放在磁盘上而非内存里,因此 harness 重启后历史对话的回退按钮依然可用。任何读写失败都降级为仅内存,不会把插件拖垮。

position 是「工作区当前站在哪个提交上」。它必须记在这里而不是从 git 读回来:还原刻意不动 HEAD,所以还原之后 git 已经答不出这个问题了。旧状态文件没有这个字段,读作「不知道」,路由会把它解析成 HEAD 并同时给出 positionKnown: false,让右栏可以说清楚自己显示的是记录还是推断。

0.2 的布尔 summarize 字段会被读取并迁移为对应的 summary.mode(false → off,true → current),不会因为升级而悄悄把用户关掉的功能重新打开。


设计边界与取舍

这些是刻意的工程决定,不是未完成项:

  1. 回退用 fork,不销毁会话。 DSH 没有「截断会话」API,只有 sessions.fork({ sessionId, atSeq })。所以「后面的对话全部删除」实现为:在该轮结束序列处分叉,得到只含前 N 轮的新会话并切换过去。原始日志保留,用户不会因为一次误点永久丢失记录。
  2. git checkout 语义是「还原内容 + 清掉新增」,HEAD 不动。 单条 git checkout <sha> -- . 不会删除该提交中不存在的路径,后面几轮新建的文件会残留、让回溯看起来只做了一半。所以在此之后还会 git rm 掉 sha..HEAD 之间新增的路径。只动 HEAD 里存在的路径,因此每一步都还能从提交历史里找回。HEAD 保持不动,使「轮次 → 检查点」映射继续可读。
  3. 未跟踪文件不碰。 它们不属于任何检查点,删掉就找不回来了。回退后它们会作为未跟踪文件留在原地。
  4. 已有仓库会被沿用,不会被重新 init。 只有当会话工作目录本身就是一个仓库根时才会沿用;仅仅位于某个上层仓库内部时,会在工作目录里 git init 出自己的仓库,避免把提交打到用户没在这个对话里打开过的祖先仓库上。这个隔离由两件事共同保证:嵌套仓库本身,以及 git add -A -- . 中显式的 pathspec —— 自 Git 2.0 起,不带 pathspec 的 git add -A 会暂存整个工作树而与当前目录无关,一旦工作区落在更大的仓库内就会把无关改动一起卷进来。
  5. 按钮落在回合图标行,而不是用户消息那一行。 用户消息的复制按钮由 shell 的 UserMessageNodeView 内联渲染,内部不渲染任何 slot;conversation.chat.node 下只声明了 tool.call.toolview / assistant-actions / turnTail / commandview 四个子席,没有任何用户消息操作位。且 UserStyleBubble / MessageIconActions 是 dsh-client-ui-chat 的模块私有符号,exports 只暴露 4 个值;所有公开 client 模块(ui-chat 4 个、ui-conversation 13 个、ui-session 3 个)都不提供可复用的气泡组件,dsh-client-ui-primitives 在本机安装树里甚至不存在。因此「复制旁边」只能落在回合图标行 —— 那里恰好是 MessageIconActions 的 extraActions,渲染顺序为 [时间, 复制按钮, extraActions, 分支按钮, …],即紧邻复制按钮。
  6. 按钮靠 messageId 反查轮次。 assistant-actions 只派发 messageId,而检查点按轮次编号存放,回退还需要分叉边界。轮次号与回合结束序列都从 Chat snapshot 的 turn-tail 节点上读回(其已声明的载荷同时携带三者),选择器返回 "turn:seq" 字符串而非对象,以保证取值按值比较稳定、不引发多余渲染。
  7. /chat-git/state 的空 sessionId 是合法的全局读。 设置页没有会话,只读开关、git 探测与状态文件路径。曾经把它当作缺参拒绝,结果设置页永远拿不到 state、开关一直禁用并显示 sessionId is required —— 现由 host 与 client 两侧的测试共同看住。
  8. git 走 ctx.subprocess 的 argv 数组,不是 shell 字符串:没有引号规则要处理、提交信息与路径没有注入面,并且与工具调用享有同等的子进程生命周期与输出上限。
  9. AI 总结在读不到模型时必须是「少一个功能」而不是「丢一个检查点」。 所以 ctx.get('llm') 与 agentDefaultModel.currentSelection() 都是可选读,整条路径返回空字符串即回退到提示词;llm 调用另有 8 秒上限,且 GenerateOptions 里不声明 —— 本插件的调用不是 harness 的 功能,冒用它会让遥测把用量记到别处。标题消息以 标注作者。

开发

运行测试

npm test              # 两套一起跑
npm run test:host     # 203 项:真实 git、假 ctx/假模型、日志折叠(含压缩检查点跳过)、整段请求(文字 + 图片句柄)读取、`set-model` 已下线、当前位置记账、间隔节流、工作区兜底、真实 loopback HTTP
npm run test:client   # 218 项:席位注册(含无渲染的转交席位)、消息→轮次反查、回退流程、三态选择器、倒序两栏历史与当前轮标记、时间戳格式(年月日 + 时分秒、同一分钟可区分、无时间不显示 1970)、图片数量标记、还原确认对话框与当前位置标记、编辑并发送三选一对话框(当前对话归档 / 新建对话保留 / 取消)、重建会话 + 预填输入区 + 图片回读 + 只取一次、间隔选择、失败重试

test/harness.mjs 用真实 git 子进程在一个临时工作区里跑完整链路,并通过真实 HTTP 服务器驱动路由处理器,因此 loopback 守卫、JSON 信封都真正被覆盖。

test/client.mjs 用一个刻意很小的 React 替身(函数组件递归展开、useState 跨渲染保持)在 Node 里驱动客户端半边,不需要浏览器。

Windows 沙箱注意:受限模式下 Node 无法用 stdio: 'pipe' 捕获子进程输出(spawn 直接 EPERM),所以测试桩把子进程输出重定向到普通文件描述符再读取。

打包

npm pack --cache .npm-cache     # Windows 沙箱下需要自定义 cache 目录

产出 dsh-chat-git-<version>.tgz,内含 lib/、test/、cordis.patch.yml、README.md 与 package.json。

代码约定

  • 注释用英文,并且解释为什么,而不是复述代码在做什么;
  • 所有面向用户的文案是中文;
  • 提交前 npm test 必须全绿。

常见问题

为什么还原之后 HEAD 没动?

因为 HEAD 是「轮次 → 检查点」映射的锚点。如果还原顺手移动了 HEAD,后面几轮的检查点就会从历史上消失,右栏也就再也列不出它们了。所以还原只覆盖工作区内容并清掉新增路径,HEAD 保持不动。

代价是 git 自己答不出「工作区现在站在哪儿」,这正是插件把 position 记在状态文件里、并在列表上标出 当前位置 的原因。

为什么未跟踪文件没被删掉?

未跟踪文件不属于任何检查点 —— 它们从未被提交过,所以「回到某个提交的状态」并不能推出它们该不该存在。删掉就找不回来了,因此插件选择不碰它们,只还原 HEAD 里存在的路径。

为什么按钮在回合图标行,而不是用户消息旁边?

因为用户消息那一行没有任何 slot。shell 的 UserMessageNodeView 内联渲染复制按钮,不派发子席;相关的气泡组件是 dsh-client-ui-chat 的模块私有符号。回合图标行里的 extraActions 是唯一可以插入动作、并且渲染顺序紧邻复制按钮的位置。详见设计边界第 5 条。

回退之后原来的对话去哪了?

归档了,不是删除了。回退到这里(还原) 会新建一条只到该轮为止的会话并把原会话归档,可以从归档里恢复;从这里打开新对话(fork) 则原样保留两条线。两者在 DSH 里都是同一个 sessions.fork 原语,区别只在原会话的去向。编辑并发送 的弹窗把这件事变成了一道选择题:选 当前对话 就是归档(本窗口变成重建后的线),选 新建对话 就是保留。

一定要装 git 吗?

不装也能用,但没有任何检查点功能:插件检测不到 git 时会把自动检查点开关锁住,并在设置页给出官方下载入口。git 是唯一的外部依赖,而且是进程级的,不是 npm 依赖。

harness 重启后历史还能回退吗?

能。sessionId -> { cwd, position, commits } 映射写在 $DSH_HOME/chat-git.json 里,不在内存里。只要磁盘可写(写失败会降级为仅内存,不会报错),重启后旧对话的回退按钮依然可用。

但 /chat-git/timeline 只对已加载的会话有效:会话没加载时如实返回 session-unavailable,而不是猜一个边界。

「编辑并发送」为什么不用卡片上显示的提示词?

因为卡片上那段是展示用的截断文本(截到 300 字)。用它交回去等于悄悄让用户对着一个自己从没写过的问题继续。所以插件单开了一条 /chat-git/turn-prompt 返回完整原文(连图片一起),输入区用原文填充。

为什么点完「编辑并发送」不会自动发出去?

因为「按下这个按钮」表达的是「我想改这一轮」,不是「照原样再问一遍」。旧行为会在切换会话的同时排队发送,模型在用户还没看清之前就开始回答;现在它只把请求放进新会话的输入区,改不改、发不发都由用户决定,模型选择器也回到 shell 自己那一个。

图片也一起带过去了吗?

带了。新线是在选中那一轮之前切开的,所以那张图的持久引用不在新会话日志里,它授权不了自己的字节 —— 转交时用原会话的 readAttachment 把字节读成浏览器文件,再铸成新会话的草稿图片。部署没挂 conversation 服务时图片会被放弃,文字照常送达。

会自动提交别人的仓库吗?

不会。只有当会话工作目录本身就是仓库根时才沿用已有仓库;仅仅位于某个上层仓库内部时,会在工作目录里 git init 出自己的仓库。同时所有 git 命令都带显式 pathspec,不会波及工作区之外。


更新日志

0.6.0

  • 「编辑并重新发送」改为「编辑并发送」,不再自动发送:点下去只重建会话并把该轮请求放进新会话的输入区,改不改、发不发由你决定(旧行为会在切换会话的同时排队发送,模型在用户还没看清之前就开始回答)。发送键回到 shell 自己那一个,模型选择也回到 shell 自己的选择器。
  • 图片一起带过去:/chat-git/turn-prompt 现在返回 { turn, text, images },images 只带 { attachmentId } 句柄;浏览器半边用原会话的 readAttachment 把字节读回来,再铸成新会话的草稿图片,与文字一起写进输入区。新线是在「这一轮之前」切开的,所以那些引用不在它的日志里,只有原会话能授权这些字节。
  • 新增无渲染的转交席位 conversation.input.dock:新会话的输入区此刻还不存在(sessions.open 是请求不是同步切换),请求因此按 session id 寄存,由该席位在它自己那一轮的输入区挂载时取走 —— 取走即删除,中途切走再切回不会覆盖用户已经打好的内容。
  • 删掉 POST /chat-git/set-model:它是为了让重建的新线跑在选定模型上而写的全局默认模型,一个历史卡片上的动作会改掉整个部署的模型。既然不再自动发送,这个时机消失了,路由与对话框里的模型下拉框一并移除;host 测试改为断言这条路由已经不存在,而不是留着没人调用的全局写入口。
  • 第 1 轮重建的会话留在原工作区分组里:以前只传 cwd,而 cwd 只决定运行目录、不进分组(host 侧只有 { workspaceId } 会走 attachSession),所以点第 1 轮的 编辑并发送 会在侧栏多出一个「未分组」条目,即使那个目录就是工作区本身。现在先按源会话反查它所属的 workspaceId 再创建,新会话与原会话同组;只有拿不到工作区服务时才退回 { cwd }(两者在协议上互斥)。
  • 历史时间戳显示年月日 + 时分秒(2026-09-13 14:05:32):只写 14:05 答不出「这是今天、昨天还是上周」,而两个回合落在同一分钟里是常事。
  • 「编辑并发送」改为先弹窗选去向:点下去不再直接动作,而是弹出 当前对话 / 新建对话 / 取消 三个按钮 —— 当前对话(还原) 在本窗口里回退到上一轮并从这一轮重新开始(原对话随即归档,可从归档恢复),新建对话(fork) 另开一条只到上一轮的会话并切换过去(原对话保持原样,两条线都能继续),取消 什么都不做。两者的唯一区别就是你正站着的这条对话还留不留下,而这件事在动作发生之后无法从界面上读出来,所以它必须在发生之前被问一次。读取整段请求与会话移动都发生在选完去向之后,失败因此报在这个对话框里。
  • 卡片按钮改名:从这里 fork → 从这里打开新对话(fork),回退到这里 → 回退到这里(还原)。原来的写法把英文动词丢给用户猜,而「回退」在 DSH 里到底是删掉还是归档,只有括号里那个词说得清。
  • 「编辑并发送」不再归档原会话,且给新线一个可区分的标题:以前切换过去之后会把原会话归档,而 fork 出来的新线继承同一个标题 —— 两件事叠在一起,侧栏里刚点过的那条对话看起来就像被「回退到了上一轮」,用户回头找不到自己原来的对话。现在 新建对话 是纯粹的分叉:原会话每一轮都在、原样留在侧栏,新线通过 increaseTitle 拿到 … (1) 这样的标题;当前对话 与 回退到这里(还原) 是仅有的两个会归档的动作。
  • 卡片记住这一轮带过几张图片:时间线现在返回 imageCount,带图片的卡片多一行 含 N 张图片。只写数量、不铺缩略图 —— 历史列表是列表而不是图库;句柄本身由唯一需要搬动它们的那一步(编辑并发送)去原会话重读。

0.5.0

  • 历史左栏改为倒序:最新一轮排在最上面,与右栏的 git log 方向一致;每一项仍带着它在时间线里的原始下标,所以「编辑并重新发送」需要的「上一轮」边界不受影响。
  • 当前轮带 当前位置 标签:最新一轮的卡片带药丸标签,列表上方另有一行「当前位置:第 N 轮。」。
  • 修复压缩检查点被当成用户提示词:上下文压缩会用一条 user/message(source.plugin === 'compact')替换被压缩的片段,折叠时间线时它曾被当成该轮的提示词显示在卡片上,并会被「编辑并重新发送」原样发给模型。现在按来源标记跳过,卡片与编辑器都只认真实输入。
  • 测试 421 项(host 205 / client 216)。

0.4.9

  • 工作区 Git 还原改为弹窗确认:行上的按钮只打开对话框(此时不发请求),对话框写明提交主题、HEAD 不动、未跟踪文件不清理,以及 当前位置 → 还原后位置;取消 成为一等出口。
  • 新增「当前位置」标签:当前所在提交的那一行带 当前位置 药丸标签、按钮变为不可点的 已在此位置,列表上方另有一行「当前位置:…」。位置记在 store 的 position 字段里,/chat-git/repo 同时返回 positionKnown。
  • 「编辑并重新发送」支持重选模型:对话框内新增扁平化模型下拉框,默认选中当前路由;新增 POST /chat-git/set-model 在会话重建之前写入默认选择,并对照实时模型目录校验。

0.4.8

  • 历史卡片新增 编辑并重新发送:载入该轮完整原始提示词,确认后删掉该轮及其之后的所有对话并重发一次,原会话归档可恢复。
  • 新增 POST /chat-git/turn-prompt,返回不截断的整段提示词。

更早版本的变更见提交历史。


许可证

MIT © NOOB-P

purpose
session-title
source: { kind: 'plugin', plugin: 'chat-git' }
  • 标题调用发生在暂存之后、提交之前。 先把 git add -A -- . 做掉,标题调用才能看到这一轮真正改了哪些文件 —— 这是「总结这一轮做了什么」而非「复述请求」的关键。代价是每轮结束会等一次模型调用(上限 8 秒);换来的是提交必定早于客户端渲染该轮的回退按钮,两者不会赛跑。
  • 偏好校验放在 store,不放在路由。 setSummary 自己拒绝未知的 mode,以及缺少 provider 或 model 的 custom —— 这样每个写入方都被覆盖,而路由只是转发。另一面是部分补丁会合并:只发 { mode: 'custom' } 会保留已存好的路由,因此「切模式」和「选路由」可以分两步走,不会互相清空。
  • 选择器只提供可用的东西,但保留已成事实的配置。 没有已注册模型的服务商不出现在列表里(选它必然被 host 拒绝,等于设陷阱);而已存储、注册表却不再列出的 provider / model 仍然可选 —— 否则打开一次设置页就会让用户的配置变得不可达。模型目录读取带 60 秒缓存,且每个服务商各自 4 秒上限,卡住的服务商只会退化成空模型列表,不会拖住整个设置页。
  • 历史标签页的数据来自会话日志,不来自客户端已渲染的内容。 Session.snapshotEvents() 的日志里 turn/start / user/message / turn/end 齐备,而 turn/end 事件自身的 seq 就是 fork 边界。这样读是权威的:不取决于聊天视图当前渲染了哪几轮,所以早已滚出窗口、甚至本次进程重启前发生的那一轮,同样拿得到可用的边界;代价是读取只对已加载的会话有效,未加载时如实报 session-unavailable,而不是猜一个边界。
  • fork 与回退在 DSH 里是同一个原语,区别在原会话的去向。 没有「截断会话」API,两者都只能 sessions.fork({ atSeq }) 出一个到该轮为止的新会话,并在切换过去之前把剩余检查点 inherit 给新会话。区别是 回退 会额外把原会话归档(可恢复),让列表反映你实际走上的那条线;fork 则原样保留,两条线都能继续。
  • 没有结束序列的一轮不允许分叉。 仍在进行中的轮次(或日志被截断)拿不到 turn/end,此时按钮置灰并说明原因 —— 猜一个边界会产出错误的 fork,比不给按钮更糟。
  • 对话与仓库分成两栏、两条路由,互不读写。 左栏只发 /chat-git/timeline、只做 sessions.fork + 归档;右栏只发 /chat-git/repo 与 /chat-git/restore。这不只是排版:两侧的失败因此彼此隔离(仓库坏掉不会带走对话列表),而且 restore 刻意不复用 /chat-git/revert —— 后者会顺手丢掉该轮之后的检查点,那是「连对话一起回退」才需要的语义,右栏只动工作区,所以那一步必须不发生。代价是右栏能还原仓库里的任意提交(包括不是本插件创建的提交),因此 sha 在进入 argv 之前先按对象名形状校验,再经 rev-parse --verify <sha>^{commit} 解析成完整提交 id:既不做路径拼接,也不可能把 --upload-pack=… 这类字符串当选项传下去。
  • 自动保存间隔只节流 git,不节流对话。 每轮对话都由 harness 自己的会话日志逐轮记录,所以把间隔设成 2 或 3 只会让工作区的改动攒着,到下一个整数倍轮次一次性提交 —— 对话历史不会因此变稀疏,中间的改动也不会丢,它们就留在工作区里(右栏如实显示为未提交改动)。取值在 store 里校验为 1–10 的整数:0 或越界值会被拒绝而不是存下来,否则「每轮提交」的开关还亮着,实际上却再也不会提交。
  • 工作区根目录可以从活着的会话头解析。 只信 store 里记的 cwd 是一个真实的 bug:插件加载时就已经打开的会话(或 harness 重启后从磁盘恢复的会话)在某一轮结束前没有记录,于是右栏对眼前明明存在的仓库回一句「没有记录到工作区」。因此 store 是快路径、会话的 header.cwd 是权威兜底,解析结果顺手记下来。cwd 仍然不来自请求,所以路由依旧无法被指向任意目录。
  • 提交行按 git log 的样子排版:图槽 + id + 主题 + 作者 · 时间。 右栏列的是仓库的真实历史(含非本插件写的提交),所以作者必须显示。图槽只用 CSS 画一条竖线加一个圆点 —— 这份数据是线性历史,画真正的泳道会暗示并不存在的分支结构。
  • 失败要说清楚是谁失败,并且要能重试。 右栏第一次上线时的报错是「读取仓库失败」,可真正的原因既不是仓库也不是 git:/chat-git/repo 这条路由根本没挂上(宿主进程还是旧构建),网页服务器自己回的是 { error: 'not found' },而客户端只读了 error.message —— 字符串没有 .message,于是拿到 undefined、落回那句泛泛的兜底文案,把唯一的线索丢了。现在错误信封先归一化成 { code, message },右栏显示宿主原话并给出重试按钮;同时不再在报错时同时显示「没有可读取的仓库」——把路由问题说成仓库问题,正好是误导用户的那个诊断。
  • 「编辑并发送」需要整段请求,所以它有自己的路由。 /chat-git/timeline 是给卡片用的,每段提示词都被截到 300 字;把这段展示用的截断文本交给新会话,等于悄悄让用户对着一个自己从没写过的问题继续。因此 turnPrompt() 与 buildTimeline() 分开:前者返回折叠出来的原文与图片句柄,后者只负责截断。这个读取发生在会话被移动之前,所以「读不到」报在用户按下按钮的地方,而且什么都不用回滚。
  • 重建仍是一次 fork,只是边界落在「上一轮」。 选第 N 轮时,截断点必须是第 N-1 轮的结束序列 —— 第 N 轮及其之后才是要消失的部分。第 1 轮没有上一轮,fork 在「没有边界」时会保留整段对话,所以那一档改为在同一工作区 sessions.create() 出一个全新会话,这才是「这一轮之前什么都不留」的诚实表达。
  • 它不再替你发送。 旧行为是「截断 → 立刻排队发送 → 归档原会话」,模型在用户还没看清新会话长什么样之前就开始回答了;而且发送失败时对话已经搬走,只能事后报错。现在顺序是「重建 → 把请求寄存到新会话 id → 切换」,发送键留给用户,会话已重建但发送失败 这个状态根本不存在了。它也不归档原会话:归档加上「分叉出来的新线继承同一个标题」,会让侧栏里刚点过的那条对话看起来像被回退到了上一轮 —— 而这是分叉,原对话每一轮都该还在。
  • 转交靠 conversation.input.dock 上的一个无渲染席位。 新会话的输入区属于另一个插件(ui-conversation),而且它此刻还不存在 —— sessions.open 是请求,不是同步切换。所以请求按 session id 寄存在插件自己的 Map 里,由注册在输入区 slot 上的 ComposerSeeder 在它自己那一轮的输入区挂载时取走,并只通过 shell 公布的 inputActions.setDraft / addImages 写入 —— 绝不去碰另一个插件的内部状态。取走即删除,否则用户中途切走再切回来,第二次挂载会把人家刚写的内容覆盖掉。
  • cwd 只来自宿主自己知道的两个来源。 见第 18 条:store 的记账与会话头的 header.cwd,二者都没有就如实拒绝(session-unknown),绝不从请求体里取路径,否则这条路由就成了任意目录读取器。
  • 还原的确认从「按钮点两下」改成对话框。 一次 git checkout 会覆盖整个工作区,而确认它需要的是一句话(「你正在离开哪个提交、要去哪个提交、什么会被留下」),列表行里放不下这句话 —— 按钮变文字只是让人再点一次同一个按钮,等于把确认的内容省掉了。所以行上的按钮只负责打开对话框(此时不发任何请求),对话框里写明提交主题、HEAD 不动、未跟踪文件不清理,以及当前位置 → 还原后位置,确认才发 /chat-git/restore。副作用是「取消」变成了真正的一等出口。
  • 「当前位置」是记在 store 里的,不是从 git 读的。 还原刻意不动 HEAD(第 2 条),所以还原之后 git rev-parse HEAD 答的已经不是「工作区站在哪儿」。位置因此记在 sessionId -> position:每次提交把它移到新提交,每次还原把它移到被还原的提交。同时返回 positionKnown,把「宿主有记录」和「宿主没有记录、这里只是 HEAD」分开 —— 右栏对这两种情况用不同措辞,而不是把推断说成事实。落到界面上就是三处:行首的 当前位置 标签、列表上方的「当前位置:…」一行、以及该行按钮变成不可点的 已在此位置(还原到自己已经站着的地方是个只会报成功的空操作)。
  • 历史左栏倒序,但序号是原始序号。 左栏与右栏(git log)方向保持一致:最新一轮在最上面。倒序只是渲染顺序 —— 每一项都带着它在时间线里的真实下标,因为「编辑并发送」需要的分叉边界属于上一轮(第 N-1 轮),而不是列表里的下一行。把倒序做进数据层会让那个边界指向错误的一轮。
  • 压缩检查点不是用户提示词。 上下文压缩会把一段历史替换成一条合成的 user/message,它同样落在 turn/start 与 turn/end 之间。若不区分,卡片会显示「This is an automatically generated checkpoint …」这类前言,而「编辑并发送」会把它当成用户请求交回去。因此按来源标记(source.kind === 'plugin' && source.plugin === 'compact')跳过,而不是按文案匹配:标记是协议,文案随时会变。标记写死在插件里而不是 import,是因为本插件零依赖 —— 标记若变,代价只是退回旧行为,绝不会崩。
  • 本插件不再写「默认模型」。 旧版本为了让重建的新线跑在选定的模型上,先调 POST /chat-git/set-model 写入部署的默认选择 —— 那是一个全局副作用:一个历史卡片上的动作会改掉整个部署的模型,而模型选择器本身对这件事毫不知情。既然现在不再自动发送,这个时机就消失了:请求落进输入区,用户按 shell 自己的模型选择器选好再发,插件没有理由替他做这个决定。因此这条路由被整个删掉,对应的 host 测试改成断言它已经不存在 —— 留着一个没人调用的全局写入口,比删掉更危险。
  • 图片按原会话的授权读回来。 新线是在「选中的那一轮之前」切开的,那张图的持久引用并不在新会话的日志里,所以它无法授权自己的字节。转交时用原会话的 readAttachment 把字节读成浏览器文件,再经 conversation.createDraftImages 铸成新会话的草稿图片 id(addImages 只认这种 id)。读不回来就放弃图片、保留文字:一个少了图片的请求仍然比一个根本交不过去的请求有用。
  • 测试 421 项(host 203 / client 218)。