dsh-wsl-tool 1.10.3 → 1.10.5

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/lib/client.js CHANGED
@@ -191,7 +191,12 @@ window.__ModuleLoader__.load({
191
191
  if (cursor === null || typeof cursor !== 'object') return undefined
192
192
  cursor = cursor[step]
193
193
  }
194
- return cursor
194
+ // A `.volatile()` field can arrive as a cosmokit cell rather than its value;
195
+ // the platform's own form model hands out plain values, but unwrapping here
196
+ // costs nothing and keeps a switch from rendering a cell object as "on".
197
+ return cursor !== null && typeof cursor === 'object' && typeof cursor.get === 'function'
198
+ ? cursor.get()
199
+ : cursor
195
200
  }
196
201
 
197
202
  /**
@@ -301,12 +306,17 @@ window.__ModuleLoader__.load({
301
306
  * a refusal, so this panel does the same — `false` means "not saved", and
302
307
  * the panel words it instead of pretending the switch moved.
303
308
  */
304
- write: async (op) => {
309
+ write: async (ops) => {
305
310
  const snapshot = read()
306
311
  if (scope === undefined || typeof scope.mutate !== 'function') return false
307
312
  if (snapshot.writable !== true) return false
313
+ // One op or an ordered batch: the service applies a batch in one
314
+ // revision-fenced write, which is what the panel's single "restore every
315
+ // default" control uses.
316
+ const batch = Array.isArray(ops) ? ops : [ops]
317
+ if (batch.length === 0) return true
308
318
  try {
309
- const landed = await scope.mutate([op], snapshot.revision)
319
+ const landed = await scope.mutate(batch, snapshot.revision)
310
320
  return Boolean(landed)
311
321
  } catch (error) {
312
322
  console.error('dsh-wsl-tool: writing the configuration failed', error)
@@ -355,6 +365,14 @@ window.__ModuleLoader__.load({
355
365
  const ROW_COPY_STYLE = { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 }
356
366
  const ROW_LABEL_STYLE = { display: 'flex', alignItems: 'center', gap: '6px', fontWeight: 500 }
357
367
  const ROW_ACTIONS_STYLE = { display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }
368
+ /** The panel's single override bar; `rowGap` keeps it readable above the rows. */
369
+ const OVERRIDE_BAR_STYLE = {
370
+ display: 'flex',
371
+ alignItems: 'center',
372
+ gap: '8px',
373
+ justifyContent: 'flex-end',
374
+ padding: '6px 0',
375
+ }
358
376
  const HINT_STYLE = { margin: 0, color: LABEL_TERTIARY, fontSize: '12px', lineHeight: '18px', maxWidth: '66ch' }
359
377
  const BADGE_STYLE = {
360
378
  border: '0.5px solid ' + BORDER_BADGE,
@@ -492,8 +510,9 @@ window.__ModuleLoader__.load({
492
510
  })
493
511
  }
494
512
 
495
- /** One switch row: switch, label and hint on the left, override controls and
496
- * the switch itself on the right. */
513
+ /** One switch row: copy on the left, an override badge and the switch on the
514
+ * right. Clearing overrides is NOT offered per row — the panel has one
515
+ * control for that, so eleven rows do not repeat the same button. */
497
516
  const rowElement = (row) => {
498
517
  const key = row.path.join('.')
499
518
  const overridden = hasPath(snapshot.user, row.path)
@@ -518,21 +537,6 @@ window.__ModuleLoader__.load({
518
537
  'div',
519
538
  { style: ROW_ACTIONS_STYLE },
520
539
  overridden ? React.createElement('span', { style: BADGE_STYLE }, '已覆盖') : null,
521
- overridden
522
- ? React.createElement(
523
- 'button',
524
- {
525
- type: 'button',
526
- style: RESET_STYLE,
527
- disabled,
528
- // Returned as well as performed: React ignores a handler's
529
- // return value, but awaiting it is how a test sees the write
530
- // settle.
531
- onClick: () => submit({ op: 'unset', path: row.path }, key),
532
- },
533
- '恢复默认',
534
- )
535
- : null,
536
540
  React.createElement(primitives.Switch, {
537
541
  checked,
538
542
  label: row.label,
@@ -575,9 +579,37 @@ window.__ModuleLoader__.load({
575
579
  React.createElement('p', { key: 'note-unavailable', style: NOTE_STYLE }, '此 DSH 未提供该插件的配置作用域'),
576
580
  )
577
581
  } else {
582
+ const overriddenPaths = SECTIONS
583
+ .flatMap((group) => group.rows)
584
+ .filter((row) => hasPath(snapshot.user, row.path))
585
+ .map((row) => row.path)
578
586
  const blocks = SECTIONS.map((group) =>
579
587
  section(group.id, group.title, React.createElement('div', { style: ROWS_STYLE }, group.rows.map(rowElement))),
580
588
  )
589
+ // ONE place to clear every override, instead of the same button repeated on
590
+ // every row. "Put this panel back the way the composition had it" is one
591
+ // intention, and the settings service takes it as one revision-fenced write.
592
+ if (overriddenPaths.length > 0) {
593
+ blocks.unshift(
594
+ React.createElement(
595
+ 'div',
596
+ { key: 'overrides', style: OVERRIDE_BAR_STYLE },
597
+ React.createElement('span', { style: BADGE_STYLE }, '已覆盖 ' + overriddenPaths.length + ' 项'),
598
+ React.createElement(
599
+ 'button',
600
+ {
601
+ type: 'button',
602
+ style: RESET_STYLE,
603
+ disabled: snapshot.writable !== true || busy !== undefined,
604
+ // Returned as well as performed: React ignores a handler's return
605
+ // value, but awaiting it is how a test sees the write settle.
606
+ onClick: () => submit(overriddenPaths.map((path) => ({ op: 'unset', path })), 'all-overrides'),
607
+ },
608
+ '全部恢复默认',
609
+ ),
610
+ ),
611
+ )
612
+ }
581
613
  if (refused) {
582
614
  blocks.unshift(
583
615
  React.createElement('p', { key: 'note-refused', style: NOTE_STYLE }, '保存被拒绝:配置没有被写入,请重试'),
package/lib/config.js CHANGED
@@ -92,7 +92,25 @@ function envInt(env, name, fallback, min, max) {
92
92
 
93
93
  /** A configured boolean, or the fallback when the layer did not set one. */
94
94
  function setting(value, fallback) {
95
- return typeof value === 'boolean' ? value : fallback
95
+ const raw = unbox(value)
96
+ return typeof raw === 'boolean' ? raw : fallback
97
+ }
98
+
99
+ /**
100
+ * Read one configuration value, unwrapping a volatile cell.
101
+ *
102
+ * A `.volatile()` schema field does NOT resolve to its value: schemastery wraps it
103
+ * with cosmokit's `createVolatile(default)`, which is why the shipped plugins read
104
+ * theirs as `config.timeoutMs.get()` (`dsh-bash-local`). The cell is written in
105
+ * place when the user edits the field, and that in-place write is exactly what makes
106
+ * an edit apply without remounting the plugin — so reading through `.get()` is both
107
+ * required for correctness and how live updates arrive. A plain value (an older
108
+ * host, or a hand-built settings object in a test) passes through untouched.
109
+ */
110
+ function unbox(value) {
111
+ return value !== null && typeof value === 'object' && typeof value.get === 'function'
112
+ ? value.get()
113
+ : value
96
114
  }
97
115
 
98
116
  /**
@@ -101,7 +119,8 @@ function setting(value, fallback) {
101
119
  * @returns a distribution name to pin, or null to use the system default.
102
120
  */
103
121
  function resolveDistro(env, configured) {
104
- if (typeof configured === 'string' && configured.trim() !== '') return configured.trim()
122
+ const distro = unbox(configured)
123
+ if (typeof distro === 'string' && distro.trim() !== '') return distro.trim()
105
124
  const raw = env.DSH_WSL_DISTRO
106
125
  return typeof raw === 'string' && raw.trim() !== '' ? raw.trim() : null
107
126
  }
@@ -121,7 +140,7 @@ function resolveWorkdir(env, settings) {
121
140
  // Only the ON position is authoritative: off means "whatever the layers below
122
141
  // say", so `DSH_WSL_WORKDIR=session` keeps working for a user who never opened
123
142
  // the panel, and a fresh install still starts in `~`.
124
- if (settings.startInSessionWorkspace === true) return null
143
+ if (unbox(settings.startInSessionWorkspace) === true) return null
125
144
  const raw = env.DSH_WSL_WORKDIR
126
145
  if (typeof raw !== 'string' || raw.trim() === '') return DEFAULTS.workdir
127
146
  const value = raw.trim()
@@ -148,11 +167,12 @@ export function resolveConfig(env = process.env, settings = {}) {
148
167
  const maxCommandTimeoutMs = envInt(
149
168
  env, 'DSH_WSL_MAX_TIMEOUT_MS', DEFAULTS.maxCommandTimeoutMs, TIMEOUT_MS_MIN, DEFAULTS.maxTimerDelayMs,
150
169
  )
151
- const tools = settings.tools ?? {}
170
+ const tools = unbox(settings.tools) ?? {}
152
171
  // 0 means "not configured", so the environment and the built-in default still
153
172
  // decide; a positive value is the user's own deadline.
154
- const configuredTimeoutMs = Number.isInteger(settings.timeoutMs) && settings.timeoutMs > 0
155
- ? settings.timeoutMs
173
+ const configuredTimeout = unbox(settings.timeoutMs)
174
+ const configuredTimeoutMs = Number.isInteger(configuredTimeout) && configuredTimeout > 0
175
+ ? configuredTimeout
156
176
  : undefined
157
177
  return {
158
178
  ...DEFAULTS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-wsl-tool",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
4
4
  "description": "Run Linux commands from Windows through WSL, essentially matching a native Linux DSH for command execution: background jobs, stdin, path translation, a destructive-command guard and a WSL capability report. A left-sidebar panel switches each capability on or off, each with a one-line explanation, and an opt-in patch gives the desktop app's sidebar terminal a WSL shell. WSL calls run below the DSH sandbox, and a project on a Windows drive keeps Windows filesystem semantics.",
5
5
  "type": "module",
6
6
  "main": "index.js",