dsh-background-by-model 0.4.4 → 0.4.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.
@@ -219,7 +219,8 @@ const HOST_CONTRACTS = [
219
219
  checks: [{
220
220
  kind: "selector",
221
221
  selector: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]",
222
- optionalWhen: "settingsClosed"
222
+ optionalWhen: "settingsOpen",
223
+ visible: true
223
224
  }],
224
225
  sources: [{
225
226
  path: "dsh-client-ui-primitives/lib/index.js",
@@ -275,7 +276,7 @@ const HOST_CONTRACTS = [
275
276
  checks: [{
276
277
  kind: "attr",
277
278
  attr: "data-composer-card",
278
- optionalWhen: "chatNotMounted"
279
+ optionalWhen: "chatHasMessages"
279
280
  }],
280
281
  sources: [{
281
282
  path: "dsh-client-ui-conversation/lib/client.js",
@@ -301,7 +302,7 @@ const HOST_CONTRACTS = [
301
302
  checks: [{
302
303
  kind: "attr",
303
304
  attr: "data-cordis-panel",
304
- optionalWhen: "cordisClosed"
305
+ optionalWhen: "cordisMounted"
305
306
  }],
306
307
  sources: [{
307
308
  path: "dsh-client-ui-cordis/lib/client.js",
@@ -327,7 +328,8 @@ const HOST_CONTRACTS = [
327
328
  checks: [{
328
329
  kind: "selector",
329
330
  selector: "div[data-sidebar-right-panel]",
330
- optionalWhen: "noRightPanel"
331
+ optionalWhen: "rightPanelMounted",
332
+ visible: true
331
333
  }],
332
334
  sources: [{
333
335
  path: `${SIDEBAR_RIGHT}/lib/client.js`,
@@ -379,7 +381,8 @@ const HOST_CONTRACTS = [
379
381
  checks: [{
380
382
  kind: "selector",
381
383
  selector: "[data-dockkit-content]",
382
- optionalWhen: "noDockTab"
384
+ optionalWhen: "dockHasTab",
385
+ visible: true
383
386
  }],
384
387
  sources: [{
385
388
  path: ".",
@@ -406,7 +409,7 @@ const HOST_CONTRACTS = [
406
409
  checks: [{
407
410
  kind: "selector",
408
411
  selector: "[data-dockkit-empty]",
409
- optionalWhen: "noDockTab"
412
+ optionalWhen: "dockHasNoTab"
410
413
  }],
411
414
  sources: [{
412
415
  path: `${SIDEBAR_RIGHT}/lib/client.js`,
@@ -432,7 +435,7 @@ const HOST_CONTRACTS = [
432
435
  checks: [{
433
436
  kind: "selector",
434
437
  selector: "[data-dockkit-float]",
435
- optionalWhen: "noFloatPane"
438
+ optionalWhen: "paneFloated"
436
439
  }],
437
440
  sources: [{
438
441
  path: `${SIDEBAR_RIGHT}/lib/client.js`,
@@ -458,7 +461,8 @@ const HOST_CONTRACTS = [
458
461
  checks: [{
459
462
  kind: "selector",
460
463
  selector: "[data-chat-flow]",
461
- optionalWhen: "chatNotMounted"
464
+ optionalWhen: "chatHasMessages",
465
+ visible: true
462
466
  }],
463
467
  sources: [{
464
468
  path: "dsh-client-ui-chat/lib/client.js",
@@ -484,7 +488,7 @@ const HOST_CONTRACTS = [
484
488
  checks: [{
485
489
  kind: "selector",
486
490
  selector: "[data-conversation-scroll]",
487
- optionalWhen: "chatNotMounted"
491
+ optionalWhen: "chatHasMessages"
488
492
  }],
489
493
  sources: [{
490
494
  path: "dsh-client-ui-conversation/lib/client.js",
@@ -510,7 +514,7 @@ const HOST_CONTRACTS = [
510
514
  checks: [{
511
515
  kind: "selector",
512
516
  selector: "[data-conversation-composer-overlay]",
513
- optionalWhen: "trajectoryNotMounted"
517
+ optionalWhen: "trajectoryMounted"
514
518
  }],
515
519
  sources: [{
516
520
  path: "dsh-client-ui-trajectory/lib/client.js",
package/lib/host-scan.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as thisPluginDir, c as DSH_FLOOR, i as resolveHostRoots, l as HOST_CONTRACTS, n as listHostPackages, o as versionOf, r as meetsFloor, t as checkHostOnDisk } from "./host-scan-B-xmbcxD.js";
1
+ import { a as thisPluginDir, c as DSH_FLOOR, i as resolveHostRoots, l as HOST_CONTRACTS, n as listHostPackages, o as versionOf, r as meetsFloor, t as checkHostOnDisk } from "./host-scan-C6rdxkf8.js";
2
2
  export { DSH_FLOOR, HOST_CONTRACTS, checkHostOnDisk, listHostPackages, meetsFloor, resolveHostRoots, thisPluginDir, versionOf };
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as checkHostOnDisk } from "./host-scan-B-xmbcxD.js";
1
+ import { t as checkHostOnDisk } from "./host-scan-C6rdxkf8.js";
2
2
  import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
package/lib/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as DSH_FLOOR, d as reportToMarkdown, f as symptomOf, i as resolveHostRoots, l as HOST_CONTRACTS, o as versionOf, p as verdictOf, r as meetsFloor, s as CONTRACT_GROUPS, t as checkHostOnDisk, u as labelOf } from "./host-scan-B-xmbcxD.js";
1
+ import { c as DSH_FLOOR, d as reportToMarkdown, f as symptomOf, i as resolveHostRoots, l as HOST_CONTRACTS, o as versionOf, p as verdictOf, r as meetsFloor, s as CONTRACT_GROUPS, t as checkHostOnDisk, u as labelOf } from "./host-scan-C6rdxkf8.js";
2
2
  const UI_CSS = `
3
3
  /* The section is rendered INLINE inside the host settings dialog's content
4
4
  * column: the host provides the modal chrome (backdrop, centering, closing).
@@ -288,26 +288,43 @@ function browserEnv(ctx) {
288
288
  if (host === null) return "";
289
289
  return getComputedStyle(host).getPropertyValue(token).trim();
290
290
  },
291
+ visible: (el) => {
292
+ if (el === null || typeof getComputedStyle !== "function") return true;
293
+ try {
294
+ return getComputedStyle(el).visibility !== "hidden";
295
+ } catch {
296
+ return true;
297
+ }
298
+ },
291
299
  styleSheets: () => typeof document === "undefined" ? [] : Array.from(document.styleSheets),
292
300
  ctx
293
301
  };
294
302
  }
295
303
  const ANCHORS = {
296
- settingsClosed: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]",
297
- chatNotMounted: "[data-chat-flow],[data-conversation-scroll]",
298
- trajectoryNotMounted: "[data-conversation-composer-overlay]",
299
- noRightPanel: "div[data-sidebar-right-panel],[data-dockkit-strip],[data-dockkit-pane]",
300
- cordisClosed: "[data-cordis-panel]",
301
- noDockTab: "[data-dockkit-tab]",
302
- noFloatPane: "[data-dockkit-float],[data-sidebar-right-mode=\"float\"]"
304
+ chatHasMessages: "[data-chat-turn]",
305
+ trajectoryMounted: "[data-conversation-composer-overlay]",
306
+ rightPanelMounted: "div[data-sidebar-right-panel],[data-dockkit-strip],[data-dockkit-pane]",
307
+ cordisMounted: "[data-cordis-panel]",
308
+ dockHasTab: "[data-dockkit-tab]",
309
+ dockHasNoTab: "[data-dockkit-empty],[data-dockkit-add-tab]",
310
+ paneFloated: "[data-dockkit-float],[data-sidebar-right-mode=\"float\"]",
311
+ settingsOpen: "div[role=\"dialog\"][aria-modal=\"true\"][aria-labelledby]"
303
312
  };
304
- function mounted(env, anchorId) {
313
+ /**
314
+ * Whether an anchor's surface is on screen.
315
+ *
316
+ * `optionalWhen: X` skips the check while this is FALSE — "the feature is not
317
+ * observable right now", which is what the field name promises. Every anchor is
318
+ * therefore written as a POSITIVE predicate (`dockHasTab`, `chatHasMessages`), and
319
+ * an anchor whose name reads as an absence would silently invert its check.
320
+ */
321
+ function anchorMounted(env, anchorId) {
305
322
  const sel = ANCHORS[anchorId];
306
323
  if (sel === void 0) return true;
307
324
  try {
308
325
  return env.querySelector(sel) !== null;
309
326
  } catch {
310
- return false;
327
+ return true;
311
328
  }
312
329
  }
313
330
  /** Walk a dotted path off a root object, tolerating a missing first hop. */
@@ -373,7 +390,9 @@ function ruleMentioned(env, literal, side) {
373
390
  }
374
391
  return false;
375
392
  }
376
- /** Which sides of stylesheet the probe can actually read (a blind check must not fail). */
393
+ /**
394
+ * Which sides of stylesheet the probe can actually read (a blind check must not fail).
395
+ */
377
396
  function readability(env) {
378
397
  const out = {
379
398
  host: false,
@@ -453,14 +472,14 @@ function evaluate(env, check) {
453
472
  };
454
473
  }
455
474
  case "selector": {
456
- const present = env.querySelector(check.selector) !== null;
475
+ const el = env.querySelector(check.selector);
457
476
  const name = `querySelector(${JSON.stringify(check.selector)})`;
458
- if (present) return {
477
+ if (el !== null && (check.visible !== true || env.visible(el))) return {
459
478
  status: "pass",
460
479
  name,
461
480
  detail: "element found"
462
481
  };
463
- if (check.optionalWhen !== void 0 && !mounted(env, check.optionalWhen)) return {
482
+ if (check.optionalWhen !== void 0 && !anchorMounted(env, check.optionalWhen)) return {
464
483
  status: "skip",
465
484
  name,
466
485
  detail: "not on screen right now"
@@ -468,8 +487,8 @@ function evaluate(env, check) {
468
487
  return {
469
488
  status: "fail",
470
489
  name,
471
- detail: "no element matches",
472
- reason: `no element matches ${check.selector} while its surface is on screen`
490
+ detail: el === null ? "no element matches" : "the element is hidden",
491
+ reason: el === null ? `no element matches ${check.selector} while its surface is on screen` : `${check.selector} is in the DOM but hidden while its surface is on screen`
473
492
  };
474
493
  }
475
494
  case "attr": {
@@ -480,7 +499,7 @@ function evaluate(env, check) {
480
499
  name,
481
500
  detail: "attribute found"
482
501
  };
483
- if (check.optionalWhen !== void 0 && !mounted(env, check.optionalWhen)) return {
502
+ if (check.optionalWhen !== void 0 && !anchorMounted(env, check.optionalWhen)) return {
484
503
  status: "skip",
485
504
  name,
486
505
  detail: "not on screen right now"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-background-by-model",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Appearance plugin for DeepSeek Harness: an ordered list of model rules, each carrying its own wallpaper, theme color, layout mode, opacity and blur 鈥?the background follows the model you switch to.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * The host self-check's own tests 鈥?run with
3
3
  *
4
4
  * pnpm test
@@ -97,7 +97,11 @@ function makeDom(host) {
97
97
  part => part.split(/\s+/).every(one),
98
98
  )
99
99
  return {
100
- querySelector: sel => (matches(sel) ? { style: {} } : null),
100
+ // The element remembers which selector found it: a stand-in has no box to
101
+ // measure, and several real attributes live on ONE element (the dock's tab
102
+ // host carries both the populated marker and the parked empty seat), so
103
+ // "is this element visible" has to be answered per element, not per fixture.
104
+ querySelector: sel => (matches(sel) ? { style: {}, foundBy: sel } : null),
101
105
  // Where the host publishes its palette: `body` in the real client, and the
102
106
  // fixture models exactly that. Reading the document element instead is what
103
107
  // made seven healthy tokens report as empty.
@@ -107,6 +111,10 @@ function makeDom(host) {
107
111
  // table either way 鈥?whether a value is readable is the host's own business,
108
112
  // and that is exactly what the table stands in for.
109
113
  computedValue: (token, _on) => (host.get(`computed:${token}`) ?? ''),
114
+ // A stand-in has no layout, so visibility is answered from an explicit table
115
+ // (`hidden:<selector>`). Anything unlisted counts as on screen 鈥?the
116
+ // conservative direction: an unanswerable question never fails a check.
117
+ visible: el => el === null || !host.has(`hidden:${el.foundBy}`),
110
118
  styleSheets: () => [],
111
119
  }
112
120
  }
@@ -119,7 +127,11 @@ const CORE_PRESENT = [
119
127
  '[data-composer-card]',
120
128
  '[data-cordis-panel]',
121
129
  '[data-chat-flow]',
130
+ '[data-chat-turn]',
122
131
  '[data-conversation-scroll]',
132
+ // The session root, so "no chat view" and "a chat view with nothing in it" can
133
+ // be told apart in the fixture the way they are in the host.
134
+ '[data-conversation-session]',
123
135
  '[data-conversation-composer-overlay]',
124
136
  '.dab-root',
125
137
  '[data-dockkit-strip]',
@@ -193,35 +205,47 @@ function hostStyles(literals = HOST_RULES, ownLiterals = OWN_RULES) {
193
205
  /**
194
206
  * A fixture host: `selectors` are the ones that match, `rules` the literals the
195
207
  * host stylesheet declares, and every token in `PRESENT_TOKENS` resolves unless
196
- * the caller drops it.
208
+ * the caller drops it. `hidden` lists selectors that are present but parked off
209
+ * screen (`visibility:hidden`) 鈥?the state the dock's empty seat sits in.
197
210
  */
198
- function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = PRESENT_TOKENS }) {
211
+ function hostEnv({ ctx, selectors = CORE_PRESENT, rules = HOST_RULES, tokens = PRESENT_TOKENS, hidden = [] }) {
199
212
  // A Map, not a Set: the fixture must answer with the token's VALUE, and only a
200
213
  // Map carries one (a Set has no `get`).
201
214
  const host = new Map()
202
215
  for (const s of selectors) host.set(s, true)
216
+ for (const s of hidden) host.set(`hidden:${s}`, true)
203
217
  for (const t of tokens) host.set(`computed:${t}`, '#123456')
204
218
  return { ...makeDom(host), styleSheets: () => hostStyles(rules), ctx }
205
219
  }
206
220
 
207
221
  /**
208
- * The two states the dock alternates between. In the real DOM they live in
209
- * different panes (a populated tab host carries `data-dockkit-content`, an empty
210
- * pane carries `data-dockkit-empty`), and each contract's anchor is what says
211
- * whether its own attribute can be observed 鈥?so the fixture provides the anchor
222
+ * The two states the dock alternates between. In the real DOM the tab host and
223
+ * the empty seat are different elements, and each contract's anchor is what says
224
+ * whether its own attribute can be observed — so the fixture provides the anchor
212
225
  * for every contract it expects to answer, and omits the anchors of the ones it
213
226
  * expects to read as n/a.
227
+ *
228
+ * The panel selector is listed twice on purpose: the fixture matches selectors by
229
+ * exact text (it has no tree to walk), and the contracts spell it both as
230
+ * `[data-sidebar-right-panel]` (the anchor) and `div[data-sidebar-right-panel]`
231
+ * (the check).
214
232
  */
215
233
  const DOCK_POPULATED = [
234
+ '[data-sidebar-right-panel]',
216
235
  'div[data-sidebar-right-panel]',
217
236
  '[data-dockkit-tab]',
218
237
  '[data-dockkit-content]',
219
238
  '[data-dockkit-empty]',
220
239
  ]
221
- const DOCK_EMPTY = ['div[data-sidebar-right-panel]', '[data-dockkit-empty]']
240
+ const DOCK_EMPTY = [
241
+ '[data-sidebar-right-panel]',
242
+ 'div[data-sidebar-right-panel]',
243
+ '[data-dockkit-empty]',
244
+ ]
222
245
 
223
246
  /** The healthy fixtures: nothing missing, every service published. */
224
- const healthyEnv = (dock = DOCK_POPULATED) => hostEnv({
247
+ const healthyEnv = (dock = DOCK_POPULATED, extra = {}) => hostEnv({
248
+ ...extra,
225
249
  ctx: healthyCtx(),
226
250
  selectors: [...CORE_PRESENT, ...dock],
227
251
  })
@@ -343,12 +367,110 @@ test('a closed dock holding only its empty seat is n/a, not a failure', () => {
343
367
  const results = probeContracts(env, 'en')
344
368
  const byId = id => results.find(r => r.id === id)
345
369
  assert.equal(byId('dockkit.content').status, 'skip')
346
- // The empty seat IS on screen here, so its own contract answers.
370
+ // The empty seat IS the thing on screen here, so its own contract answers — it
371
+ // is present (the host renders it visibly in this state; it is only parked and
372
+ // hidden once a tab opens).
347
373
  assert.equal(byId('dockkit.empty').status, 'pass')
348
374
  assert.equal(byId('dockkit.float').status, 'skip')
349
375
  assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
350
376
  })
351
377
 
378
+ test('a populated dock reports its empty seat as n/a, not as missing', () => {
379
+ // The state the panel showed the user a red line for. With a tab open the host
380
+ // renders no seat on that screen at all — the seat is not the thing being shown —
381
+ // so the question has no answer there, which is n/a and must never be a failure.
382
+ const env = hostEnv({
383
+ ctx: healthyCtx(),
384
+ selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-tab]', '[data-dockkit-content]'],
385
+ })
386
+ const results = probeContracts(env, 'en')
387
+ const byId = id => results.find(r => r.id === id)
388
+ assert.equal(byId('dockkit.empty').status, 'skip')
389
+ assert.equal(byId('dockkit.content').status, 'pass')
390
+ assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
391
+ })
392
+
393
+ test('a parked empty seat is still a pass, not a failure', () => {
394
+ // Presence is the whole question. The host parks the seat with
395
+ // `visibility:hidden` as tabs come and go, so asking whether it is VISIBLE is
396
+ // asking about a state the host uses both ways — which is how this contract ended
397
+ // up reporting a red line on a healthy dock.
398
+ const env = hostEnv({
399
+ ctx: healthyCtx(),
400
+ selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-add-tab]', '[data-dockkit-empty]'],
401
+ hidden: ['[data-dockkit-empty]'],
402
+ })
403
+ const results = probeContracts(env, 'en')
404
+ assert.equal(results.find(r => r.id === 'dockkit.empty').status, 'pass')
405
+ })
406
+
407
+ test('a seat that is gone while the dock is empty fails', () => {
408
+ // The real rename, and the one way this contract can fail: the dock is on screen
409
+ // with the host's "add a tab" button — so it holds no tab — and the seat is not
410
+ // there. (The old form tested the opposite state, a populated dock, instead.)
411
+ const env = hostEnv({
412
+ ctx: healthyCtx(),
413
+ selectors: [...CORE_PRESENT, 'div[data-sidebar-right-panel]', '[data-dockkit-add-tab]'],
414
+ })
415
+ const results = probeContracts(env, 'en')
416
+ const row = results.find(r => r.id === 'dockkit.empty')
417
+ assert.equal(row.status, 'fail')
418
+ assert.match(row.reason, /no element matches/)
419
+ })
420
+
421
+ test('a surface that is mounted and empty is n/a, not a renamed marker', () => {
422
+ // The hero screen: a session is open, the composer is there, and the host has
423
+ // deliberately rendered no message column because there are no messages. The
424
+ // previous anchor ("the conversation scrollport exists") is true here, so
425
+ // `[data-chat-flow]` was reported as renamed on a healthy, brand-new session.
426
+ const env = hostEnv({
427
+ ctx: healthyCtx(),
428
+ selectors: ['[data-conversation-session]', '[data-conversation-scroll]', '[data-composer-card]',
429
+ '[data-content-phase]', '[data-shell-overlay]', '.dab-root', ...DOCK_EMPTY],
430
+ })
431
+ const results = probeContracts(env, 'en')
432
+ const byId = id => results.find(r => r.id === id)
433
+ assert.equal(byId('chat.flow').status, 'skip')
434
+ // The two markers that ARE expected in that phase still have to answer, or this
435
+ // guard would hide a real rename behind "the chat is not mounted".
436
+ assert.equal(byId('composer.card').status, 'pass')
437
+ assert.equal(byId('conversation.scroll').status, 'pass')
438
+ assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
439
+ })
440
+
441
+ test('the trajectory tab is n/a for the chat column, not a failure', () => {
442
+ // Measured on the running host: the trajectory view unmounts the chat column
443
+ // (`data-trajectory-scroll` in, `data-chat-flow` out) while the session root
444
+ // changes phase rather than disappearing. Switching to it used to report
445
+ // "no element matches [data-chat-flow] while its surface is on screen".
446
+ const env = hostEnv({
447
+ ctx: healthyCtx(),
448
+ selectors: ['[data-conversation-session]', '[data-content-phase]', '[data-conversation-scroll]',
449
+ '[data-composer-card]', '[data-conversation-composer-overlay]', '[data-shell-overlay]', '.dab-root',
450
+ ...DOCK_POPULATED],
451
+ })
452
+ const results = probeContracts(env, 'en')
453
+ const byId = id => results.find(r => r.id === id)
454
+ assert.equal(byId('chat.flow').status, 'skip')
455
+ assert.equal(byId('trajectory.root').status, 'pass')
456
+ assert.deepEqual(results.filter(r => r.status === 'fail').map(r => r.id), [])
457
+ })
458
+
459
+ test('a message column that is present but hidden is a failure', () => {
460
+ // The other half of the fix: `visible` must not soften the check into "it is
461
+ // somewhere in the DOM". A column the user cannot see is not a column the card
462
+ // can be drawn on, and the report has to say which of the two happened.
463
+ const env = hostEnv({
464
+ ctx: healthyCtx(),
465
+ selectors: [...CORE_PRESENT, ...DOCK_POPULATED],
466
+ hidden: ['[data-chat-flow]'],
467
+ })
468
+ const results = probeContracts(env, 'en')
469
+ const row = results.find(r => r.id === 'chat.flow')
470
+ assert.equal(row.status, 'fail')
471
+ assert.match(row.reason, /hidden/)
472
+ })
473
+
352
474
  test('a renamed dock tab attribute still fails once a tab is open', () => {
353
475
  // The guard must not swallow a real rename: with a tab open the host has to
354
476
  // carry `data-dockkit-content` on the tab host, and if the name moved, say so.