@godv61/dsh-task-engine 0.22.3 → 0.22.4

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/.p0-test.mjs CHANGED
@@ -33,6 +33,7 @@ async function assertThrows(fn, fragment, msg) {
33
33
 
34
34
  function makeFs(initial = {}) {
35
35
  const files = new Map(Object.entries(initial))
36
+ const versions = new Map()
36
37
  return {
37
38
  async resolve(relPath, opts) {
38
39
  const base = opts && opts.cwd ? String(opts.cwd).replace(/\\/g, '/') : ''
@@ -42,8 +43,31 @@ function makeFs(initial = {}) {
42
43
  async readText(target) {
43
44
  return files.get(target.targetKey)
44
45
  },
45
- async writeText(target, content) {
46
- files.set(target.targetKey, content)
46
+ async lstat(path, opts) {
47
+ const base = opts && opts.cwd ? String(opts.cwd).replace(/\\/g, '/') : ''
48
+ const key = base ? `${base}/${path}` : path
49
+ if (!files.has(key)) return undefined
50
+ return { version: `v${versions.get(key) ?? 0}` }
51
+ },
52
+ async writeText(target, content, expected, signal, sandboxPolicy) {
53
+ const key = target.targetKey
54
+ if (expected !== undefined && expected !== null) {
55
+ if (expected.kind === 'createIfAbsent' && files.has(key)) {
56
+ const err = new Error('already exists')
57
+ err.code = 'FS_NOT_OBSERVED'
58
+ throw err
59
+ }
60
+ if (expected.kind === 'replaceIfVersion') {
61
+ const current = `v${versions.get(key) ?? 0}`
62
+ if (!files.has(key) || current !== expected.version) {
63
+ const err = new Error('stale version')
64
+ err.code = 'FS_STALE_VERSION'
65
+ throw err
66
+ }
67
+ }
68
+ }
69
+ files.set(key, content)
70
+ versions.set(key, (versions.get(key) ?? 0) + 1)
47
71
  },
48
72
  _files: files,
49
73
  }
@@ -152,13 +176,25 @@ await assertThrows(
152
176
  const exe = await registered(fs)
153
177
  const prop10 = await exe({ operation: 'init', phase: 'propose', content: '# new' }, EXEC)
154
178
  const hash10 = /content hash: ([0-9a-f]{64})/.exec(prop10)[1]
179
+ const existingHash10 = /existing hash: ([0-9a-f]{64})/.exec(prop10)[1]
180
+ assert(existingHash10 !== undefined, 'propose returns the existing-file hash when the file exists')
155
181
  await assertThrows(
156
182
  () => exe({ operation: 'init', phase: 'apply', content: '# new', expected_hash: hash10 }, EXEC),
183
+ 'requires existing_hash',
184
+ 'apply without existing_hash on an existing file is rejected',
185
+ )
186
+ await assertThrows(
187
+ () => exe({ operation: 'init', phase: 'apply', content: '# new', expected_hash: hash10, existing_hash: '0'.repeat(64) }, EXEC),
188
+ 'existing_hash mismatch',
189
+ 'apply with a stale existing_hash is rejected (TOCTOU)',
190
+ )
191
+ await assertThrows(
192
+ () => exe({ operation: 'init', phase: 'apply', content: '# new', expected_hash: hash10, existing_hash: existingHash10 }, EXEC),
157
193
  'protected',
158
194
  'apply without overwrite on an existing file is rejected',
159
195
  )
160
196
  await assertThrows(
161
- () => exe({ operation: 'init', phase: 'apply', content: '# new', expected_hash: hash10, overwrite: true }, EXEC),
197
+ () => exe({ operation: 'init', phase: 'apply', content: '# new', expected_hash: hash10, existing_hash: existingHash10, overwrite: true }, EXEC),
162
198
  'approval',
163
199
  'overwrite requires an approval service',
164
200
  )
@@ -667,6 +703,9 @@ function memProbe(files) {
667
703
  ctl.registerWorkspace('D:/proj/one')
668
704
  assert(ctl.checkedPath('D:/PROJ/one') === 'D:/PROJ/one', 'registered workspace passes under any case')
669
705
  await assertThrows(() => ctl.checkedPath('D:/other-project'), 'not registered', 'unregistered workspace is rejected once the registry is active')
706
+ ctl.enableStrictWorkspaces()
707
+ await assertThrows(() => ctl.checkedPath('D:/still-unregistered'), 'not registered', 'strict mode keeps rejecting unregistered workspaces')
708
+ assert(ctl.checkedPath('D:/proj/one') === 'D:/proj/one', 'strict mode still admits registered workspaces')
670
709
  }
671
710
 
672
711
  console.log(`\nP0 acceptance: ${passed} checks passed`)
package/README.md CHANGED
@@ -186,4 +186,5 @@ dsh plugin --profile <name> add .
186
186
  - **审计和发布质量(0.22.0)**:① 流程快照 hash——任务创建时固化 `config` 的 SHA-256,工具与提交钩子读任务记录时校验,被手改的快照一律拒绝继续;② 提交钩子完整性检测——新增 `dev_task verify_hook`,比对 `.git/hooks/commit-msg` 与内置门禁的 hash,被替换/篡改立即报错;③ 风险降级审批——新增 `set_risk`,`high_risk → standard` 必须人工批准并落 `risk_downgrades` 审计记录;④ 验证回执绑定项目根——receipt 记录 `root`,与任务 workspace 绑定;⑤ 内置规则指纹锁定——创建时固化内置规则内容指纹,包升级后 `status` 报 `bindings_drift` 而非静默换规则;⑥ 多项目 / 多语言 / 多任务测试矩阵补强。
187
187
  - **沙箱写入修复(0.22.1)**:`dev_task` 的文件写入此前没有携带按调用传递的沙箱策略(`writeText` 的 `sandboxPolicy` 参数),在 DSH 文件沙箱下会把 workspace 内的任务记录写入误判为越界而拒绝(`file access denied under workspace-write mode`,且会话策略变化无法影响它);现在每次写入显式携带 `{ mode, workspaceRoot: 会话目录 }`,并新增 `sandbox_permissions` 参数(`workspace-write` / `danger-full-access`)作为被拒后的一次性升级路径。
188
188
  - **质量修补(0.22.2,采纳 codex 评审五项)**:① client typecheck 修复——`project.ts` 不再依赖 `node:path`,浏览器面可完整类型检查;② sandbox 升级补审批——`sandbox_permissions` 必须与 `justification` 成对出现,`danger-full-access` 需人工一次批准,无审批服务即拒绝;③ Remote 任意路径设防——工作台 Remote 拒绝非绝对路径与系统级根目录;④ `set_risk` 升到 `high_risk` 时受流程能力门约束(`minimal`/`agile` 拒绝,不再绕过 `create` 的检查);⑤ `loadTask` 补齐旧记录缺失字段(items / verification / review / commits),损坏记录 fail-closed 而非引擎裸崩。
189
- - **可信边界加固(0.22.3,采纳 GPT 评审)**:① 验证命令统一在任务记录的项目根运行(monorepo 子目录不再跑错目录),receipt.root 与任务根强校验;② 旧任务(无 frozen 快照)禁止升 `high_risk`——迁移或重建后才可;③ `init apply` 强制 `expected_hash`,新增 `existing_hash` 防审批期间文件被换(TOCTOU);④ 任务记录新增 `revision`,每次写入 compare-and-swap,并发覆盖直接报错(`changed concurrently`);⑤ 工作台 Remote 增加 host workspace registry(`registerWorkspace` 供 harness 集成,注册后未授权路径一律拒绝);⑥ sandbox 升级审批展示 workspace;⑦ 文档明确 hook(本地反馈)/ host(工具流约束)/ CI(最终可信门禁)三层职责边界。
189
+ - **可信边界加固(0.22.3,采纳 GPT 评审)**:① 验证命令统一在任务记录的项目根运行(monorepo 子目录不再跑错目录),receipt.root 与任务根强校验;② 旧任务(无 frozen 快照)禁止升 `high_risk`——迁移或重建后才可;③ `init apply` 强制 `expected_hash`,新增 `existing_hash` 防审批期间文件被换(TOCTOU);④ 任务记录新增 `revision`,每次写入 compare-and-swap,并发覆盖直接报错(`changed concurrently`);⑤ 工作台 Remote 增加 host workspace registry(`registerWorkspace` 供 harness 集成,注册后未授权路径一律拒绝);⑥ sandbox 升级审批展示 workspace;⑦ 文档明确 hook(本地反馈)/ host(工具流约束)/ CI(最终可信门禁)三层职责边界。
190
+ - **并发与发布闭环(0.22.4,采纳 REVIEW-0.22.3)**:① `init apply` 在目标文件存在时**强制** `existing_hash`(不再可选),propose→apply 之间文件被篡改直接 fail-closed;② 任务写入升级为真正的原子 CAS——逻辑 `revision` 检查 + dsh-fs 的 `lstat` 版本号 `replaceIfVersion` write intent,并发写入报 `FS_STALE_VERSION` 而非静默覆盖;③ workspace registry 增加 `enableStrictWorkspaces()` 生产模式(空 registry 不再回退放行);④ `exports["./client"]` 补 `types` 条件(`lib/client.d.ts` 随构建生成);⑤ 新增 `npm run verify:package` 发布包黑盒验证(pack → 白名单一致性 → 真实安装 → 主入口 import → client 注册 → 包内 121 checks → CLI 语法)。REVIEW 指出的 Typert invocation context、host 可信状态签名、CI 最终门禁仍属 harness 层,集成点已在文档标注。
package/docs/manual.html CHANGED
@@ -229,7 +229,7 @@ pnpm install &amp;&amp; pnpm run build &amp;&amp; pnpm dsh web</code></pre>
229
229
  <ol>
230
230
  <li><b>inspect</b>:对 AI 说「帮我初始化这个项目」;AI 读现有 <code>AGENTS.md</code>(若有)返回,没有则扫目录结构、技术栈、构建 / 运行命令、约定与红线。同时报告:项目根(自动从 workspace 向上发现)、语言栈(Node / Java / Python / Go / Rust)与默认验证命令、其它治理文件(<code>CLAUDE.md</code>、<code>.cursorrules</code>——init 只管理 AGENTS.md,不覆盖它们)、项目级 skill/rule 目录(<code>.dsh/rules/*.md</code>、<code>.dsh/skills/&lt;name&gt;/SKILL.md</code>)。</li>
231
231
  <li><b>propose</b>:AI 写成 <code>AGENTS.md</code> 草稿,此步<b>只预览、不落盘</b>,同样硬校验<b>行数 ≤ 200 行</b>,并返回内容 hash。</li>
232
- <li><b>apply</b>:落盘。首次创建直接写入;覆盖已有 <code>AGENTS.md</code> 必须带 <code>overwrite</code> 且<b>经人工批准</b>;传回 <code>expected_hash</code> 与 propose 的内容 hash 不一致会被拒绝(防止预览与落盘之间内容被换)。写入目标必须在项目根内。</li>
232
+ <li><b>apply</b>:落盘。<b>必须传回 propose 返回的 <code>expected_hash</code></b>(缺失或与内容不一致一律拒绝,防止预览与落盘之间内容被换);覆盖已有 <code>AGENTS.md</code> 时<b>还必须传回 <code>existing_hash</code></b>(证明审批期间现有文件未被他人改动)并带 <code>overwrite</code> 且<b>经人工批准</b>。写入目标必须在项目根内。</li>
233
233
  </ol>
234
234
  <pre><code>项目根/
235
235
  ├─ AGENTS.md ← init 生成,DSH 每会话自动注入
@@ -0,0 +1,6 @@
1
+ /** dsh-task-engine browser half — declared surface for TypeScript consumers of "./client". */
2
+ import type { Context } from '@deepseek-ai/cordis'
3
+
4
+ export const inject: string[]
5
+
6
+ export function apply(ctx: Context): Promise<() => Promise<void>>
package/lib/client.js CHANGED
@@ -4469,6 +4469,7 @@ var surface = "var(--dsw-alias-bg-layer-2)";
4469
4469
  var danger = "var(--dsw-alias-state-error-primary)";
4470
4470
  var success = "var(--dsw-alias-state-success-primary)";
4471
4471
  var warn = "var(--dsw-alias-state-warn-primary)";
4472
+ var brand = "var(--dsw-alias-state-business-primary)";
4472
4473
  var styles = {
4473
4474
  wrap: { display: "flex", flexDirection: "column", gap: 16, padding: 20, maxWidth: 900 },
4474
4475
  head: { display: "flex", flexDirection: "column", gap: 4 },
@@ -4533,7 +4534,54 @@ var styles = {
4533
4534
  noticeTitle: { color: warn, fontSize: 13, fontWeight: 600, display: "flex", alignItems: "center", gap: 8 },
4534
4535
  okText: { color: success, fontSize: 13, display: "flex", alignItems: "center", gap: 6 },
4535
4536
  status: { color: textTertiary, fontSize: 13, display: "flex", alignItems: "center", gap: 6 },
4536
- actions: { display: "flex", alignItems: "center", gap: 12, marginTop: 4 }
4537
+ actions: {
4538
+ display: "flex",
4539
+ alignItems: "center",
4540
+ gap: 12,
4541
+ marginTop: 4,
4542
+ paddingTop: 14,
4543
+ borderTop: `1px solid ${border}`
4544
+ },
4545
+ // ── flow preset picker ────────────────────────────────────────────────────
4546
+ flowGrid: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(230px, 1fr))", gap: 10 },
4547
+ flowOption: {
4548
+ display: "flex",
4549
+ flexDirection: "column",
4550
+ gap: 6,
4551
+ alignItems: "stretch",
4552
+ border: `1px solid ${border}`,
4553
+ borderRadius: 10,
4554
+ padding: "10px 12px",
4555
+ background: surface,
4556
+ cursor: "pointer",
4557
+ color: text,
4558
+ fontSize: 13,
4559
+ textAlign: "left",
4560
+ fontFamily: "inherit",
4561
+ transition: "border-color 120ms ease, box-shadow 120ms ease"
4562
+ },
4563
+ flowOptionActive: {
4564
+ borderColor: brand,
4565
+ boxShadow: `0 0 0 1px ${brand}`
4566
+ },
4567
+ flowOptionTitle: {
4568
+ display: "flex",
4569
+ alignItems: "center",
4570
+ gap: 7,
4571
+ fontSize: 13,
4572
+ fontWeight: 600,
4573
+ color: text
4574
+ },
4575
+ flowOptionDesc: { color: textTertiary, fontSize: 12, lineHeight: 1.45, margin: 0 },
4576
+ // ── init preview ──────────────────────────────────────────────────────────
4577
+ mdPreview: {
4578
+ border: `1px solid ${border}`,
4579
+ borderRadius: 8,
4580
+ background: "var(--dsw-alias-bg-layer-1)",
4581
+ padding: "12px 14px",
4582
+ maxHeight: 420,
4583
+ overflowY: "auto"
4584
+ }
4537
4585
  };
4538
4586
 
4539
4587
  // src/client/TaskEngineSection.tsx
@@ -4722,8 +4770,8 @@ function deriveCapabilities(config) {
4722
4770
  if (config.commit.file_scope) caps.push("file_scope");
4723
4771
  return caps;
4724
4772
  }
4725
- function preset(id, version, label, description, config) {
4726
- return { id, version, label, description, capabilities: deriveCapabilities(config), config };
4773
+ function preset(id, version, label2, description, config) {
4774
+ return { id, version, label: label2, description, capabilities: deriveCapabilities(config), config };
4727
4775
  }
4728
4776
  var FLOW_PRESETS = {
4729
4777
  standard: preset("standard", 1, "\u6807\u51C6\u7814\u53D1", "\u9700\u6C42\u8BC4\u5BA1 \u2192 \u8BBE\u8BA1 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u4EE3\u7801\u5BA1\u6838\uFF0C\u542B\u4EA7\u7269\u95E8 + \u786E\u8BA4\u95E8", STANDARD),
@@ -4873,9 +4921,28 @@ function TaskEngineSection(props) {
4873
4921
  ] }),
4874
4922
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.field, children: [
4875
4923
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.fieldLabel, children: "\u6D41\u7A0B\u9884\u8BBE" }),
4876
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.chips, children: FLOW_OPTIONS.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Pill, { active: flow === option.id, onClick: () => {
4877
- selectFlow(option.id);
4878
- }, title: option.description, children: option.label }, option.id)) }),
4924
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.flowGrid, children: FLOW_OPTIONS.map((option) => {
4925
+ const active = flow === option.id;
4926
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
4927
+ "button",
4928
+ {
4929
+ type: "button",
4930
+ "aria-pressed": active,
4931
+ onClick: () => {
4932
+ selectFlow(option.id);
4933
+ },
4934
+ style: { ...styles.flowOption, ...active ? styles.flowOptionActive : {} },
4935
+ children: [
4936
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: styles.flowOptionTitle, children: [
4937
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.StateDot, { state: active ? "done" : "ongoing", size: 8 }),
4938
+ option.label
4939
+ ] }),
4940
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.flowOptionDesc, children: option.description })
4941
+ ]
4942
+ },
4943
+ option.id
4944
+ );
4945
+ }) }),
4879
4946
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.hint, children: FLOW_OPTIONS.find((option) => option.id === flow)?.description })
4880
4947
  ] }),
4881
4948
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionCard, { icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.IconBranchOutline16, { size: 16 }), title: "\u6D41\u7A0B\u8282\u70B9", hint: "\u8BE5\u6D41\u7A0B\u9884\u8BBE\u56FA\u5B9A\u7684\u9636\u6BB5\u987A\u5E8F\uFF1B\u6539\u53D8\u6D41\u7A0B\u8BF7\u5207\u6362\u4E0A\u9762\u7684\u9884\u8BBE\u3002", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: styles.chips, children: stages.map((stage) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Pill, { active: stage === (FLOW_PRESETS[flow]?.config.start_stage ?? ""), title: stage === FLOW_PRESETS[flow]?.config.start_stage ? "\u8D77\u59CB\u9636\u6BB5" : void 0, children: stage }, stage)) }) }),
@@ -5113,12 +5180,25 @@ function InitPanel({ workspace, remote }) {
5113
5180
  { style: styles.muted },
5114
5181
  "\u9879\u76EE\u6839 AGENTS.md \u4F1A\u88AB DSH \u81EA\u52A8\u6CE8\u5165\u5230\u6BCF\u4E2A\u4F1A\u8BDD\u3002\u521D\u59CB\u5316\u4E00\u6B21\uFF0C\u4E4B\u540E\u6BCF\u6B21\u4EFB\u52A1\u5F00\u5DE5 AI \u90FD\u81EA\u5E26\u8FD9\u4EFD\u9879\u76EE\u8BA4\u77E5\uFF08\u9879\u76EE\u662F\u4EC0\u4E48 \u2192 \u600E\u4E48\u8DD1 \u2192 \u7ED3\u6784 \u2192 \u7EA6\u5B9A \u2192 \u5751\uFF09\u3002"
5115
5182
  ),
5116
- exists && !editing && draft === null ? (0, import_react2.createElement)(import_dsh_client_ui_primitives2.MarkdownText, { text: view.content, labels: MD_LABELS }) : null,
5183
+ exists && !editing && draft === null ? (0, import_react2.createElement)(
5184
+ "div",
5185
+ { style: styles.mdPreview },
5186
+ (0, import_react2.createElement)(import_dsh_client_ui_primitives2.MarkdownText, { text: view.content, labels: MD_LABELS })
5187
+ ) : null,
5117
5188
  draft !== null ? (0, import_react2.createElement)(
5118
5189
  "div",
5119
5190
  { style: styles.section },
5120
- (0, import_react2.createElement)("span", { style: styles.sourceBadge }, `AI \u8349\u7A3F \xB7 ${draft.lines} \u884C \xB7 \u5C1A\u672A\u5199\u5165`),
5121
- (0, import_react2.createElement)(import_dsh_client_ui_primitives2.MarkdownText, { text: draft.content, labels: MD_LABELS })
5191
+ (0, import_react2.createElement)(
5192
+ "div",
5193
+ { style: styles.sourceRow },
5194
+ (0, import_react2.createElement)(import_dsh_client_ui_primitives2.StateDot, { state: "ongoing", size: 8 }),
5195
+ (0, import_react2.createElement)("span", { style: styles.sourceBadge }, `AI \u8349\u7A3F \xB7 ${draft.lines} \u884C \xB7 \u5C1A\u672A\u5199\u5165`)
5196
+ ),
5197
+ (0, import_react2.createElement)(
5198
+ "div",
5199
+ { style: styles.mdPreview },
5200
+ (0, import_react2.createElement)(import_dsh_client_ui_primitives2.MarkdownText, { text: draft.content, labels: MD_LABELS })
5201
+ )
5122
5202
  ) : null,
5123
5203
  editing ? (0, import_react2.createElement)("textarea", {
5124
5204
  style: { ...styles.textarea, minHeight: 300 },
@@ -5946,6 +6026,10 @@ var header = {
5946
6026
  padding: "12px 20px",
5947
6027
  borderBottom: "1px solid var(--dsw-alias-border-l2)"
5948
6028
  };
6029
+ var headerBrand = { display: "flex", alignItems: "center", gap: 10, minWidth: 0 };
6030
+ var headerText = { display: "flex", flexDirection: "column", gap: 1, minWidth: 0 };
6031
+ var headerTitle = { fontSize: 15, fontWeight: 600, color: "var(--dsw-alias-label-primary)" };
6032
+ var headerSub = { fontSize: 12, color: "var(--dsw-alias-label-tertiary)", margin: 0 };
5949
6033
  var tabbar = {
5950
6034
  display: "flex",
5951
6035
  alignItems: "center",
@@ -5999,12 +6083,33 @@ function Workbench({ useStore, actions, useWorkspaces, remote }) {
5999
6083
  (0, import_react7.createElement)(
6000
6084
  "div",
6001
6085
  { style: header },
6002
- (0, import_react7.createElement)("span", { style: { fontSize: 15, fontWeight: 600, color: "var(--dsw-alias-label-primary)" } }, "\u5DE5\u7A0B\u6D41\u7A0B"),
6086
+ (0, import_react7.createElement)(
6087
+ "div",
6088
+ { style: headerBrand },
6089
+ (0, import_react7.createElement)(import_dsh_client_ui_primitives7.IconSettingsOutline16, { size: 18 }),
6090
+ (0, import_react7.createElement)(
6091
+ "div",
6092
+ { style: headerText },
6093
+ (0, import_react7.createElement)("span", { style: headerTitle }, "\u5DE5\u7A0B\u6D41\u7A0B"),
6094
+ (0, import_react7.createElement)("p", { style: headerSub }, "\u5DE5\u7A0B\u5316\u4EA4\u4ED8\u5DE5\u4F5C\u53F0 \xB7 \u9700\u6C42\u8BC4\u5BA1 \u2192 \u8BBE\u8BA1 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u4EE3\u7801\u5BA1\u6838")
6095
+ )
6096
+ ),
6003
6097
  (0, import_react7.createElement)(
6004
6098
  "button",
6005
- { type: "button", title: "\u5173\u95ED", onClick: () => {
6006
- actions.close();
6007
- }, style: closeStyle },
6099
+ {
6100
+ type: "button",
6101
+ title: "\u5173\u95ED",
6102
+ onClick: () => {
6103
+ actions.close();
6104
+ },
6105
+ style: closeStyle,
6106
+ onMouseEnter: (event) => {
6107
+ event.currentTarget.style.background = "var(--dsw-alias-bg-layer-2)";
6108
+ },
6109
+ onMouseLeave: (event) => {
6110
+ event.currentTarget.style.background = "transparent";
6111
+ }
6112
+ },
6008
6113
  (0, import_react7.createElement)(import_dsh_client_ui_primitives7.IconCloseOutline16, { size: 16 })
6009
6114
  )
6010
6115
  ),
@@ -6033,6 +6138,8 @@ function Workbench({ useStore, actions, useWorkspaces, remote }) {
6033
6138
  // src/client/TriggerButton.tsx
6034
6139
  var import_react8 = require("react");
6035
6140
  var import_dsh_client_ui_primitives8 = require("@deepseek-ai/dsh-client-ui-primitives");
6141
+ var hover = "var(--dsw-alias-bg-layer-2)";
6142
+ var label = "var(--dsw-alias-label-primary)";
6036
6143
  function TriggerButton({ wide, actions }) {
6037
6144
  return (0, import_react8.createElement)(
6038
6145
  "button",
@@ -6043,24 +6150,39 @@ function TriggerButton({ wide, actions }) {
6043
6150
  onClick: () => {
6044
6151
  actions.open();
6045
6152
  },
6153
+ onMouseEnter: (event) => {
6154
+ event.currentTarget.style.background = hover;
6155
+ },
6156
+ onMouseLeave: (event) => {
6157
+ event.currentTarget.style.background = "transparent";
6158
+ },
6046
6159
  style: {
6047
6160
  display: "flex",
6048
6161
  alignItems: "center",
6049
6162
  justifyContent: wide ? "flex-start" : "center",
6050
6163
  gap: wide ? 8 : 0,
6051
- width: "100%",
6052
- height: 32,
6164
+ width: wide ? "100%" : 36,
6165
+ minWidth: wide ? 0 : 36,
6166
+ height: wide ? 42 : 36,
6167
+ flex: wide ? "1 1 auto" : "none",
6053
6168
  border: "none",
6054
6169
  background: "transparent",
6055
6170
  cursor: "pointer",
6056
- color: "var(--dsw-alias-label-secondary)",
6057
- borderRadius: 6,
6058
- padding: wide ? "0 10px" : 0,
6059
- boxSizing: "border-box"
6171
+ color: label,
6172
+ borderRadius: wide ? 12 : "50%",
6173
+ padding: wide ? "0 10px 0 8px" : 0,
6174
+ // The native settings row bleeds 2px past its seat on both sides so its
6175
+ // hover chrome spans the full row; matching keeps both icon columns on
6176
+ // the same x.
6177
+ margin: wide ? "0 -2px" : "0 auto 2px",
6178
+ fontSize: wide ? 14 : void 0,
6179
+ lineHeight: wide ? "22px" : void 0,
6180
+ boxSizing: "border-box",
6181
+ transition: "background 120ms ease"
6060
6182
  }
6061
6183
  },
6062
6184
  (0, import_react8.createElement)(import_dsh_client_ui_primitives8.IconSettingsOutline16, { size: wide ? 16 : 18 }),
6063
- wide ? (0, import_react8.createElement)("span", { style: { fontSize: 13 } }, "\u5DE5\u7A0B\u6D41\u7A0B") : null
6185
+ wide ? (0, import_react8.createElement)("span", { style: { fontSize: 14, lineHeight: "22px" } }, "\u5DE5\u7A0B\u6D41\u7A0B") : null
6064
6186
  );
6065
6187
  }
6066
6188