@mzzsfy/dsh-shell-select 0.4.4 → 0.4.6

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/README.md CHANGED
@@ -40,20 +40,17 @@ Windows 宿主 shell 链接管:接管根组合中的官方 `pwsh` 工具与执
40
40
  ```yaml
41
41
  shell-select:
42
42
  shells:
43
- - id: pwsh
44
- name: PowerShell
45
- kind: pwsh # pwsh | bash | cmd | wsl
46
- path: "" # 留空自动探测;bash 形探测序含 msys2 锚位(见下)
47
- args: [] # 留空用形态默认;自定义模板以 {command} 为命令占位
48
- login: false # bash 形 true 时 -lc 登录壳(-lc 只取 /usr/bin;/mingw64/bin 需另配 env MSYSTEM=MINGW64)
49
- distro: "" # wsl 形发行版(如 Ubuntu-22.04),留空用 wsl 默认;args 模板条目忽略
50
- env: {} # 条目级环境变量,如 { MSYSTEM: MINGW64 };wsl 形键自动经 WSLENV 透传
43
+ - id: pwsh # id 亦作显示名
44
+ kind: pwsh # pwsh | bash | cmd | wsl
45
+ path: "" # 留空自动探测;bash 形探测序含 msys2 锚位(见下)
46
+ args: [] # 留空用形态默认;自定义模板以 {command} 为命令占位
47
+ login: false # bash 形 true 时 -lc 登录壳(-lc 只取 /usr/bin;/mingw64/bin 需另配 env MSYSTEM=MINGW64)
48
+ distro: "" # wsl 形发行版(如 Ubuntu-22.04),留空用 wsl 默认;args 模板条目忽略
49
+ env: {} # 条目级环境变量,如 { MSYSTEM: MINGW64 };wsl 形键自动经 WSLENV 透传
51
50
  - id: git-bash
52
- name: Git Bash
53
51
  kind: bash
54
52
  path: ""
55
53
  - id: cmd
56
- name: CMD
57
54
  kind: cmd
58
55
  path: ""
59
56
  default: pwsh
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mzzsfy/dsh-shell-select",
3
3
  "description": "Windows shell 接管:替换默认 pwsh 工具为可配置多客户端的 shell 工具(git bash/WSL/cmd/pwsh,自定义路径,默认客户端,模型按名选择);支持命令 deny 黑名单、会话终端卡增强和直接 pwsh 接管徽章,接管失败自动回退官方 pwsh 链",
4
- "version": "0.4.4",
4
+ "version": "0.4.6",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
package/src/client.js CHANGED
@@ -27,12 +27,14 @@ window.__ModuleLoader__.load({
27
27
  }
28
28
  return fallback
29
29
  }
30
- const DOT_SVG = { done: '#2e9e5b', error: '#d4553f', warning: '#d9a13b', ongoing: '#7a7f8a' }
30
+ // 降级自绘状态点:色相经 CSS 类消费包令牌(禁 JS 侧字面色,主题切换跟随)
31
+ const DOT_CLASS = { done: 'sls-dot--ok', error: 'sls-dot--err', warning: 'sls-dot--warn', ongoing: 'sls-dot--idle' }
31
32
  // 降级自绘:模块表缺 primitives 时保持可见性(形态近似官方 StateDot/图标)
32
33
  const TOOLVIEW_ICONS = {
33
34
  StateDot: createElementOf(['StateDot'], ({ state }) => h('span', {
35
+ className: 'sls-dot ' + (DOT_CLASS[state] ?? DOT_CLASS.ongoing),
34
36
  style: {
35
- width: 8, height: 8, borderRadius: '50%', background: DOT_SVG[state] ?? DOT_SVG.ongoing,
37
+ width: 8, height: 8, borderRadius: '50%',
36
38
  display: 'inline-block', flex: 'none',
37
39
  },
38
40
  })),
@@ -159,7 +161,7 @@ window.__ModuleLoader__.load({
159
161
  })
160
162
  .catch((error) => {
161
163
  pathMapRequested = false
162
- console.warn('shell-select: pathmap fetch failed', error)
164
+ console.warn('shell-select: pathmap fetch failed: ' + (error instanceof Error ? error.message : String(error)))
163
165
  })
164
166
  }
165
167
  // LOGIC-END loadPathMap
@@ -203,43 +205,50 @@ window.__ModuleLoader__.load({
203
205
  }
204
206
 
205
207
  const CSS = [
206
- '.sls-card { display:flex; flex-direction:column; gap:10px; }',
208
+ // 色彩令牌:直接引用宿主语义别名,禁止字面 fallback(DEVELOPMENT/plugin-ui-design.md)
209
+ '.sls-card { --sls-border:var(--dsw-alias-border-l1); --sls-accent:var(--dsw-alias-brand-primary); --sls-track:var(--dsw-alias-border-l1); --sls-thumb:var(--dsw-alias-label-tertiary); --sls-bg-base:var(--dsw-alias-bg-base); --sls-mono:var(--ds-font-family-code);',
210
+ ' display:flex; flex-direction:column; gap:10px; }',
207
211
  '.sls-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }',
208
- '.sls-entry { display:flex; flex-direction:column; gap:6px; padding:10px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); border-radius:8px; }',
212
+ '.sls-entry { display:flex; flex-direction:column; gap:6px; padding:10px; border:1px solid var(--sls-border); border-radius:8px; }',
209
213
  '.sls-entry__head { display:flex; align-items:center; gap:8px; }',
210
214
  '.sls-grid { display:grid; grid-template-columns: 90px 1fr; gap:4px 8px; align-items:center; }',
211
215
  '.sls-grid__label { opacity:.75; font-size:12px; }',
212
- '.sls-input { padding:4px 8px; border-radius:6px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); background:transparent; color:inherit; min-width:0; }',
216
+ '.sls-input { padding:4px 8px; border-radius:6px; border:1px solid var(--sls-border); background:transparent; color:inherit; min-width:0; }',
213
217
  '.sls-input--wide { width:100%; box-sizing:border-box; }',
214
- '.sls-btn { padding:3px 10px; border-radius:6px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); background:transparent; color:inherit; cursor:pointer; font-size:12px; }',
218
+ '.sls-btn { padding:3px 10px; border-radius:6px; border:1px solid var(--sls-border); background:transparent; color:inherit; cursor:pointer; font-size:12px; }',
215
219
  '.sls-btn:disabled { opacity:.5; cursor:default; }',
216
- '.sls-btn--primary { border-color: transparent; background: var(--sls-accent, #4b7bcc); color:#fff; }',
217
- '.sls-badge { font-size:11px; padding:1px 8px; border-radius:999px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); opacity:.85; }',
218
- '.sls-badge--ok { color:#2e9e5b; border-color:#2e9e5b; }',
219
- '.sls-badge--bad { color:#c44; border-color:#c44; }',
220
- '.sls-badge--default { color:#fff; background:var(--sls-accent, #4b7bcc); border-color:transparent; }',
220
+ '.sls-btn--primary { border-color: transparent; background: var(--sls-accent); color:var(--sls-bg-base); }',
221
+ '.sls-badge { font-size:11px; padding:1px 8px; border-radius:999px; border:1px solid var(--sls-border); opacity:.85; }',
222
+ '.sls-badge--ok { color:var(--dsw-alias-state-success-primary); border-color:var(--dsw-alias-state-success-primary); }',
223
+ '.sls-badge--bad { color:var(--dsw-alias-state-error-primary); border-color:var(--dsw-alias-state-error-primary); }',
224
+ '.sls-badge--default { color:var(--sls-bg-base); background:var(--sls-accent); border-color:transparent; }',
221
225
  '.sls-hint { font-size:12px; opacity:.7; white-space:pre-wrap; overflow-wrap:anywhere; }',
222
- '.sls-error { font-size:12px; color:#c44; white-space:pre-wrap; }',
223
- '.sls-args { font-size:12px; font-family:var(--sls-mono, monospace); }',
224
- '.sls-rules { display:flex; flex-direction:column; gap:6px; padding:10px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); border-radius:8px; }',
226
+ '.sls-error { font-size:12px; color:var(--dsw-alias-state-error-primary); white-space:pre-wrap; }',
227
+ '.sls-args { font-size:12px; font-family:var(--sls-mono); }',
228
+ '.sls-rules { display:flex; flex-direction:column; gap:6px; padding:10px; border:1px solid var(--sls-border); border-radius:8px; }',
225
229
  '.sls-rules__head { display:flex; align-items:center; gap:8px; }',
226
230
  '.sls-rules__title { font-weight:500; }',
227
231
  '.sls-rules__note { font-size:12px; opacity:.7; }',
228
232
  '.sls-rule { display:flex; align-items:center; gap:6px; }',
229
233
  '.sls-rule__no { flex:none; width:18px; text-align:right; font-size:12px; opacity:.45; user-select:none; }',
230
- '.sls-rule__input { flex:1; min-width:0; padding:4px 8px; border-radius:6px; border:1px solid var(--sls-border, rgba(128,128,128,.35)); background:transparent; color:inherit; font-size:12px; font-family:var(--sls-mono, monospace); resize:vertical; }',
231
- '.sls-rule__input:focus-visible { outline:2px solid var(--sls-accent, #4b7bcc); outline-offset:1px; }',
232
- '.sls-rule--bad .sls-rule__input { border-color:#c44; }',
233
- '.sls-rule__err { flex:none; max-width:40%; font-size:11px; color:#c44; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }',
234
+ '.sls-rule__input { flex:1; min-width:0; padding:4px 8px; border-radius:6px; border:1px solid var(--sls-border); background:transparent; color:inherit; font-size:12px; font-family:var(--sls-mono); resize:vertical; }',
235
+ '.sls-rule__input:focus-visible { outline:2px solid var(--sls-accent); outline-offset:1px; }',
236
+ '.sls-rule--bad .sls-rule__input { border-color:var(--dsw-alias-state-error-primary); }',
237
+ '.sls-rule__err { flex:none; max-width:40%; font-size:11px; color:var(--dsw-alias-state-error-primary); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }',
238
+ // 降级状态点:色相消费状态语义令牌
239
+ '.sls-dot--ok { background:var(--dsw-alias-state-success-primary); }',
240
+ '.sls-dot--err { background:var(--dsw-alias-state-error-primary); }',
241
+ '.sls-dot--warn { background:var(--dsw-alias-state-warn-primary); }',
242
+ '.sls-dot--idle { background:var(--dsw-alias-label-tertiary); }',
234
243
  '.sls-rule__del { flex:none; padding:2px 8px; }',
235
244
  '.sls-switch { display:inline-flex; align-items:center; cursor:pointer; }',
236
245
  '.sls-switch input[type="checkbox"] { position:absolute; opacity:0; width:1px; height:1px; }',
237
- '.sls-switch__track { position:relative; width:34px; height:19px; border-radius:999px; box-sizing:border-box; background:var(--sls-track, rgba(128,128,128,.35)); border:1px solid var(--sls-border, rgba(128,128,128,.35)); transition:background .15s, border-color .15s; }',
238
- '.sls-switch__thumb { position:absolute; top:50%; left:2px; width:13px; height:13px; border-radius:50%; background:var(--sls-thumb, rgba(128,128,128,.6)); transform:translateY(-50%); transition:left .15s, background .15s; }',
239
- '.sls-switch:hover .sls-switch__track { border-color:var(--sls-accent, #4b7bcc); }',
240
- '.sls-switch input[type="checkbox"]:checked + .sls-switch__track { background:var(--sls-accent, #4b7bcc); border-color:var(--sls-accent, #4b7bcc); }',
241
- '.sls-switch input[type="checkbox"]:checked + .sls-switch__track .sls-switch__thumb { left:17px; background:var(--sls-bg-base, #fff); }',
242
- '.sls-switch input[type="checkbox"]:focus-visible + .sls-switch__track { outline:2px solid var(--sls-accent, #4b7bcc); outline-offset:1px; }',
246
+ '.sls-switch__track { position:relative; width:34px; height:19px; border-radius:999px; box-sizing:border-box; background:var(--sls-track); border:1px solid var(--sls-border); transition:background .15s, border-color .15s; }',
247
+ '.sls-switch__thumb { position:absolute; top:50%; left:2px; width:13px; height:13px; border-radius:50%; background:var(--sls-thumb); transform:translateY(-50%); transition:left .15s, background .15s; }',
248
+ '.sls-switch:hover .sls-switch__track { border-color:var(--sls-accent); }',
249
+ '.sls-switch input[type="checkbox"]:checked + .sls-switch__track { background:var(--sls-accent); border-color:var(--sls-accent); }',
250
+ '.sls-switch input[type="checkbox"]:checked + .sls-switch__track .sls-switch__thumb { left:17px; background:var(--sls-bg-base); }',
251
+ '.sls-switch input[type="checkbox"]:focus-visible + .sls-switch__track { outline:2px solid var(--sls-accent); outline-offset:1px; }',
243
252
  '.sls-switch input[type="checkbox"]:disabled + .sls-switch__track { opacity:.5; }',
244
253
  // tool.call.toolview 卡片(key 'shell'):官方 terminal 行同构 + 复制/折行增强。
245
254
  // 色彩对齐官方 ToolRow:标题 label-secondary、icon 盒 label-tertiary(实证自
@@ -249,39 +258,35 @@ window.__ModuleLoader__.load({
249
258
  '.sls-tv__row--exp { cursor:pointer; user-select:none; }',
250
259
  '.sls-tv__lead { display:flex; align-items:center; gap:4px; color:var(--dsw-alias-label-tertiary, inherit); }',
251
260
  // 官方 ToolRow 同构:行首仅 icon,无展开箭头(可展开性由指针与整行点击承载);
252
- // 错误/进行中状态点绝对定位覆盖 icon(红点掩盖 icon)
253
- '.sls-tv__leadStack { position:relative; width:16px; height:16px; display:inline-flex; align-items:center; justify-content:center; flex:none; }',
254
- '.sls-tv__iconIdle { display:inline-flex; }',
255
- '.sls-tv__stateCover { position:absolute; inset:0; margin:auto; display:inline-flex; align-items:center; justify-content:center; background:transparent; }',
256
- '.sls-tv__stateCover::before { content:""; position:absolute; inset:-2px; border-radius:50%; background:var(--dsw-alias-bg-primary, #fff); }',
257
- '.sls-tv__stateCover > span { position:relative; }',
261
+ // 错误/进行中状态点原位替换 icon(官方 icon 槽 StateDot 同型先例,无背景依赖)
258
262
  '.sls-tv__sr { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }',
259
263
  '.sls-tv__title { font-weight:400; }',
260
264
  '.sls-tv__sep { width:2px; height:2px; border-radius:1px; background:var(--dsw-alias-label-caption, currentColor); opacity:.8; flex:none; }',
261
265
  '.sls-tv__sum { color:var(--dsw-alias-label-tertiary, inherit); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }',
262
- '.sls-tv__sum--err { color:var(--dsw-alias-state-error-primary, #d4553f); }',
263
- '.sls-tv__body { margin:6px 0 4px; border:1px solid rgba(128,128,128,.28); border-radius:8px; overflow:hidden; }',
264
- '.sls-tv__head { display:flex; align-items:center; gap:8px; padding:6px 10px; border-bottom:1px solid rgba(128,128,128,.18); background:rgba(128,128,128,.05); }',
266
+ '.sls-tv__sum--err { color:var(--dsw-alias-state-error-primary); }',
267
+ '.sls-tv__body { margin:6px 0 4px; border:1px solid var(--sls-border); border-radius:8px; overflow:hidden; }',
268
+ '.sls-tv__head { display:flex; align-items:center; gap:8px; padding:6px 10px; border-bottom:1px solid var(--sls-border); background:color-mix(in srgb, var(--dsw-alias-label-tertiary) 5%, transparent); }',
265
269
  // 头部单行片段禁折行:cwd 行宽不足时收缩出省略号,徽章不收缩不折行
266
- '.sls-tv__cwd { font-family:var(--sls-mono, monospace); font-size:12px; opacity:.7; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }',
270
+ '.sls-tv__cwd { font-family:var(--sls-mono); font-size:12px; opacity:.7; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }',
267
271
  '.sls-tv__takeover { font-size:11px; padding:0 7px; border-radius:999px; border:1px solid currentColor; opacity:.75; flex:none; white-space:nowrap; }',
268
- '.sls-tv__badge { font-size:11px; padding:0 7px; border-radius:999px; border:1px solid rgba(128,128,128,.35); opacity:.85; flex:none; white-space:nowrap; }',
269
- '.sls-tv__pill { font-size:12px; color:#d4553f; flex:none; white-space:nowrap; }',
272
+ '.sls-tv__badge { font-size:11px; padding:0 7px; border-radius:999px; border:1px solid var(--sls-border); opacity:.85; flex:none; white-space:nowrap; }',
273
+ '.sls-tv__pill { font-size:12px; color:var(--dsw-alias-state-error-primary); flex:none; white-space:nowrap; }',
270
274
  '.sls-tv__pill--bg { color:var(--dsw-alias-label-tertiary, inherit); }',
271
- '.sls-tv__duration { font-family:var(--sls-mono, monospace); font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; white-space:nowrap; font-variant-numeric:tabular-nums; }',
275
+ '.sls-tv__duration { font-family:var(--sls-mono); font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; white-space:nowrap; font-variant-numeric:tabular-nums; }',
272
276
  // 运行中脉冲文字:零信息窗口(参数未到)的活体反馈
273
277
  '.sls-tv__pulse { font-size:12px; color:var(--dsw-alias-label-tertiary, inherit); flex:none; white-space:nowrap; animation:sls-pulse 1.5s ease-in-out infinite; }',
274
278
  '@keyframes sls-pulse { 0%,100% { opacity:.35; } 50% { opacity:1; } }',
275
279
  '.sls-tv__sp { flex:1; }',
276
280
  '.sls-tv__copy { display:flex; align-items:center; gap:2px; flex:none; }',
277
- '.sls-tv__copyBtn { padding:2px 8px; border:1px solid rgba(128,128,128,.35); border-radius:6px; background:transparent; color:inherit; cursor:pointer; font-size:12px; }',
281
+ '.sls-tv__copyBtn { padding:2px 8px; border:1px solid var(--sls-border); border-radius:6px; background:transparent; color:inherit; cursor:pointer; font-size:12px; }',
278
282
  '.sls-tv__copyBtn:disabled { opacity:.4; cursor:default; }',
279
- '.sls-tv__copyBtn:not(:disabled):hover { background:rgba(128,128,128,.12); }',
280
- '.sls-tv__cmd { padding:8px 10px; font-family:var(--sls-mono, monospace); font-size:12.5px; white-space:pre-wrap; word-break:break-all; display:flex; }',
283
+ '.sls-tv__copyBtn:not(:disabled):hover { background:var(--dsw-alias-interactive-bg-hover); }',
284
+ '.sls-tv__cmd { padding:8px 10px; font-family:var(--sls-mono); font-size:12.5px; white-space:pre-wrap; word-break:break-all; display:flex; }',
281
285
  '.sls-tv__cmdNo { flex:none; text-align:right; margin-right:10px; opacity:.4; user-select:none; }',
282
286
  '.sls-tv__cmdText { flex:1; min-width:0; }',
283
- '.sls-tv__out { margin:0; padding:8px 10px; border-top:1px solid rgba(128,128,128,.18); font-family:var(--sls-mono, monospace); font-size:12.5px; white-space:pre; overflow-x:auto; max-height:320px; overflow-y:auto; }',
284
- '.sls-tv__inspect { display:flex; align-items:center; gap:4px; padding:4px 10px; border:none; border-top:1px solid rgba(128,128,128,.18); background:transparent; color:inherit; opacity:.6; cursor:pointer; font-size:12px; }',
287
+ '.sls-tv__out { margin:0; padding:8px 10px; border-top:1px solid var(--sls-border); font-family:var(--sls-mono); font-size:12.5px; white-space:pre; overflow-x:auto; max-height:320px; overflow-y:auto; }',
288
+ '.sls-tv__out--inline { border:1px solid var(--sls-border); border-radius:8px; }',
289
+ '.sls-tv__inspect { display:flex; align-items:center; gap:4px; padding:4px 10px; border:none; border-top:1px solid var(--sls-border); background:transparent; color:inherit; opacity:.6; cursor:pointer; font-size:12px; }',
285
290
  '.sls-tv__inspect:hover { opacity:1; }',
286
291
  ].join('\n')
287
292
 
@@ -428,18 +433,12 @@ window.__ModuleLoader__.load({
428
433
  }
429
434
  // LOGIC-END shellCardModel
430
435
 
431
- // 官方 ToolRow leading 同构:行首仅 icon;错误/进行中状态红点叠 icon 之上
432
- // (掩盖 icon,官方状态点语义)
433
- function leadingStack({ status, icons }) {
436
+ // 行首状态节点:错误/进行中原位替换 icon(官方 icon 槽 running→StateDot 同型
437
+ // 先例;不叠白片遮罩——宿主无 bg-primary token,遮罩恒落 #fff 白圆,暗色刺眼)
438
+ function leadingNode({ status, icons }) {
434
439
  const failed = status === 'failed' || status === 'signaled' || status === 'generic-error'
435
440
  const dot = failed ? icons.StateDot({ state: 'error' }) : status === 'generic-warn' ? icons.StateDot({ state: 'warning' }) : status === 'running' ? icons.StateDot({ state: 'ongoing' }) : null
436
- if (dot !== null) {
437
- return h('span', { className: 'sls-tv__leadStack' },
438
- h('span', { className: 'sls-tv__iconIdle' }, icons.IconApi({ size: 14 })),
439
- h('span', { className: 'sls-tv__stateCover' }, dot),
440
- )
441
- }
442
- return icons.IconApi({ size: 14 })
441
+ return dot !== null ? dot : icons.IconApi({ size: 14 })
443
442
  }
444
443
 
445
444
  // 时长格式:秒 → m:ss;超一小时 → h:mm:ss
@@ -536,7 +535,7 @@ window.__ModuleLoader__.load({
536
535
  } : undefined,
537
536
  },
538
537
  h('span', { className: 'sls-tv__lead' },
539
- leadingStack({ status, icons: TOOLVIEW_ICONS }),
538
+ leadingNode({ status, icons: TOOLVIEW_ICONS }),
540
539
  ),
541
540
  h('span', { className: 'sls-tv__title' }, 'Shell'),
542
541
  summary !== '' ? h('span', { className: 'sls-tv__sep', 'aria-hidden': true }) : null,
@@ -544,7 +543,7 @@ window.__ModuleLoader__.load({
544
543
  model.intercepted ? h(TakeoverBadge, { en }) : null,
545
544
  status === 'running' ? h('span', { className: 'sls-tv__pulse' }, en ? 'running…' : '运行中…') : null,
546
545
  ),
547
- open && expandable ? h('pre', { className: 'sls-tv__out', style: { border: '1px solid rgba(128,128,128,.28)', borderRadius: 8 } }, model.command + (model.output !== null && model.output !== '' ? `\n${model.output}` : '')) : null,
546
+ open && expandable ? h('pre', { className: 'sls-tv__out sls-tv__out--inline' }, model.command + (model.output !== null && model.output !== '' ? `\n${model.output}` : '')) : null,
548
547
  inspect !== undefined ? h('button', { className: 'sls-tv__inspect', onClick: inspect }, TOOLVIEW_ICONS.IconInspect({}), en ? 'Inspect' : '检查') : null,
549
548
  )
550
549
  }
@@ -637,7 +636,7 @@ window.__ModuleLoader__.load({
637
636
  },
638
637
  },
639
638
  h('span', { className: 'sls-tv__lead' },
640
- leadingStack({ status: model.status, icons: TOOLVIEW_ICONS }),
639
+ leadingNode({ status: model.status, icons: TOOLVIEW_ICONS }),
641
640
  ),
642
641
  srStatus !== null ? h('span', { className: 'sls-tv__sr' }, srStatus) : null,
643
642
  h('span', { className: 'sls-tv__title' }, 'Shell'),
package/src/guard.js CHANGED
@@ -208,6 +208,9 @@ export async function installGuard(ctx, {
208
208
  const dead = detectDeadState(ctx.loader, { applyState: shellSelectApplyState })
209
209
  if (dead === true && mounted.size === 0 && mounting === null) await mountOfficial()
210
210
  else if (dead === false && (mounted.size > 0 || mounting !== null)) await unmountOfficial()
211
+ } catch {
212
+ // 停机竞态:fiber 已停而末次 tick 先于 effect 清理触发,loader 访问必抛;
213
+ // rejection 逃逸会被宿主 fail-loud 判 fatal load failure 杀进程,静默放行
211
214
  } finally {
212
215
  sweeping = false
213
216
  }
@@ -246,10 +249,15 @@ export async function installGuard(ctx, {
246
249
  ctx.logger.info?.(`shell-select/guard: 初始判定 dead=${initial}`)
247
250
  if (initial === true) await mountOfficial()
248
251
 
249
- const timer = setInterval(() => {
250
- void sweep()
251
- }, sweepIntervalMs)
252
- timer.unref?.()
252
+ // 定时器随 fiber 清理:裸 setInterval 会在行停用后残留 tick,停机序列中
253
+ // 访问 loader 抛错并经宿主 fail-loud 升级为进程级 fatal
254
+ ctx.effect(() => {
255
+ const timer = setInterval(() => {
256
+ void sweep()
257
+ }, sweepIntervalMs)
258
+ timer.unref?.()
259
+ return () => clearInterval(timer)
260
+ }, 'shell-select/guard sweep timer')
253
261
  return undefined
254
262
  }
255
263
 
@@ -162,6 +162,43 @@ test('后台卡头部守卫:状态点缺省渲染(bg pill 即标识),非法 Stat
162
162
  assert.doesNotMatch(source, /dot: 'none'/)
163
163
  })
164
164
 
165
+ test('状态点原位替换:失败/进行中只渲染状态点,不再叠图标与白片遮罩', () => {
166
+ // Given 失败/进行中/完成的调用块;When 折叠渲染;Then 行首仅状态点或图标,
167
+ // 无图标+状态点叠加、无遮罩容器(遮罩白底 #fff 在暗色主题呈现白圆事故)。
168
+ const nodesOf = (element) => {
169
+ const nodes = []
170
+ const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) return node.forEach(walk); nodes.push(node); node.children?.forEach(walk) }
171
+ walk(element)
172
+ return nodes
173
+ }
174
+ const failed = nodesOf(toolviewOf(loadClient(), 'shell')({
175
+ block: settledBlock(JSON.stringify({ command: 'exit 3', description: 'boom' }), 'err\n[exit code: 3]'),
176
+ cwd: 'C:\\repo',
177
+ }))
178
+ assert.ok(failed.some((node) => String(node.props?.className ?? '').includes('sls-dot--err')), '失败行应有红色状态点(sls-dot--err 令牌类,主题自适应)')
179
+ assert.ok(failed.every((node) => node.props?.style?.borderRadius !== 3), '状态点行不应再渲染降级图标矩形(叠加形态)')
180
+ assert.ok(failed.every((node) => !String(node.props?.className ?? '').includes('stateCover')), '不应存在遮罩容器')
181
+ const running = nodesOf(toolviewOf(loadClient(), 'shell')({
182
+ block: { callId: 'c9', name: 'shell', argsRaw: JSON.stringify({ command: 'sleep 1', description: 'wait' }), time: Date.now() },
183
+ cwd: 'C:\\repo',
184
+ }))
185
+ assert.ok(running.some((node) => String(node.props?.className ?? '').includes('sls-dot--idle')), '进行中行应有状态点(sls-dot--idle 令牌类)')
186
+ assert.ok(running.every((node) => node.props?.style?.borderRadius !== 3), '进行中行不应再渲染降级图标矩形(叠加形态)')
187
+ const done = nodesOf(toolviewOf(loadClient(), 'shell')({
188
+ block: settledBlock(JSON.stringify({ command: 'echo ok', description: 'fine' }), 'ok\n[exit code: 0]'),
189
+ cwd: 'C:\\repo',
190
+ }))
191
+ assert.ok(done.some((node) => node.props?.style?.borderRadius === 3), '完成行保留工具图标')
192
+ assert.ok(done.every((node) => node.props?.style?.background === undefined), '完成行无状态点')
193
+ })
194
+
195
+ test('遮罩路径清除守卫:遮罩容器类与白底 token 不再存在', () => {
196
+ // Given 修复后的 client.js;When 源码扫描;Then 遮罩结构与其背景依赖全部消失,
197
+ // 状态点以原位替换呈现,双主题无背景依赖。
198
+ assert.doesNotMatch(source, /stateCover|leadStack|iconIdle/)
199
+ assert.doesNotMatch(source, /--dsw-alias-bg-primary/)
200
+ })
201
+
165
202
  test('hooks 恒序守卫:组件内 hook 调用必须全部位于第一个提前 return 之前', () => {
166
203
  // 真实 React 的 hooks 链表按调用序对位:提前 return 之后的 hook 使不同
167
204
  // 块形态(generic/terminal)的 hook 数量分叉 → "Rendered fewer hooks
@@ -45,6 +45,9 @@ function stubCtx({ rows, importOfficial }) {
45
45
  },
46
46
  })
47
47
  },
48
+ effect(execute) {
49
+ return execute()
50
+ },
48
51
  }
49
52
  return { ctx, log, mounts, listeners }
50
53
  }
@@ -0,0 +1,110 @@
1
+ // guard 生命周期:定时器随 fiber 清理(effect),停机竞态异常不逃逸为
2
+ // unhandledRejection——宿主 fail-loud 会把这类逃逸判为 fatal load failure
3
+ // 杀掉整个进程(dsh-maintain 重启停机窗口实测事故)。
4
+
5
+ import { test } from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+ import { setTimeout as sleep } from 'node:timers/promises'
8
+ import { installGuard } from '../src/guard.js'
9
+ import { MAIN_ROW_ID } from '../src/guard-state.mjs'
10
+
11
+ const OFFICIAL_MODULE = (name) => ({ __name: name, name, apply() {} })
12
+ const makeImport = async (packageName) => OFFICIAL_MODULE(packageName)
13
+
14
+ function row({ id, disabled = false, running = false }) {
15
+ return {
16
+ options: { id },
17
+ get disabled() {
18
+ return disabled
19
+ },
20
+ fiber: running ? { uid: 'f' } : undefined,
21
+ }
22
+ }
23
+
24
+ function deadWorld() {
25
+ return [
26
+ row({ id: MAIN_ROW_ID, disabled: true }),
27
+ row({ id: 'tool-pwsh', disabled: true }),
28
+ row({ id: 'pwsh-sandbox', disabled: true }),
29
+ ]
30
+ }
31
+
32
+ function stubCtx({ rows, resolveCount = { value: 0 } }) {
33
+ const log = []
34
+ const mounts = []
35
+ const effects = []
36
+ const listeners = new Map()
37
+ const ctx = {
38
+ logger: {
39
+ warn: (msg) => log.push(['warn', msg]),
40
+ error: (msg) => log.push(['error', msg]),
41
+ info: (msg) => log.push(['info', msg]),
42
+ },
43
+ get loader() {
44
+ return {
45
+ resolve(id) {
46
+ resolveCount.value += 1
47
+ const found = rows.find((candidate) => candidate.options.id === id)
48
+ if (found) return found
49
+ throw new Error('not found')
50
+ },
51
+ }
52
+ },
53
+ on(event, handler) {
54
+ listeners.set(event, handler)
55
+ },
56
+ effect(execute, label) {
57
+ const dispose = execute()
58
+ effects.push({ label, dispose })
59
+ return dispose
60
+ },
61
+ plugin(module, config) {
62
+ mounts.push({ moduleName: module.__name, config })
63
+ return Promise.resolve({
64
+ dispose: () => {
65
+ mounts.length = 0
66
+ return Promise.resolve()
67
+ },
68
+ })
69
+ },
70
+ }
71
+ return { ctx, log, mounts, effects, resolveCount }
72
+ }
73
+
74
+ test('定时器随 fiber 清理:effect dispose 后不再扫描', async () => {
75
+ const sweepIntervalMs = 20
76
+ const { ctx, mounts, effects, resolveCount } = stubCtx({ rows: deadWorld() })
77
+ await installGuard(ctx, { importOfficial: makeImport, sweepIntervalMs })
78
+ assert.equal(mounts.length, 2)
79
+ const timerEffect = effects.find((entry) => entry.label.includes('sweep'))
80
+ assert.ok(timerEffect, 'sweep 定时器必须注册为 effect')
81
+ assert.equal(typeof timerEffect.dispose, 'function')
82
+ timerEffect.dispose()
83
+ await sleep(sweepIntervalMs * 3)
84
+ const settled = resolveCount.value
85
+ await sleep(sweepIntervalMs * 3)
86
+ assert.equal(resolveCount.value, settled, 'dispose 后不得再有 sweep 扫描')
87
+ })
88
+
89
+ test('sweep 异常不逃逸:停机竞态下 loader 抛错不产生 unhandledRejection', async () => {
90
+ const sweepIntervalMs = 20
91
+ const { ctx, mounts } = stubCtx({ rows: deadWorld() })
92
+ await installGuard(ctx, { importOfficial: makeImport, sweepIntervalMs })
93
+ assert.equal(mounts.length, 2)
94
+ // 模拟 fiber 停用后 ctx.loader 访问抛 inactive context 错误
95
+ Object.defineProperty(ctx, 'loader', {
96
+ get() {
97
+ throw new Error('cannot get required service "loader" in inactive context')
98
+ },
99
+ })
100
+ const rejections = []
101
+ const onRejection = (error) => rejections.push(error)
102
+ process.on('unhandledRejection', onRejection)
103
+ try {
104
+ await sleep(sweepIntervalMs * 4)
105
+ assert.equal(mounts.length, 2, '异常不得破坏已挂载状态')
106
+ assert.deepEqual(rejections, [])
107
+ } finally {
108
+ process.off('unhandledRejection', onRejection)
109
+ }
110
+ })