dsh-composer-ux 0.18.0 → 0.19.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/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.19.1] — 2026-10-09(只改文档:README 的变异条数写旧了)
6
+
7
+ 0.19.0 发版时 README 里那句变异条数还写着上一版的 **174**,而这一版实际跑出来是 **180**
8
+ (新增 GZ/HA/HB/HC/HD/HE 六条)。`npm run gates` 的文档门禁当场抓住了它 —— 但 0.19.0 的
9
+ tarball 已经发出去了,所以补一个文档修正版。
10
+
11
+ **代码零改动**:29 个套件 2540 条 0 失败、变异 180/180 与 0.19.0 完全一致,只是把 README 里
12
+ 那两个数字改成事实。
13
+
14
+ ## [0.19.0] — 2026-10-09(「工作区目录选择」加设置开关,默认关)
15
+
16
+ 用户 2026-10-09:在设置里加一个条目让这个功能可以开关、**默认关闭**。
17
+
18
+ 0.18.0 把它做成了"装了就生效",而它的做法是**接管官方那两个目录流程槽位**(优先级 -1 的 shadow,
19
+ 见 0.18.0 那节)—— 属于改默认行为,不打招呼就换掉别人的选择框是越界。所以这一版补上开关:
20
+
21
+ - 设置 →「输入体验」→ 卡片「**工作区目录选择**」(在「微信通知」下面),**默认关**;
22
+ - **即时生效**,不用重启:客户端半订阅设置快照,开就装、关就卸、再开重装
23
+ (`createDirectoryFlowController` 保证幂等 —— 同一个槽位同优先级重复注册会当场抛错,
24
+ 所以"幂等"不是优化而是必需);
25
+ - **关掉即回到官方原样**:官方那条默认优先级 `0` 的条目立刻成为最低者,界面回到官方对话框;
26
+ - 卡片正文把四件事说清:改了哪个框(系统文件夹框 / 任意盘符 / 不用手输路径)、只在桌面应用里生效、
27
+ 手机与浏览器不变、以及官方自己承认的那条「不枚举盘符根 —— 跨盘依赖浏览器 UI 的路径输入入口」。
28
+
29
+ **代码**:`settings-contract.ts`(字段 `directoryPickEnabled`,默认 `false`,含类型与净化)、
30
+ `directory-flow.ts`(`installDirectoryFlow` 改为返回卸载句柄;新增 `createDirectoryFlowController`)、
31
+ `client.tsx`(改成 `ctx.effect` + `live.subscribe` 的同步器:启动同步一次、每次设置变化同步、卸载退订)、
32
+ `SettingsSection.tsx`(新增卡片 + 标题行开关;卡内不留第二个开关)。
33
+
34
+ **验证**:`npm test` —— **29 个套件 2540 passed / 0 failed**(`directory-flow` 37→49:新增控制器 12 条,
35
+ 覆盖幂等 / 关掉卸载 / 反复拨 / 远端不装;`settings-render` 92→102:新增第 9 节把两种概览、标题行开关
36
+ 随设置值、正文四件事与"卡内没有第二个开关"钉住;`client-registration` 248→256:字段 / 默认值 / 净化 /
37
+ 订阅 / 退订的接线)。`npm run test:mutations` —— **180 条全部咬住**(新增 GZ/HA/HB/HC/HD/HE:控制器
38
+ 不幂等、关掉不卸、默认值改成开、净化不读该字段、客户端写死 `true`、设置页不渲染这张卡)。
39
+
40
+ **注意**:默认关意味着**升级后这个开关是关的** —— 想用系统文件夹框的人需要自己去设置里拨一下
41
+ (即时生效,不用重启)。
42
+
5
43
  ## [0.18.0] — 2026-10-09(工作区目录选择:桌面弹系统文件夹框,手机/远端仍用官方应用内框)
6
44
 
7
45
  用户 2026-10-09:先要「换盘不用手输」,选了 A 方案(profile 补丁钉官方 `-native`,实测通过),
package/README.md CHANGED
@@ -475,7 +475,7 @@ Windows 上 DSH 给模型的终端工具是 **PowerShell**(工具名 `pwsh`)
475
475
  - **六条自开路由全部过了官方那道关卡**:`connection.requestRejection(request)`(Host/Origin 围栏 + 浏览器登录令牌)。`webServer` 是可以绑 `0.0.0.0` 的,漏掉它等于把会话用量、设置写入与**账号余额**摊给同网段任何一台机器 —— 2026-09-29 评审时先补了金额那三条,同日对比社区插件时又发现**「优化提示词」与「快捷指令存储」这两条也漏着**(一个花你的模型额度、一个会覆盖你的提示词库),一并补上,且关卡摆在**读请求体之前**(被拒的请求连 body 都不读)。现在 6/6:用量、价目同步、余额、重启、优化、快捷指令存储(终端状态那条在 `terminal/host.ts` 里自带同一道关卡)。变异 `CN`/`CO` 守着这两条。
476
476
  - **与出处有意不同的两处**(都钉在测试里):① 金额**至少保留两位小数**(出处会把 `0.1` 显示成 `¥0.1`、把 `0` 显示成 `¥0.`);② **覆盖价的币种语义**——出处是"有覆盖价时先整体重建人民币档、美元再从人民币折算",写成"在美元列上套覆盖价再折算"会差一个汇率(≈15 倍)而屏幕上只是个数字,所以专门有一条断言守着。
477
477
 
478
- ## 工作区目录选择:桌面用系统文件夹框,手机/远端用官方应用内框(0.18.0)
478
+ ## 工作区目录选择:设置里开关,桌面用系统文件夹框、手机/远端用官方应用内框(0.18.0 起,0.19.0 起可开关且默认关)
479
479
 
480
480
  **为什么会有这一节**:DSH 官方的目录选择是两个**互斥**的界面 —— `-native`(本机 OS 选择器,Windows 上就是系统文件夹框)与 `-browse`(应用内 Miller 分栏对话框),由 `-auto` 在**宿主启动时**二选一,判据只有「绑定地址 / SSH / 平台」,**不看客户端是谁**。官方在 `-browse` 的 README「已知限制」里承认了由此产生的缺口:
481
481
 
@@ -483,24 +483,33 @@ Windows 上 DSH 给模型的终端工具是 **PowerShell**(工具名 `pwsh`)
483
483
 
484
484
  于是本机换盘必须手输路径;而社区 bundle `dsh-mobile` 为了手机端能选目录又把 browse 钉死,连桌面也一起用上了应用内框(官方把「按客户端自适应」明确列为 deferred)。
485
485
 
486
+ **开关在哪儿**:设置 →「输入体验」→ 卡片「**工作区目录选择**」(在「微信通知」下面)。**默认关**,拨一下就即时生效(不用重启)。
487
+
488
+ - **关(默认)**:完全不管这件事,桌面用的是官方那个应用内浏览对话框(与没装本插件时一致)。
489
+ - **开**:桌面应用里点「选择工作区目录」→ 直接弹 **Windows 系统文件夹框**:任意盘符点进去、可新建文件夹,**不用手输路径**。
490
+
491
+ 为什么默认关:这一项的做法是**接管官方那两个目录流程槽位**(把官方的框换成系统的框),属于改默认行为 —— 不打招呼就换掉别人的选择框是越界,所以留给你自己点(与「微信通知」总开关默认关同一个理由)。
492
+
486
493
  **本插件怎么做的**(不改官方任何文件、不抄官方那一千多行界面):用槽位自身的 shadow 能力 —— 官方 `ui-slots` 的规则是「entries sharing one cell coexist at distinct priorities … the cell's lowest live entry renders」,只有**同优先级**的第二次注册才报错:
487
494
 
488
- | 客户端 | 有没有 preload 桥 | 本插件 | 你看到什么 |
495
+ | 客户端 | 有没有 preload 桥 | 开关打开时本插件 | 你看到什么 |
489
496
  |---|---|---|---|
490
497
  | 桌面应用(`DeepSeek Harness.exe`) | 有(`window.__DSH_DIRECTORY_PICKER__`) | 在优先级 `-1` 上接管这两个目录流程槽位 | 直接弹 **Windows 文件夹框**:任意盘符点进去、可新建文件夹,**不用手输** |
491
498
  | 手机 / 远端浏览器 | 没有 | **一个槽位都不注册** | 官方那个应用内浏览对话框(路径输入、层级列表、隐藏文件开关、新建文件夹) |
492
499
 
493
- 所以两件事同时成立:桌面不再手输盘符,手机端体验**零退化**;也**不需要任何 profile 补丁**(插件不装 = 恢复官方原样)。
500
+ 所以两件事同时成立:桌面不再手输盘符,手机端体验**零退化**;也**不需要任何 profile 补丁**(插件不装或开关关着 = 官方原样)。
494
501
 
495
502
  几条边界(都是刻意的):
496
503
 
497
504
  - **判据是 preload 桥,不是"你是不是本机"**:在电脑上用普通浏览器打开 `127.0.0.1:19387`(不是桌面应用窗口)也会走应用内框 —— 客户端拿不到"我离宿主多远"这个事实,官方把「按连接自适应」列为 deferred。
505
+ - **关掉要能真的卸掉**:开关是运行时值,用户会在不重启的情况下反复拨。`createDirectoryFlowController` 负责幂等装卸(开就装、关就卸、再开重装),`test/directory-flow.mjs` 第七节 12 条盯着它,变异 `GZ`/`HA` 盯着"幂等"与"关掉要卸"。
498
506
  - **优先级必须是负数**:官方界面注册在默认优先级 `0`;同优先级会撞上「第二次注册」⇒ 安装期直接抛 `single slot "…" already has a registration`,整块客户端插件挂不上。`test/directory-flow.mjs` 钉住这条。
499
507
  - **一条上升沿只弹一次**:持有方正在采纳(`open` 仍为 true、`busy`)时的重渲染不会再弹一次;`open` 落回才重新上膛。
500
508
  - **卸载后迟到的结果丢弃**:原生对话框没有按请求中止的能力;HMR 换掉占用者后旧实例的请求仍会落地,让它去 `onPicked` 会凭空多出一个工作区。
501
509
  - **取消与失败各是一条路**:取消(`pick` resolve 成 null)→ 持有方只关掉流程;失败(抛异常)→ 界面显示 `Error.message`(不是 `Error: xxx`)。
502
510
  - **只依赖槽位服务**:本插件客户端半的 `inject` 仍然只有 `slots`,没有引入 `uiWorkspace` —— 少一个依赖就少一种"等不到依赖而整块不挂载"的死法。
503
511
 
512
+
504
513
  ## 维护:重启 DSH
505
514
 
506
515
  装了新插件、改了宿主半代码(比如本插件的「默认终端」、键位 schema)之后,DSH 需要重启才会加载新代码。设置页「输入体验」卡片**抬头右端**(GitHub 链接左边)有一枚「重启 DSH」。
@@ -544,8 +553,8 @@ Windows 上 DSH 给模型的终端工具是 **PowerShell**(工具名 `pwsh`)
544
553
  | 工具循环的消息形状 / 轮次封顶 / 异常必须降级 | 同上第 6 节(假 llm 驱动整条循环,逐条检查 `role:'tool'` 与 `toolCallId`) |
545
554
  | 工具路径:默认关、开了才派、**查完吐散文必须回落**、拿不到工作目录就不派 | `test/quick-commands.mjs` 5g 节(13 条,真路由 + 假 llm) |
546
555
  | 工具读到的内容不进台账 | 同 5g 节:断言台账里只有工具名与次数、没有读到的文件内容 |
547
- | 变异守卫真的会咬人,且**必须是指定的那条测试变红** | `npm run test:mutations`:**174 条全部咬住**(判据是 `red.some(line => line.includes(item.expect))`,不是"有红就行") |
548
- | **桌面应用里弹系统文件夹框**(0.18.0 工作区目录选择) | ✅ **有真机证据**:2026-10-09 用户在桌面应用点「选择工作区目录」,实测「成功替换为系统文件管理器」(先用 profile 补丁验过一次,之后的插件内 shadow 版走的是同一条 preload 桥);`node test/directory-flow.mjs` 37 条 + 变异 FS–FY 罩着判定与状态机 |
556
+ | 变异守卫真的会咬人,且**必须是指定的那条测试变红** | `npm run test:mutations`:**180 条全部咬住**(判据是 `red.some(line => line.includes(item.expect))`,不是"有红就行") |
557
+ | **桌面应用里弹系统文件夹框**(0.18.0 起;0.19.0 起默认关、设置页可开) | ✅ **有真机证据**:2026-10-09 用户在桌面应用点「选择工作区目录」,实测「成功替换为系统文件管理器」(先用 profile 补丁验过一次,之后的插件内 shadow 版走的是同一条 preload 桥);`node test/directory-flow.mjs` 49 条 + 变异 FS–FY(判定/状态机)与 GZ/HA(开关幂等装卸)罩着 |
549
558
  | 发布门禁三条判据 | `npm run gates` 全过;`check:tag` 实测 `v0.12.0` 的 tag 名与所指提交里的版本一致 |
550
559
  | 三平台 CI | GitHub Actions:ubuntu / windows / macos × Node 20(`npm ci → typecheck → build → test → check:pack → check:docs`) |
551
560
 
@@ -565,7 +574,7 @@ Windows 上 DSH 给模型的终端工具是 **PowerShell**(工具名 `pwsh`)
565
574
  | 会话上下文「只用于消歧义」 | 0.12.0 起就声明过:这是**结构约束**(上下文内容不能当引文),不是对模型行为的保证。 |
566
575
  | `sync-types` 的边界 | 它是**形状核对**,不是类型代码生成:宿主 API 真变了但形状没变的那种情况查不出来。 |
567
576
  | 界面语言 | 只有中文界面(上游做了 zh/en 跟随 DSH)。英文用户能读 `README.en.md`,但界面本身不跟语言。 |
568
- | **远端(手机/浏览器)回落到官方应用内框**(0.18.0) | 判据是「客户端有没有 preload 桥」,`test/directory-flow.mjs` 第六节钉住「没有桥 ⇒ 一个槽位都不 inject、`installDirectoryFlow` 返回 false」;但**没有在真手机上点过这一次**。另外「电脑上的普通浏览器」也走这一支(客户端拿不到"我离宿主多远",官方把这个能力列为 deferred)。 |
577
+ | **远端(手机/浏览器)回落到官方应用内框**(0.18.0 起) | 判据是「客户端有没有 preload 桥」,`test/directory-flow.mjs` 第六/七节钉住「没有桥 ⇒ 一个槽位都不 inject、`installDirectoryFlow` 返回 undefined;开关即使开着也不装」;但**没有在真手机上点过这一次**。另外「电脑上的普通浏览器」也走这一支(客户端拿不到"我离宿主多远",官方把这个能力列为 deferred)。 |
569
578
 
570
579
  ## 结构
571
580
 
@@ -680,9 +689,9 @@ node build.mjs # 产出 lib/index.js + lib/client.js
680
689
  # ⚠️ 宿主半会**内联** schemastery / cosmokit:
681
690
  # 优先用 DSH 检出里的 vendor 副本,检出不在时退到 node_modules
682
691
  # 里同版本的 npm 包(两者逐字节相同)——CI 上走的就是退路
683
- npm test # 29 个套件;当前 2510 passed, 0 failed(2026-10-09 实测;CI 三平台同样全绿)
692
+ npm test # 29 个套件;当前 2540 passed, 0 failed(2026-10-09 实测;CI 三平台同样全绿)
684
693
  # 走 scripts/run-tests.mjs:顺带把"多少套件/多少条"记进 test/.last-run.json
685
- npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(174 条,须单独跑)
694
+ npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(180 条,须单独跑)
686
695
  # 同样记录结果,供下面的文档门禁核对
687
696
  npm run gates # 发版门禁三条一起跑:tag 指向 / 包内容 / 文档数字
688
697
  npm run check:tag # ① tag 名里的版本 == 该 tag 所指提交里的 package.json 版本
package/lib/client.js CHANGED
@@ -1017,6 +1017,7 @@ var NOTIFY_PUSHPLUS_FIELD = "notifyPushplusToken";
1017
1017
  var NOTIFY_WECOM_FIELD = "notifyWecomWebhook";
1018
1018
  var NOTIFY_KINDS_FIELD = "notifyKinds";
1019
1019
  var NOTIFY_TEST_API_PATH = "/composer-ux/notify/test";
1020
+ var DIRECTORY_PICK_ENABLED_FIELD = "directoryPickEnabled";
1020
1021
  var KEYS_ENABLED_FIELD = "keysEnabled";
1021
1022
  var MENU_ENABLED_FIELD = "menuEnabled";
1022
1023
  var QUICK_ENABLED_FIELD = "quickEnabled";
@@ -1290,6 +1291,8 @@ var DEFAULT_SETTINGS = {
1290
1291
  notifyPushplusToken: "",
1291
1292
  notifyWecomWebhook: "",
1292
1293
  notifyKinds: { "needs-input": true, done: true, error: true },
1294
+ // 工作区目录选择自适应:默认**关**(0.19.0)。接管官方槽位这件事必须由用户自己点。
1295
+ directoryPickEnabled: false,
1293
1296
  // 总开关默认开:它只是总闸,真正"要不要用这一栏"由下面五个开关决定(默认关)。
1294
1297
  enabled: true,
1295
1298
  // 五栏默认关(老文档由 sectionEnabledOf 迁移成"碰过就开")。
@@ -1517,6 +1520,8 @@ function sanitizeSettings(value) {
1517
1520
  return {
1518
1521
  // 微信通知(0.17.0,单向):默认全关/全空。
1519
1522
  notifyEnabled: asBool(NOTIFY_ENABLED_FIELD),
1523
+ // 工作区目录选择自适应(0.19.0):默认关。
1524
+ directoryPickEnabled: asBool(DIRECTORY_PICK_ENABLED_FIELD),
1520
1525
  notifyChannel: asNotifyChannel(),
1521
1526
  notifyPushplusToken: asString(NOTIFY_PUSHPLUS_FIELD),
1522
1527
  notifyWecomWebhook: asString(NOTIFY_WECOM_FIELD),
@@ -6001,6 +6006,34 @@ function NotifyCard({ settings, setField }) {
6001
6006
  }
6002
6007
  );
6003
6008
  }
6009
+ function DirectoryPickCard({ settings, setField }) {
6010
+ const on = settings.directoryPickEnabled;
6011
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
6012
+ FoldCard,
6013
+ {
6014
+ name: "\u5DE5\u4F5C\u533A\u76EE\u5F55\u9009\u62E9",
6015
+ summary: on ? "\u5F00\u542F\u4E2D \xB7 \u684C\u9762\u5E94\u7528\u91CC\u5F39\u7CFB\u7EDF\u6587\u4EF6\u5939\u6846\uFF08\u4EFB\u610F\u76D8\u7B26\u70B9\u9009\uFF09" : "\u7528\u5B98\u65B9\u5E94\u7528\u5185\u6D4F\u89C8\u5BF9\u8BDD\u6846",
6016
+ toggle: { checked: on, onChange: (next) => {
6017
+ setField(DIRECTORY_PICK_ENABLED_FIELD, next);
6018
+ } },
6019
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { margin: "10px 0", fontSize: 12, lineHeight: 1.6, opacity: 0.85 }, children: [
6020
+ "\u6253\u5F00\u540E\uFF1A\u5728",
6021
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: "\u684C\u9762\u5E94\u7528" }),
6022
+ "\u91CC\u70B9\u300C\u9009\u62E9\u5DE5\u4F5C\u533A\u76EE\u5F55\u300D\u76F4\u63A5\u5F39 Windows \u7CFB\u7EDF\u6587\u4EF6\u5939\u6846 \u2014\u2014 \u4EFB\u610F\u76D8\u7B26\u70B9\u8FDB\u53BB\u9009\uFF0C",
6023
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: "\u4E0D\u7528\u518D\u624B\u52A8\u8F93\u5165\u76D8\u7B26\u8DEF\u5F84" }),
6024
+ "\uFF0C\u4E5F\u80FD\u5728\u91CC\u9762\u65B0\u5EFA\u6587\u4EF6\u5939\u3002",
6025
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("br", {}),
6026
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { opacity: 0.75 }, children: "\u5B98\u65B9\u90A3\u4E2A\u5E94\u7528\u5185\u5BF9\u8BDD\u6846\u5728 Windows \u4E0A\u7956\u5148\u94FE\u6B62\u4E8E\u76D8\u7B26\u6839\uFF08\u5B83\u81EA\u5DF1\u7684 README \u5199\u7740\u300C\u4E0D\u679A\u4E3E\u76D8\u7B26\u6839 \u2014\u2014 \u8DE8\u76D8\u4F9D\u8D56\u6D4F\u89C8\u5668 UI \u7684\u8DEF\u5F84\u8F93\u5165\u5165\u53E3\u300D\uFF09\uFF0C\u6240\u4EE5\u6362\u76D8\u624D\u8981\u624B\u8F93\uFF1B\u8FD9\u4E00\u9879\u5C31\u662F\u628A\u5B83\u6362\u6210\u7CFB\u7EDF\u6846\u3002" }),
6027
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("br", {}),
6028
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { style: { opacity: 0.75 }, children: [
6029
+ "\u53EA\u5728\u684C\u9762\u5E94\u7528\u91CC\u751F\u6548\uFF08\u53EA\u6709\u90A3\u513F\u6709\u7CFB\u7EDF\u5BF9\u8BDD\u6846\u7684\u63A5\u5165\u53E3\uFF09\uFF1B\u624B\u673A / \u6D4F\u89C8\u5668\u91CC\u4ECD\u662F\u5B98\u65B9\u90A3\u4E2A\u5E94\u7528\u5185\u6D4F\u89C8 \u5BF9\u8BDD\u6846\uFF08\u5E26\u8DEF\u5F84\u8F93\u5165\uFF09\uFF0C\u8FDC\u7AEF\u4F53\u9A8C\u4E0D\u53D8\u3002\u5F00\u5173",
6030
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: "\u5373\u65F6\u751F\u6548" }),
6031
+ "\uFF0C\u4E0D\u7528\u91CD\u542F\uFF1B\u5173\u6389\u5373\u56DE\u5230\u5B98\u65B9\u539F\u6837\u3002"
6032
+ ] })
6033
+ ] })
6034
+ }
6035
+ );
6036
+ }
6004
6037
  function RestartButton({ restart }) {
6005
6038
  const busy = restart.stage === "restarting" || restart.stage === "asking";
6006
6039
  const blocked = restart.blocked;
@@ -6130,6 +6163,7 @@ function SettingsSection({ useLive, useBook, useBookStatus, useWriteNotice, acti
6130
6163
  ] })
6131
6164
  ] }),
6132
6165
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NotifyCard, { settings, setField }),
6166
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DirectoryPickCard, { settings, setField }),
6133
6167
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RestartBanner, { restart }),
6134
6168
  writeNotice !== "" && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-ux-restartBanner dsh-ux-restartBannerFailed", role: "status", children: [
6135
6169
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "dsh-ux-restartBannerText", children: [
@@ -8371,14 +8405,31 @@ function createPickFlow(options) {
8371
8405
  }
8372
8406
  function installDirectoryFlow(slots, scope, component) {
8373
8407
  const bridge = nativePickBridge(scope);
8374
- if (bridge === void 0) return false;
8408
+ if (bridge === void 0) return void 0;
8375
8409
  const pick = () => bridge.pick();
8376
8410
  const injected = () => ({ pick });
8377
- slots.inject(DIRECTORY_FLOW_SLOTS[0], () => slots.inject(DIRECTORY_FLOW_SLOTS[1], function* () {
8411
+ const dispose = slots.inject(DIRECTORY_FLOW_SLOTS[0], () => slots.inject(DIRECTORY_FLOW_SLOTS[1], function* () {
8378
8412
  yield slots.register({ name: DIRECTORY_FLOW_SLOTS[0], inject: injected, priority: NATIVE_SHADOW_PRIORITY }, component);
8379
8413
  yield slots.register({ name: DIRECTORY_FLOW_SLOTS[1], inject: injected, priority: NATIVE_SHADOW_PRIORITY }, component);
8380
8414
  }));
8381
- return true;
8415
+ return typeof dispose === "function" ? dispose : void 0;
8416
+ }
8417
+ function createDirectoryFlowController(options) {
8418
+ let installed;
8419
+ const dispose = () => {
8420
+ installed?.();
8421
+ installed = void 0;
8422
+ };
8423
+ return {
8424
+ sync(enabled) {
8425
+ if (enabled) {
8426
+ if (installed === void 0) installed = installDirectoryFlow(options.slots, options.scope, options.component);
8427
+ return;
8428
+ }
8429
+ dispose();
8430
+ },
8431
+ dispose
8432
+ };
8382
8433
  }
8383
8434
 
8384
8435
  // src/client/DirectoryFlowEntry.tsx
@@ -8420,7 +8471,6 @@ var NULL_SCOPE = {
8420
8471
  };
8421
8472
  function apply(ctx) {
8422
8473
  const slots = ctx.slots;
8423
- installDirectoryFlow(slots, globalThis, DirectoryFlowEntry);
8424
8474
  const live = (0, import_dsh_client_store.createSnapshotStore)({ ...DEFAULT_SETTINGS });
8425
8475
  const menu2 = (0, import_dsh_client_store.createSnapshotStore)(null);
8426
8476
  const panel = (0, import_dsh_client_store.createSnapshotStore)(null);
@@ -8967,6 +9017,18 @@ function apply(ctx) {
8967
9017
  inject: () => ({ hooks: { live } })
8968
9018
  }, CostChipEntry));
8969
9019
  ctx.effect(() => installSettingsCardStyle(), "composer-ux: settings card style");
9020
+ ctx.effect(() => {
9021
+ const flow = createDirectoryFlowController({ slots, scope: globalThis, component: DirectoryFlowEntry });
9022
+ const syncDirectoryFlow = () => {
9023
+ flow.sync(live.getSnapshot().directoryPickEnabled === true);
9024
+ };
9025
+ syncDirectoryFlow();
9026
+ const unsubscribe = live.subscribe(syncDirectoryFlow);
9027
+ return () => {
9028
+ unsubscribe();
9029
+ flow.dispose();
9030
+ };
9031
+ }, "composer-ux: directory flow");
8970
9032
  ctx.effect(() => installQuickButtonStyle(), "composer-ux: quick button style");
8971
9033
  ctx.effect(() => installPanelResizeStyle(), "composer-ux: panel resize style");
8972
9034
  ctx.effect(() => {