dsh-panel-control 0.3.16 → 0.7.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/README.md +53 -15
- package/client.js +728 -36
- package/docs/discussion-bundle-evaluated-twice.md +100 -0
- package/docs/sidebar-layout.md +40 -0
- package/index.js +195 -135
- package/package.json +2 -2
- package/scripts/host-smoke.mjs +45 -2
- package/scripts/layout-smoke.mjs +48 -0
- package/scripts/read-page-trace.mjs +104 -0
package/scripts/host-smoke.mjs
CHANGED
|
@@ -29,6 +29,9 @@ const MODULE = pathToFileURL(override ? path.resolve(override) : path.join(here,
|
|
|
29
29
|
// The recovery nudge rewrites the real client.js when a page never reports; the
|
|
30
30
|
// harness must stay read-only about the checkout.
|
|
31
31
|
process.env.DPC_NUDGE = 'off';
|
|
32
|
+
// Rescue-by-current-revision is an explicit opt-in now: the pages below exercise
|
|
33
|
+
// it deliberately, while the default path is asserted later.
|
|
34
|
+
process.env.DPC_SERVE_CURRENT = 'on';
|
|
32
35
|
|
|
33
36
|
const timers = [];
|
|
34
37
|
const realSetTimeout = globalThis.setTimeout;
|
|
@@ -62,6 +65,10 @@ const clientModulesStub = {
|
|
|
62
65
|
{ id: 'a' },
|
|
63
66
|
{ id: 'b' },
|
|
64
67
|
{ id: 'dsh-panel-control', rev: 'rev-now', url: 'plugins/??dsh-panel-control/client.js&rev=rev-now' },
|
|
68
|
+
// The plugin that sits next to this one in the real profile: it carries no
|
|
69
|
+
// fallback of its own, so a retired revision used to drop it silently
|
|
70
|
+
// while the manager still reported it running.
|
|
71
|
+
{ id: 'dsh-workspace-control', rev: 'rev-now', url: 'plugins/??dsh-workspace-control/client.js&rev=rev-now' },
|
|
65
72
|
],
|
|
66
73
|
batches: [],
|
|
67
74
|
}),
|
|
@@ -195,6 +202,19 @@ if (route) {
|
|
|
195
202
|
'/plugins/??dsh-panel-control/client.js&rev=rev-old',
|
|
196
203
|
);
|
|
197
204
|
check('stale revision is served at the current one', stale.status === 200, `status ${stale.status}`);
|
|
205
|
+
// A combo is the batch the page booted with. Answering it with this one
|
|
206
|
+
// module's bytes registers this plugin and silently drops every other module
|
|
207
|
+
// in the batch -- the page then runs one plugin instead of the list it
|
|
208
|
+
// booted with. The stale combo has to stay a 404.
|
|
209
|
+
const staleCombo = await clientModulesStub.bundleResource(
|
|
210
|
+
'GET',
|
|
211
|
+
'/plugins/??a/client.js,b/client.js,dsh-panel-control/client.js&rev=rev-old-combo',
|
|
212
|
+
);
|
|
213
|
+
check(
|
|
214
|
+
'a stale combo is not answered with the single bundle',
|
|
215
|
+
staleCombo.status === 404,
|
|
216
|
+
`status ${staleCombo.status}`,
|
|
217
|
+
);
|
|
198
218
|
// A revision this Host already handed out must survive the registry
|
|
199
219
|
// forgetting it: the page keeps asking for the list it booted with.
|
|
200
220
|
await clientModulesStub.bundleResource('GET', '/plugins/??dsh-panel-control/client.js&rev=rev-now');
|
|
@@ -211,11 +231,23 @@ if (route) {
|
|
|
211
231
|
);
|
|
212
232
|
check('current revision passes straight through', fresh.status === 200, `status ${fresh.status}`);
|
|
213
233
|
const other = await clientModulesStub.bundleResource('GET', '/plugins/??someone-else/client.js&rev=old');
|
|
214
|
-
check('
|
|
234
|
+
check('packages outside the graph keep their 404', other.status === 404, `status ${other.status}`);
|
|
235
|
+
// The reported bug: a *neighbouring* plugin on a retired revision. Our
|
|
236
|
+
// bundle loaded, theirs 404'd, and it vanished from the page while the
|
|
237
|
+
// manager kept saying "running". One fallback for the whole list fixes it.
|
|
238
|
+
const neighbour = await clientModulesStub.bundleResource(
|
|
239
|
+
'GET',
|
|
240
|
+
'/plugins/??dsh-workspace-control/client.js&rev=rev-old',
|
|
241
|
+
);
|
|
242
|
+
check(
|
|
243
|
+
'a neighbouring plugin is served at the current revision too',
|
|
244
|
+
neighbour.status === 200,
|
|
245
|
+
`status ${neighbour.status}`
|
|
246
|
+
);
|
|
215
247
|
check(
|
|
216
248
|
'inventory ids',
|
|
217
249
|
Array.isArray(body.result.value.ids) &&
|
|
218
|
-
body.result.value.ids.join() === 'a,b,dsh-panel-control',
|
|
250
|
+
body.result.value.ids.join() === 'a,b,dsh-panel-control,dsh-workspace-control',
|
|
219
251
|
body.result.value && body.result.value.ids.join(),
|
|
220
252
|
);
|
|
221
253
|
} catch (error) {
|
|
@@ -259,6 +291,17 @@ if (route) {
|
|
|
259
291
|
} catch (error) {
|
|
260
292
|
check('custom-protocol page allowed', false, error.message);
|
|
261
293
|
}
|
|
294
|
+
// The desktop dev-proxy serves the page from a reserved .invalid origin; the
|
|
295
|
+
// page is still ours, so its calls must not be refused.
|
|
296
|
+
try {
|
|
297
|
+
const response = await route.fetch(
|
|
298
|
+
request(envelope('inventory'), { host: 'unlag.invalid', origin: 'http://unlag.invalid' }),
|
|
299
|
+
);
|
|
300
|
+
const body = await response.json();
|
|
301
|
+
check('dev-proxy page allowed', response.ok === true && body.result && body.result.ok === true, String(response.status));
|
|
302
|
+
} catch (error) {
|
|
303
|
+
check('dev-proxy page allowed', false, error.message);
|
|
304
|
+
}
|
|
262
305
|
// An oversized body is refused before it is parsed.
|
|
263
306
|
try {
|
|
264
307
|
const huge = request(envelope('inventory'));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Static gate for the sidebar layout guard (discussion #12083).
|
|
2
|
+
//
|
|
3
|
+
// The guard is CSS plus a handful of attributes the engine writes; neither is
|
|
4
|
+
// reachable from a node unit test because client.js only loads inside the page
|
|
5
|
+
// module loader. These assertions keep the pieces the runtime check proved are
|
|
6
|
+
// necessary from silently disappearing in a refactor.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
|
|
9
|
+
const read = (name) => readFileSync(new URL('../' + name, import.meta.url), 'utf8');
|
|
10
|
+
const client = read('client.js');
|
|
11
|
+
const pkg = JSON.parse(read('package.json'));
|
|
12
|
+
const problems = [];
|
|
13
|
+
|
|
14
|
+
const expect = (ok, message) => {
|
|
15
|
+
if (!ok) problems.push(message);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
for (const marker of [
|
|
19
|
+
'data-dpc-list',
|
|
20
|
+
'data-dpc-unbounded',
|
|
21
|
+
'data-dpc-region',
|
|
22
|
+
'--dpc-panellist-max',
|
|
23
|
+
'data-dpc-group-id',
|
|
24
|
+
'dpc-caret',
|
|
25
|
+
'dpc-sepCount',
|
|
26
|
+
'layoutTitle',
|
|
27
|
+
'layoutGuard',
|
|
28
|
+
'layoutMaxNone',
|
|
29
|
+
'toggleCollapse',
|
|
30
|
+
]) {
|
|
31
|
+
expect(client.includes(marker), 'client.js lost ' + marker);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
expect(client.includes("const BUILD_STAMP = 'dsh-panel-control/" + pkg.version + "'"), 'BUILD_STAMP and package.json version disagree');
|
|
35
|
+
expect(pkg.scripts && String(pkg.scripts.check).includes('layout-smoke'), 'npm run check no longer runs this gate');
|
|
36
|
+
expect(/\[data-dpc-list\]\{[^}]*overflow-y:auto/.test(client), 'the panel list is not scrollable');
|
|
37
|
+
expect(/\[data-dpc-list\]\{[^}]*max-height:max\(120px,var\(--dpc-panellist-max,45vh\)\)/.test(client), 'the panel list lost its height cap');
|
|
38
|
+
expect(/\[data-dpc-region\]\{[^}]*min-height/.test(client), 'the region below the list lost its floor');
|
|
39
|
+
|
|
40
|
+
expect(/\.dpc-layoutRow:first-of-type\{[^}]*border-top:0/.test(client), 'the first layout row has a hairline touching the card hint');
|
|
41
|
+
expect(/\.dpc-layout > \.dpc-note\{[^}]*margin:6px 0 10px/.test(client), 'the card hint lost its gap above the rows');
|
|
42
|
+
expect(/\.dpc-layoutActions\{[^}]*margin-top:12px/.test(client), 'the collapse/expand actions lost their separation');
|
|
43
|
+
if (problems.length) {
|
|
44
|
+
console.error('layout-smoke: ' + problems.length + ' problem(s)');
|
|
45
|
+
for (const problem of problems) console.error(' - ' + problem);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
console.log('layout-smoke: ok (' + pkg.version + ')');
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Read the page-side trace this plugin leaves in the renderer's localStorage.
|
|
4
|
+
*
|
|
5
|
+
* The Host never hears from the Client half when the runner does not apply it,
|
|
6
|
+
* so the trace is written by the bundle's own module scope and picked up here,
|
|
7
|
+
* offline, straight out of Chromium's Local Storage LevelDB. No browser, no
|
|
8
|
+
* DevTools, no waiting on a person to read a window title.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node scripts/read-page-trace.mjs [--out <file>]
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
const KEY = 'dsh.panel-control.trace';
|
|
17
|
+
const ROAMING = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
18
|
+
const PROFILE = path.join(ROAMING, '@deepseek-ai', 'dsh-desktop', 'Local Storage', 'leveldb');
|
|
19
|
+
const OUT = (() => {
|
|
20
|
+
const at = process.argv.indexOf('--out');
|
|
21
|
+
return at !== -1 ? process.argv[at + 1] : path.join(process.env.USERPROFILE || os.homedir(), '.dsh', 'panel-control', 'page-trace.log');
|
|
22
|
+
})();
|
|
23
|
+
|
|
24
|
+
function printable(text) {
|
|
25
|
+
return text.replace(/[^\x20-\x7e\u4e00-\u9fff]/g, ' ').replace(/ {2,}/g, ' ').trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const found = [];
|
|
29
|
+
let files = [];
|
|
30
|
+
try {
|
|
31
|
+
files = fs.readdirSync(PROFILE).filter((name) => /\.(log|ldb)$/.test(name));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('cannot read', PROFILE, error.message);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const name of files) {
|
|
38
|
+
const buf = fs.readFileSync(path.join(PROFILE, name));
|
|
39
|
+
// keys are UTF-8, values are UTF-16LE: read the key to confirm the entry, then
|
|
40
|
+
// decode the UTF-16LE value that carries the stage list.
|
|
41
|
+
const keyAt = buf.toString('latin1').indexOf(KEY);
|
|
42
|
+
const needle = Buffer.from('#ml=', 'utf16le');
|
|
43
|
+
let value = '';
|
|
44
|
+
let at = buf.indexOf(needle);
|
|
45
|
+
while (at !== -1) {
|
|
46
|
+
const slice = buf.subarray(Math.max(0, at - 800), at + 1600);
|
|
47
|
+
value += printable(slice.toString('utf16le')) + '\n';
|
|
48
|
+
at = buf.indexOf(needle, at + 1);
|
|
49
|
+
}
|
|
50
|
+
if (keyAt !== -1 || value) found.push({ name, keyAt, value: value.trim() });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The full boot manifest sits under its own key; it is the only place that says
|
|
54
|
+
// whether the page was handed the same module twice.
|
|
55
|
+
const bootNeedle = Buffer.from('"dupes":', 'utf16le');
|
|
56
|
+
const boots = [];
|
|
57
|
+
for (const name of files) {
|
|
58
|
+
const buf = fs.readFileSync(path.join(PROFILE, name));
|
|
59
|
+
let at = buf.indexOf(bootNeedle);
|
|
60
|
+
while (at !== -1) {
|
|
61
|
+
const text = printable(buf.subarray(Math.max(0, at - 120), at + 9000).toString('utf16le')).trim();
|
|
62
|
+
if (text) boots.push(text);
|
|
63
|
+
at = buf.indexOf(bootNeedle, at + 1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Small always-fresh record: "DPCPAGE|" is written on every stage and on every
|
|
68
|
+
// page error, so at least one copy stays in the live (uncompressed) log file.
|
|
69
|
+
const pageNeedle = Buffer.from('DPCPAGE|', 'utf16le');
|
|
70
|
+
const notes = [];
|
|
71
|
+
for (const name of files) {
|
|
72
|
+
const buf = fs.readFileSync(path.join(PROFILE, name));
|
|
73
|
+
let at = buf.indexOf(pageNeedle);
|
|
74
|
+
while (at !== -1) {
|
|
75
|
+
const text = printable(buf.subarray(at, at + 4000).toString('utf16le'));
|
|
76
|
+
if (text) notes.push(`${name}: ${text}`);
|
|
77
|
+
at = buf.indexOf(pageNeedle, at + 1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push(`read ${new Date().toISOString()} from ${PROFILE}`);
|
|
82
|
+
lines.push(`files: ${files.join(' ')}`);
|
|
83
|
+
if (found.length === 0) {
|
|
84
|
+
lines.push(`no "${KEY}" entry: the bundle's module scope never ran in that page`);
|
|
85
|
+
} else {
|
|
86
|
+
for (const item of found) {
|
|
87
|
+
lines.push('');
|
|
88
|
+
lines.push(`== ${item.name} (key at ${item.keyAt})`);
|
|
89
|
+
lines.push(item.value || '(key present, value unreadable)');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
|
93
|
+
// BOOT KEY: "dsh.panel-control.trace.boot"
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push('== page note');
|
|
96
|
+
if (notes.length === 0) lines.push('none');
|
|
97
|
+
else lines.push(notes.join('\n---\n'));
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push('== boot dump');
|
|
100
|
+
if (boots.length === 0) lines.push('no boot dump: the bundle never described one');
|
|
101
|
+
else lines.push(boots.join('\n---\n'));
|
|
102
|
+
fs.writeFileSync(OUT, lines.join('\n') + '\n', 'utf8');
|
|
103
|
+
console.log(lines.join('\n'));
|
|
104
|
+
console.log('\nwritten to', OUT);
|