dsh-worktree-switch 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 deepseek-plugins contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,20 +2,33 @@
2
2
 
3
3
  在新会话屏幕的输入框工具行里放一个 **Worktree 开关**。默认打开(按项目记忆):
4
4
  打开时立刻在 `<项目根>/.dsh/worktrees/<名字>` 建一份仓库副本,并把这条会话的工作
5
- 目录切进去;关掉时什么都不做,会话就在原目录里跑。副本不会自动消失、也不会自动合并,
6
- 用户可以在 **Worktrees 面板**里把它丢弃,或者自己把分支合回源仓库。
5
+ 目录切进去;在发出第一条消息之前再关掉,会话会被切回项目目录,开关自己刚建的干净
6
+ 副本也一并删掉——关掉就是撤销这次隔离。副本不会自动消失、也不会自动合并,用户可以
7
+ 在 **Worktrees 面板**里把它丢弃,或者自己把分支合回源仓库。会话在副本里时,宿主半还会
8
+ 给模型一条工作区说明,并拒绝把 git 指向主检出的 shell 调用(可关)。
7
9
 
8
10
  ## 它做什么
9
11
 
10
12
  - **开关**(`conversation.input.left` 槽,只在空白新会话的 hero 上出现):显示本会话
11
- 当前的副本状态——检查中 / 关闭 / 创建中 / 就绪(显示名字)/ 失败(显示原因 + 重试)
12
- / 不可用(项目不是 Git 仓库)。开关位置本身记的是"这个项目的新会话要不要隔离"。
13
- - **创建**发生在开关打开的那一刻,早于第一条消息。副本名 = 分支名 = 池内目录名,形如
13
+ 当前的副本状态——检查中 / 关闭 / 创建中 / 离开中 / 就绪(显示名字)/ 失败(显示原因 +
14
+ 重试)/ 不可用(项目不是 Git 仓库)。开关位置本身记的是"这个项目的新会话要不要隔离"。
15
+ - **创建**发生在开关打开的那一刻,早于第一条消息;**关闭**发生在第一条消息之前时,
16
+ 会话被切回项目目录,开关在这次打开中建的那份干净副本被删除(脏副本或开关之前就存在
17
+ 的副本留在池里,交给面板处理)。副本名 = 分支名 = 池内目录名,形如
14
18
  `quiet-harbor-3fa9`,由插件生成,短且唯一。
15
- - **创建失败不回落**:提示词不发送,错误就地显示。从检查开始、到副本就绪之前(或失败
16
- 之后),开关通过 `ctx.conversation.blocks` 把该会话的输入框置为"暂停发送";重试成功、
17
- 关掉开关(确认不要隔离)或副本就绪时才解除。关掉开关是明确的退出方式——它表示"这条
18
- 会话就在原目录里跑",而不是假装隔离成功。
19
+ - **创建失败不回落**:提示词不发送,错误就地显示。从检查开始、到副本就绪之前、离开副本
20
+ 的过程中(或失败之后),开关通过 `ctx.conversation.blocks` 把该会话的输入框置为"暂停
21
+ 发送";重试成功、关掉开关(确认不要隔离)或副本就绪时才解除。关掉开关是明确的退出
22
+ 方式——它表示"这条会话就在原目录里跑",而不是假装隔离成功。
23
+ - **会话范围说明**:会话在副本里时,宿主半跟着每轮请求给模型补一条上下文(紧跟工作目录那
24
+ 行):当前目录是 `<项目>` 的 linked worktree 副本,git 与文件操作在副本里做,主检出是源。
25
+ 它会随恢复会话、每次进/出副本重新推导,回到项目目录后自动消失。这是**提示**,不是权限
26
+ 边界——真正的写隔离仍取决于沙箱根(见「已知边界」)。
27
+ - **主检出守卫**:会话在副本里时,`tools/pre-execute` 会拦下“把 git 指向主检出”的 bash
28
+ 调用(命令里出现主检出的绝对路径,或 `workdir` 落在主检出里),拒绝原因直接告诉模型
29
+ 在副本里该怎么做(`git fetch origin main` + `merge`/`rebase`)。这是启发式拦截,不是
30
+ 沙箱边界:`cd ..` 之类的相对逃逸、shell 变量展开不在它的覆盖范围内;配置
31
+ `guardSourceCheckout: false` 可关掉。拦下的是 git,主检出里的普通写入仍然由沙箱放行。
19
32
  - **面板**(侧边栏条目 + 自己的页面):按项目列出该仓库的所有 linked worktree(包括
20
33
  手工 `git worktree add` 出来的),含名字、分支、路径、创建时间和未提交改动数;丢弃
21
34
  需要第二次确认,且**有未提交改动时拒绝丢弃**并说明原因。丢弃会删掉检出目录和分支。
@@ -47,8 +60,9 @@ dsh --profile web --dump-config # 应能看到 "# == dsh-worktree-switch" 和
47
60
  未提交内容留在原地。
48
61
  4. 侧边栏出现 **Worktree** 入口:打开后按项目列出磁盘上的副本,含创建时间与未提交改动。
49
62
  5. 想丢弃一份副本:点两次 Discard(第二次是确认);有未提交改动时按钮禁用并说明原因。
50
- 6. 想换个项目默认:在新会话的开关上拨一下。关掉只影响**下一个**会话,当前会话如果已经
51
- 建好副本,仍继续在副本里跑(提示里会写明这一点)。
63
+ 6. 开关只在空白会话上出现:发第一条消息之前关掉,会把会话切回项目目录并删掉刚建的空
64
+ 副本;发过消息之后开关消失,副本留在池里,由面板丢弃。拨开关同时记住这个项目下次
65
+ 的默认位置。
52
66
 
53
67
  ## 配置
54
68
 
@@ -77,6 +91,14 @@ worktree 服务自己的 `directory` 选项——本插件不另存一份设置
77
91
  - 目录对每个项目都生效(是相对路径,不是在某个绝对位置开池)。若想按项目不同,只能
78
92
  在项目里放不同的 profile——目前没做这种粒度。
79
93
 
94
+ **行 id 撞车**:dsh 0.2.2 的 Forge bundle(`@deepseek-ai/dsh-experimental-forge-bundle`)
95
+ 也插入一行 `id: worktree`。Loader 按行 id 只留一条、后层覆盖,所以本包的
96
+ `directory: '.dsh/worktrees'` 只在**本包的层排在那份 bundle 之后**时才生效——默认情况正是
97
+ 如此(profile 先列宿主 bundle,再列装进来的插件)。如果你手动调整了 bundle 顺序,请在
98
+ profile 的 patch 层里按上面的写法重新声明 `directory`,否则池会静默退回 harness 的默认
99
+ 目录(`.agents/worktrees`)。这条只影响目录位置,开关、面板与丢弃逻辑都按服务实际用的
100
+ 池子工作。
101
+
80
102
  改完重启 profile 生效。已经在旧目录里的副本不会消失:面板列的是仓库的所有 linked
81
103
  worktree,所以新旧目录的副本都会出现,可以照常丢弃。
82
104
 
@@ -85,6 +107,7 @@ worktree,所以新旧目录的副本都会出现,可以照常丢弃。
85
107
  | 字段 | 默认 | 含义 |
86
108
  |---|---|---|
87
109
  | `defaultEnabled` | `true` | 用户从未在这个项目上拨过开关时,新会话是否默认隔离。用户拨过一次后,按项目记在浏览器 localStorage 里,优于此项。 |
110
+ | `guardSourceCheckout` | `true` | 会话在副本里时,用 `tools/pre-execute` 拒绝把 git 指向主检出的 bash 调用。关掉就只剩提示。 |
88
111
 
89
112
  ## 目录与命名
90
113
 
@@ -105,18 +128,22 @@ worktree,所以新旧目录的副本都会出现,可以照常丢弃。
105
128
  定位文件)显示的是源项目根,而不是副本。这是刻意的:沙箱边界与侧边栏归属都依赖它。
106
129
  - 沙箱边界仍是源项目根,因此副本不是"写隔离",它隔离的是**提交与未提交改动**:
107
130
  助手在副本里改的文件不会落到源检出的工作树里,但同一会话仍可写源项目根下的文件。
131
+ 主检出守卫只覆盖 bash 里明显指向主检出的 git 调用,是提示之外的兜底,不是权限。
108
132
  - 不自动合并、不自动回收、不支持多仓库、不识别旧插件的 `.worktree-space/` 目录。
109
133
 
110
134
  ## 实现
111
135
 
112
- 宿主半(`src/index.ts`)只做两件事:拿 `cordis.patch.yml` 挂上的 `ctx.worktrees` 和
113
- `ctx.sessionController`,把四个动词注册成 `/api/dsh-worktree-switch/<verb>` 上的精确
114
- Fetch 路由(`src/host/routes.ts`),说 `client-request` / `server-response` 信封:
136
+ 宿主半(`src/index.ts`)做三件事:拿 `cordis.patch.yml` 挂上的 `ctx.worktrees` 和
137
+ `ctx.sessionController`,把五个动词注册成 `/api/dsh-worktree-switch/<verb>` 上的精确
138
+ Fetch 路由(`src/host/routes.ts`),说 `client-request` / `server-response` 信封;另注册一条
139
+ `systemPrompt.context`(`worktree-switch:scope`),内容来自 `SessionScopeRegistry`,由五个
140
+ 动词、`agent/created` 和每次组装时的目录校验共同维护。
115
141
 
116
142
  | 动词 | 入参 | 行为 |
117
143
  |---|---|---|
118
144
  | `status` | `{ sessionId }` | 解析仓库根、本会话是否已在某个 linked worktree 里、源仓库未提交改动数 |
119
145
  | `prepare` | `{ sessionId }` | 幂等;必要时经 `ctx.worktrees.create` 建副本并切目录,同名冲突自动换名重试 |
146
+ | `release` | `{ sessionId }` | 把会话切回项目目录;删掉开关自己建的那份干净副本,脏副本与非开关建的副本留在池里 |
120
147
  | `list` | `{ path }` | 列出该仓库除主检出外的所有 linked worktree,返回名字/分支/路径/创建时间/改动数 |
121
148
  | `discard` | `{ path, name }` | 干净才删:`git worktree remove` + `git branch -D`;改动非空则拒绝 |
122
149
 
@@ -133,7 +160,7 @@ pnpm --filter dsh-worktree-switch test
133
160
  pnpm build # 构建全部包并校验 exports / bundle patch 文件存在
134
161
  ```
135
162
 
136
- 测试只有两条接缝:宿主半的四个动词(真实临时 Git 仓库 + 真实服务,`tests/host.spec.ts`)
163
+ 测试只有两条接缝:宿主半的五个动词(真实临时 Git 仓库 + 真实服务,`tests/host.spec.ts`)
137
164
  和浏览器半的槽注册与渲染(真实 `SlotRegistry` + 假 `connection`/`conversation`,
138
165
  `tests/*.client.spec.tsx`)。不断言内部 Git 调用次数,也不测上游 worktree 服务自己的
139
166
  Git 细节。
package/cordis.patch.yml CHANGED
@@ -5,6 +5,14 @@
5
5
  # bundle uses: a profile that already mounts the model-facing worktree tool
6
6
  # overrides this row instead of registering the `worktrees` service twice
7
7
  # (Cordis throws on a duplicate service name).
8
+ #
9
+ # dsh 0.2.2's @deepseek-ai/dsh-experimental-forge-bundle inserts that same
10
+ # `worktree` id too. Loader keeps one row per id and the last layer wins, so
11
+ # this row's `config` (including `directory`) survives only while this bundle's
12
+ # layer is applied after the bundle that owns the other `worktree` row — which
13
+ # is the normal case, because a profile lists host bundles before installed
14
+ # plugins. If you reorder bundles, restate `directory` in the profile's own
15
+ # patch layer, or the pool silently falls back to the service default.
8
16
  - insert:
9
17
  - id: worktree
10
18
  name: '@deepseek-ai/dsh-experimental-worktree'
package/lib/client.js CHANGED
@@ -30,6 +30,7 @@ function chipLabel(state, t) {
30
30
  case "checking": return t("chip.checking");
31
31
  case "off": return t("chip.off");
32
32
  case "creating": return t("chip.creating");
33
+ case "reverting": return t("chip.reverting");
33
34
  case "ready": return t("chip.ready", { name: state.worktree?.name ?? "" });
34
35
  case "failed": return t("chip.failed");
35
36
  case "disabled": return t("chip.disabled");
@@ -46,6 +47,7 @@ function chipHint(state, t) {
46
47
  case "checking": return t("chip.hint.checking");
47
48
  case "off": return t("chip.hint.off");
48
49
  case "creating": return t("chip.hint.creating");
50
+ case "reverting": return t("chip.hint.reverting");
49
51
  case "ready": {
50
52
  const parts = [t("chip.hint.ready", {
51
53
  branch: state.worktree?.branch ?? "",
@@ -53,6 +55,7 @@ function chipHint(state, t) {
53
55
  })];
54
56
  if (!state.intent) parts.push(t("chip.hint.intentOff", { name: state.worktree?.name ?? "" }));
55
57
  if (state.sourceDirty > 0) parts.push(t("chip.hint.dirty", { count: state.sourceDirty }));
58
+ if (state.error !== null) parts.push(state.error);
56
59
  return parts.join(" ");
57
60
  }
58
61
  case "failed": {
@@ -79,7 +82,7 @@ function WorktreeChip(props) {
79
82
  block(null);
80
83
  return;
81
84
  }
82
- const reason = state.phase === "checking" ? t("chip.blocked.checking") : !state.intent ? null : state.phase === "creating" ? t("chip.blocked.creating") : state.phase === "failed" ? t("chip.blocked.failed") : null;
85
+ const reason = state.phase === "checking" ? t("chip.blocked.checking") : state.phase === "creating" ? t("chip.blocked.creating") : state.phase === "reverting" ? t("chip.blocked.reverting") : state.phase === "failed" && state.intent ? t("chip.blocked.failed") : null;
83
86
  block(reason);
84
87
  }, [
85
88
  block,
@@ -93,7 +96,7 @@ function WorktreeChip(props) {
93
96
  }, [block]);
94
97
  if (!hero) return null;
95
98
  const hint = chipHint(state, t);
96
- const locked = state.phase === "checking" || state.phase === "disabled";
99
+ const locked = state.phase === "checking" || state.phase === "disabled" || state.phase === "reverting";
97
100
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
98
101
  className: "dsh-wt-chip",
99
102
  "data-phase": state.phase,
@@ -411,6 +414,7 @@ const en = {
411
414
  "chip.checking": "Worktree…",
412
415
  "chip.off": "Worktree off",
413
416
  "chip.creating": "Creating worktree…",
417
+ "chip.reverting": "Leaving worktree…",
414
418
  "chip.ready": "Worktree: {name}",
415
419
  "chip.failed": "Worktree failed",
416
420
  "chip.retry": "Retry",
@@ -419,13 +423,15 @@ const en = {
419
423
  "chip.hint.off": "New sessions in this project start in the project directory.",
420
424
  "chip.hint.on": "This session runs in a copy of the project; the source checkout is untouched.",
421
425
  "chip.hint.creating": "Creating a checkout inside the project…",
426
+ "chip.hint.reverting": "Moving this session back to the project directory…",
422
427
  "chip.hint.ready": "Branch {branch} at {path}",
423
428
  "chip.hint.failed": "The copy was not created, so this session is still in the project directory.",
424
429
  "chip.hint.disabled": "This project is not a Git repository, so there is nothing to copy.",
425
430
  "chip.hint.dirty": "The source checkout has {count} uncommitted change(s); the copy starts from the last commit.",
426
- "chip.hint.intentOff": "Applies to the next session; this one keeps {name}.",
431
+ "chip.hint.intentOff": "Turning off could not leave {name}; this session still runs in it.",
427
432
  "chip.blocked.checking": "Checking whether this session gets a worktree…",
428
433
  "chip.blocked.creating": "Creating the worktree before the first prompt…",
434
+ "chip.blocked.reverting": "Leaving the worktree before the first prompt…",
429
435
  "chip.blocked.failed": "The worktree could not be created. Retry, turn the switch off, or discard the failed copy first.",
430
436
  "panel.title": "Worktrees",
431
437
  "panel.subtitle": "Copies created by the Worktree switch: one checkout and one branch each. Discard removes both.",
@@ -455,6 +461,7 @@ const zh = {
455
461
  "chip.checking": "Worktree…",
456
462
  "chip.off": "Worktree 已关",
457
463
  "chip.creating": "正在创建 Worktree…",
464
+ "chip.reverting": "正在离开 Worktree…",
458
465
  "chip.ready": "Worktree:{name}",
459
466
  "chip.failed": "Worktree 创建失败",
460
467
  "chip.retry": "重试",
@@ -463,13 +470,15 @@ const zh = {
463
470
  "chip.hint.off": "这个项目的新会话直接在项目目录里开始。",
464
471
  "chip.hint.on": "本会话在项目的一份副本里运行,源仓库不受影响。",
465
472
  "chip.hint.creating": "正在项目里创建检出…",
473
+ "chip.hint.reverting": "正在把本会话切回项目目录…",
466
474
  "chip.hint.ready": "分支 {branch},位于 {path}",
467
475
  "chip.hint.failed": "副本没有创建成功,本会话仍然在项目目录里。",
468
476
  "chip.hint.disabled": "这个项目不是 Git 仓库,没有可复制的内容。",
469
477
  "chip.hint.dirty": "源仓库有 {count} 处未提交改动;副本会从最后一次提交开始。",
470
- "chip.hint.intentOff": "仅对下一个会话生效;本会话仍然使用 {name}。",
478
+ "chip.hint.intentOff": "关闭时没能离开 {name},本会话仍在副本里运行。",
471
479
  "chip.blocked.checking": "正在检查本会话是否需要 Worktree…",
472
480
  "chip.blocked.creating": "正在创建 Worktree,副本就绪前暂停发送…",
481
+ "chip.blocked.reverting": "正在离开 Worktree,切回项目目录前暂停发送…",
473
482
  "chip.blocked.failed": "Worktree 创建失败。请重试、关闭开关,或先丢弃失败的副本。",
474
483
  "panel.title": "Worktree",
475
484
  "panel.subtitle": "Worktree 开关创建的副本:每份副本是一个检出加一个分支,丢弃会同时删掉两者。",
@@ -623,6 +632,10 @@ var InternalBinding = class {
623
632
  started = false;
624
633
  /** The project whose remembered choice is in play; null until status answers. */
625
634
  project = null;
635
+ /** A create is in flight; a second one would race the same copy. */
636
+ creating = false;
637
+ /** The release in flight, shared by callers that ask to leave at once. */
638
+ releasing = null;
626
639
  face;
627
640
  /**
628
641
  * @param ctx - browser Cordis context.
@@ -683,6 +696,7 @@ var InternalBinding = class {
683
696
  ...base,
684
697
  phase: "ready"
685
698
  });
699
+ if (!intent) this.release();
686
700
  return;
687
701
  }
688
702
  if (!intent) {
@@ -702,8 +716,10 @@ var InternalBinding = class {
702
716
  }
703
717
  /** Create the copy for a session the switch says should have one. */
704
718
  async create() {
719
+ if (this.creating) return;
705
720
  const before = this.store.getSnapshot();
706
- if (before.repositoryRoot === null) return;
721
+ if (before.repositoryRoot === null || !before.intent) return;
722
+ this.creating = true;
707
723
  this.store.set({
708
724
  ...before,
709
725
  phase: "creating",
@@ -712,6 +728,10 @@ var InternalBinding = class {
712
728
  try {
713
729
  const prepared = await callHost(this.ctx, "prepare", { sessionId: this.sessionId }, this.signal);
714
730
  const now = this.store.getSnapshot();
731
+ if (!now.intent) {
732
+ if (now.phase === "reverting") this.release();
733
+ return;
734
+ }
715
735
  this.store.set({
716
736
  ...now,
717
737
  phase: "ready",
@@ -724,12 +744,7 @@ var InternalBinding = class {
724
744
  if (this.signal.aborted) return;
725
745
  const now = this.store.getSnapshot();
726
746
  if (!now.intent) {
727
- this.store.set({
728
- ...now,
729
- phase: "off",
730
- worktree: null,
731
- error: null
732
- });
747
+ if (now.phase === "reverting") this.release();
733
748
  return;
734
749
  }
735
750
  this.store.set({
@@ -737,22 +752,76 @@ var InternalBinding = class {
737
752
  phase: "failed",
738
753
  error: messageOf(error)
739
754
  });
755
+ } finally {
756
+ this.creating = false;
757
+ }
758
+ }
759
+ /**
760
+ * Leave the copy this session runs in, so "off" means the project directory.
761
+ * Concurrent callers share one round trip.
762
+ * @returns completion of the leave.
763
+ */
764
+ release() {
765
+ const running = this.releasing;
766
+ if (running !== null) return running;
767
+ const operation = this.leave().finally(() => {
768
+ if (this.releasing === operation) this.releasing = null;
769
+ });
770
+ this.releasing = operation;
771
+ return operation;
772
+ }
773
+ /** Ask the host to undo the copy and settle the switch on what it reported. */
774
+ async leave() {
775
+ const before = this.store.getSnapshot();
776
+ this.store.set({
777
+ ...before,
778
+ phase: "reverting",
779
+ error: null
780
+ });
781
+ try {
782
+ await callHost(this.ctx, "release", { sessionId: this.sessionId }, this.signal);
783
+ if (this.signal.aborted) return;
784
+ const now = this.store.getSnapshot();
785
+ this.store.set({
786
+ ...now,
787
+ phase: "off",
788
+ intent: false,
789
+ worktree: null,
790
+ error: null
791
+ });
792
+ } catch (error) {
793
+ if (this.signal.aborted) return;
794
+ const now = this.store.getSnapshot();
795
+ this.store.set({
796
+ ...now,
797
+ phase: now.worktree === null ? "off" : "ready",
798
+ intent: false,
799
+ error: messageOf(error)
800
+ });
740
801
  }
741
802
  }
742
803
  /** Persist and apply one switch move. */
743
804
  setIntent(next) {
744
805
  const state = this.store.getSnapshot();
745
- if (state.phase === "checking" || state.phase === "disabled") return;
806
+ if (state.phase === "checking" || state.phase === "disabled" || state.phase === "reverting") return;
746
807
  if (this.project !== null) writeIntent(this.project, next);
747
- const phase = !next && state.phase === "failed" ? "off" : state.phase;
808
+ if (!next) {
809
+ if (state.intent === next) return;
810
+ this.store.set({
811
+ ...state,
812
+ intent: false,
813
+ error: null
814
+ });
815
+ this.release();
816
+ return;
817
+ }
818
+ if (state.intent === next) return;
748
819
  this.store.set({
749
820
  ...state,
750
- phase,
751
- intent: next,
752
- error: next ? state.error : null
821
+ intent: true,
822
+ error: null
753
823
  });
754
- if (!next || state.intent === next) return;
755
- if (phase === "off") this.create();
824
+ if (state.phase === "off" || state.phase === "failed") this.create();
756
825
  }
757
826
  /** Re-run the host round trip with the switch on. */
758
827
  retry() {
package/lib/index.d.ts CHANGED
@@ -13,6 +13,8 @@ export declare const inject: string[];
13
13
  export interface Config {
14
14
  /** Default for a project the user has never switched either way. */
15
15
  defaultEnabled: boolean;
16
+ /** Refuse shell commands that run Git against the project checkout while the session is in a copy. */
17
+ guardSourceCheckout: boolean;
16
18
  }
17
19
  /** Schemastery schema for {@link Config}. */
18
20
  export declare const Config: Schema<Config>;