dsh-sidebar-browser-agent 0.2.6 → 0.2.8
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/CHANGELOG.md +17 -0
- package/icon.svg +16 -7
- package/lib/client.js +164 -2
- package/lib/{entry-0.2.6.js → entry-0.2.8.js} +1 -1
- package/lib/index.js +1 -1
- package/package.json +3 -2
- package/test/client-harness.mjs +14 -0
- package/test/host-harness.mjs +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.8
|
|
4
|
+
|
|
5
|
+
- The plugin page now follows the official plugin-page style: the configuration
|
|
6
|
+
form is built on the platform's shared settings primitives (`SettingsForm`,
|
|
7
|
+
`Switch`), with the official footer save action, token-based typography and
|
|
8
|
+
spacing, and a switch list plus allowlist field. A carrier without those
|
|
9
|
+
primitives keeps the previous self-contained form as a fallback, and
|
|
10
|
+
`sidebar_browser status` reports which path is active.
|
|
11
|
+
|
|
12
|
+
## 0.2.7
|
|
13
|
+
|
|
14
|
+
- Redrew the plugin icon in the official plugin style: a transparent 36x36
|
|
15
|
+
gradient glyph (browser window, sidebar list, agent cursor) using the same
|
|
16
|
+
palette and stroke language as the built-in plugin icons.
|
|
17
|
+
- Declared `icon` in `package.json`, the field the Plugin Manager reads for a
|
|
18
|
+
plugin row, and added a GitHub-hosted 180px raster for storefront tiles.
|
|
19
|
+
|
|
3
20
|
## 0.2.6
|
|
4
21
|
|
|
5
22
|
- Publishing is now token-free: the publish workflow authenticates to npm with
|
package/icon.svg
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
<svg
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="sidebar-browser-page" x1="8" y1="9" x2="29" y2="27" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#67C7FE" />
|
|
5
|
+
<stop offset="1" stop-color="#3F77D8" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="sidebar-browser-side" x1="10" y1="11" x2="15" y2="25" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#6AECB6" />
|
|
9
|
+
<stop offset="1" stop-color="#37A6B1" />
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect x="7.7" y="8.7" width="20.6" height="18.6" rx="3.4" stroke="url(#sidebar-browser-page)" stroke-width="2.4" />
|
|
13
|
+
<rect x="10.4" y="12.2" width="3.6" height="1.8" rx="0.9" fill="url(#sidebar-browser-side)" />
|
|
14
|
+
<rect x="10.4" y="15.7" width="2.8" height="1.8" rx="0.9" fill="url(#sidebar-browser-side)" fill-opacity="0.65" />
|
|
15
|
+
<rect x="10.4" y="19.2" width="3.2" height="1.8" rx="0.9" fill="url(#sidebar-browser-side)" fill-opacity="0.45" />
|
|
16
|
+
<path d="M17.4 15.4L17.4 24.9L19.9 22.5L21.8 26.1L24.1 25.1L22.2 21.5L25.4 21.4Z" fill="url(#sidebar-browser-page)" stroke="#FFFFFF" stroke-width="0.7" stroke-linejoin="round" />
|
|
8
17
|
</svg>
|
package/lib/client.js
CHANGED
|
@@ -23,12 +23,31 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
id: 'dsh-sidebar-browser-agent',
|
|
24
24
|
factory(require) {
|
|
25
25
|
const React = require('react')
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Official settings UI primitives are platform seed modules of the browser
|
|
29
|
+
* bundle, so a dynamic plugin can require them without a build step. The
|
|
30
|
+
* loader is defensive: a carrier without them keeps the plugin alive and
|
|
31
|
+
* the configuration page falls back to the plugin-defined form.
|
|
32
|
+
*/
|
|
33
|
+
let officialPrimitives = null
|
|
34
|
+
try {
|
|
35
|
+
const candidate = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
36
|
+
if (candidate !== null && typeof candidate === 'object'
|
|
37
|
+
&& typeof candidate.SettingsForm === 'function'
|
|
38
|
+
&& typeof candidate.Switch === 'function') {
|
|
39
|
+
officialPrimitives = candidate
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
void error
|
|
43
|
+
officialPrimitives = null
|
|
44
|
+
}
|
|
26
45
|
const PLUGIN_ROOT = 'plugin/sidebar-browser-agent'
|
|
27
46
|
const MAX_BACKOFF_MS = 5_000
|
|
28
47
|
const BASE_BACKOFF_MS = 400
|
|
29
48
|
const MAX_EVAL_CHARS = 100_000
|
|
30
49
|
/** Plugin version reported by diagnostics; keep in sync with package.json. */
|
|
31
|
-
const PLUGIN_VERSION = '0.2.
|
|
50
|
+
const PLUGIN_VERSION = '0.2.8'
|
|
32
51
|
const MAX_SCREENSHOT_WIDTH = 1_400
|
|
33
52
|
const MAX_SCREENSHOT_BASE64 = 12_000_000
|
|
34
53
|
/**
|
|
@@ -1051,6 +1070,7 @@ window.__ModuleLoader__.load({
|
|
|
1051
1070
|
webviews: document.querySelectorAll('webview').length,
|
|
1052
1071
|
tabWrappers: document.querySelectorAll('[data-sidebar-right-tab]').length,
|
|
1053
1072
|
}
|
|
1073
|
+
checks.settingsForm = officialPrimitives === null ? 'fallback' : 'official'
|
|
1054
1074
|
try {
|
|
1055
1075
|
const configForms = ctx.get?.('configForms')
|
|
1056
1076
|
const form = configForms?.get?.(CONFIG_ENTRY_ID)
|
|
@@ -1206,6 +1226,42 @@ window.__ModuleLoader__.load({
|
|
|
1206
1226
|
cursor: 'pointer',
|
|
1207
1227
|
font: 'inherit',
|
|
1208
1228
|
},
|
|
1229
|
+
// --- official-style settings page (shared SettingsForm + Switch) ---
|
|
1230
|
+
switchList: { display: 'flex', flexDirection: 'column' },
|
|
1231
|
+
switchRow: {
|
|
1232
|
+
display: 'flex',
|
|
1233
|
+
alignItems: 'center',
|
|
1234
|
+
gap: '16px',
|
|
1235
|
+
minHeight: '44px',
|
|
1236
|
+
padding: '8px 0',
|
|
1237
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2)',
|
|
1238
|
+
},
|
|
1239
|
+
switchRowLast: {
|
|
1240
|
+
display: 'flex',
|
|
1241
|
+
alignItems: 'center',
|
|
1242
|
+
gap: '16px',
|
|
1243
|
+
minHeight: '44px',
|
|
1244
|
+
padding: '8px 0',
|
|
1245
|
+
},
|
|
1246
|
+
switchText: { display: 'flex', flexDirection: 'column', gap: '2px', flex: '1 1 auto', minWidth: 0 },
|
|
1247
|
+
switchLabel: { color: 'var(--dsw-alias-label-primary)', fontSize: '13px', lineHeight: '20px', fontWeight: 500 },
|
|
1248
|
+
switchHint: { color: 'var(--dsw-alias-label-secondary)', fontSize: '13px', lineHeight: '18px' },
|
|
1249
|
+
fieldLabel: { color: 'var(--dsw-alias-label-primary)', fontSize: '13px', lineHeight: '20px' },
|
|
1250
|
+
textareaOfficial: {
|
|
1251
|
+
boxSizing: 'border-box',
|
|
1252
|
+
width: '100%',
|
|
1253
|
+
minHeight: '96px',
|
|
1254
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1255
|
+
background: 'var(--dsw-alias-bg-base)',
|
|
1256
|
+
border: '0.5px solid var(--dsw-alias-border-l4)',
|
|
1257
|
+
borderRadius: 'var(--dsw-radius-md, 8px)',
|
|
1258
|
+
font: 'inherit',
|
|
1259
|
+
fontSize: '13px',
|
|
1260
|
+
lineHeight: '20px',
|
|
1261
|
+
padding: '8px 10px',
|
|
1262
|
+
resize: 'vertical',
|
|
1263
|
+
},
|
|
1264
|
+
noticeOfficial: { color: 'var(--dsw-alias-label-secondary)', margin: 0, fontSize: '13px', lineHeight: '20px' },
|
|
1209
1265
|
}
|
|
1210
1266
|
|
|
1211
1267
|
/**
|
|
@@ -1353,6 +1409,111 @@ window.__ModuleLoader__.load({
|
|
|
1353
1409
|
)
|
|
1354
1410
|
}
|
|
1355
1411
|
|
|
1412
|
+
/**
|
|
1413
|
+
* Project the staged row form onto the shared Plugins settings-form model.
|
|
1414
|
+
* @param formState - `form.state` handed to `plugins.row.config`.
|
|
1415
|
+
* @param dirty - whether the local draft differs from Host values.
|
|
1416
|
+
* @param busy - whether a save is in flight.
|
|
1417
|
+
* @param failed - whether the last save was rejected.
|
|
1418
|
+
* @returns the `state` prop the shared `SettingsForm` renders.
|
|
1419
|
+
*/
|
|
1420
|
+
function settingsFormState(formState, dirty, busy, failed) {
|
|
1421
|
+
const ready = formState !== null && typeof formState === 'object'
|
|
1422
|
+
&& formState.status === 'ready' && formState.value !== undefined
|
|
1423
|
+
return {
|
|
1424
|
+
available: ready,
|
|
1425
|
+
writable: ready && formState.writable !== false,
|
|
1426
|
+
dirty: ready && dirty === true,
|
|
1427
|
+
invalid: false,
|
|
1428
|
+
saving: busy === true,
|
|
1429
|
+
failed: ready && failed === true,
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Official-style configuration page: the shared SettingsForm footer plus a
|
|
1435
|
+
* switch list and the allowlist field, matching the built-in plugin pages.
|
|
1436
|
+
* @param props - `{ form, t }` from the Plugins page.
|
|
1437
|
+
*/
|
|
1438
|
+
function OfficialConfigEditor(props) {
|
|
1439
|
+
const t = props.t
|
|
1440
|
+
const SettingsForm = officialPrimitives.SettingsForm
|
|
1441
|
+
const Switch = officialPrimitives.Switch
|
|
1442
|
+
const formState = props.form.state
|
|
1443
|
+
const accepted = formState.value ?? {}
|
|
1444
|
+
const [draft, setDraft] = React.useState(() => draftOf(accepted))
|
|
1445
|
+
const [busy, setBusy] = React.useState(false)
|
|
1446
|
+
const [failed, setFailed] = React.useState(false)
|
|
1447
|
+
React.useEffect(() => {
|
|
1448
|
+
setDraft(draftOf(accepted))
|
|
1449
|
+
setFailed(false)
|
|
1450
|
+
}, [formState.revision, formState.status])
|
|
1451
|
+
const ready = formState !== null && typeof formState === 'object' && formState.status === 'ready' && formState.value !== undefined
|
|
1452
|
+
const writable = ready && formState.writable !== false
|
|
1453
|
+
const ops = configOps(draft, accepted)
|
|
1454
|
+
const model = settingsFormState(formState, ops.length > 0, busy, failed)
|
|
1455
|
+
const labels = {
|
|
1456
|
+
unavailable: formState?.status === 'loading' ? t('config.loading') : t('config.unavailable'),
|
|
1457
|
+
readOnly: t('config.readOnly'),
|
|
1458
|
+
save: t('config.save'),
|
|
1459
|
+
saving: t('config.saving'),
|
|
1460
|
+
saveFailed: t('config.failed'),
|
|
1461
|
+
}
|
|
1462
|
+
const save = async () => {
|
|
1463
|
+
if (busy === true || ops.length === 0) return
|
|
1464
|
+
setBusy(true)
|
|
1465
|
+
setFailed(false)
|
|
1466
|
+
try {
|
|
1467
|
+
const acceptedChange = await props.form.mutate(ops, formState.revision)
|
|
1468
|
+
if (acceptedChange !== true) setFailed(true)
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
void error
|
|
1471
|
+
setFailed(true)
|
|
1472
|
+
} finally {
|
|
1473
|
+
setBusy(false)
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const discard = () => {
|
|
1477
|
+
setDraft(draftOf(accepted))
|
|
1478
|
+
setFailed(false)
|
|
1479
|
+
}
|
|
1480
|
+
const toggleRow = (field, labelKey, hintKey, last) => React.createElement('div', {
|
|
1481
|
+
style: last === true ? configStyles.switchRowLast : configStyles.switchRow,
|
|
1482
|
+
},
|
|
1483
|
+
React.createElement('div', { style: configStyles.switchText },
|
|
1484
|
+
React.createElement('span', { style: configStyles.switchLabel }, t(labelKey)),
|
|
1485
|
+
React.createElement('span', { style: configStyles.switchHint }, t(hintKey)),
|
|
1486
|
+
),
|
|
1487
|
+
React.createElement(Switch, {
|
|
1488
|
+
checked: draft[field] !== false,
|
|
1489
|
+
onChange: (next) => setDraft({ ...draft, [field]: next }),
|
|
1490
|
+
label: t(labelKey),
|
|
1491
|
+
disabled: writable !== true,
|
|
1492
|
+
}),
|
|
1493
|
+
)
|
|
1494
|
+
return React.createElement(SettingsForm, { labels, state: model, onSave: save, onDiscard: discard },
|
|
1495
|
+
React.createElement('p', { style: configStyles.noticeOfficial }, t('config.intro')),
|
|
1496
|
+
React.createElement('div', { style: configStyles.switchList },
|
|
1497
|
+
toggleRow('enabled', 'config.enabled', 'config.enabledHint', false),
|
|
1498
|
+
toggleRow('allowEval', 'config.allowEval', 'config.allowEvalHint', false),
|
|
1499
|
+
toggleRow('allowNativeInput', 'config.allowNativeInput', 'config.allowNativeInputHint', true),
|
|
1500
|
+
),
|
|
1501
|
+
React.createElement('div', { style: configStyles.field },
|
|
1502
|
+
React.createElement('span', { style: configStyles.fieldLabel }, t('config.hosts')),
|
|
1503
|
+
React.createElement('textarea', {
|
|
1504
|
+
style: configStyles.textareaOfficial,
|
|
1505
|
+
rows: 4,
|
|
1506
|
+
spellCheck: false,
|
|
1507
|
+
disabled: writable !== true,
|
|
1508
|
+
placeholder: t('config.hostsPlaceholder'),
|
|
1509
|
+
value: draft.hostsText,
|
|
1510
|
+
onChange: (event) => setDraft({ ...draft, hostsText: event.target.value }),
|
|
1511
|
+
}),
|
|
1512
|
+
React.createElement('span', { style: configStyles.hint }, t('config.hostsHint')),
|
|
1513
|
+
),
|
|
1514
|
+
)
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1356
1517
|
/**
|
|
1357
1518
|
* The `plugins.row.config` page: a summary line and the staged form.
|
|
1358
1519
|
* @param props - `{ view, form, t }` from the Plugins page.
|
|
@@ -1361,6 +1522,7 @@ window.__ModuleLoader__.load({
|
|
|
1361
1522
|
function ConfigPage(props) {
|
|
1362
1523
|
if (props.view === 'summary') return props.t('config.summary')
|
|
1363
1524
|
if (props.form === undefined) return React.createElement('p', { style: configStyles.hint }, props.t('config.unavailable'))
|
|
1525
|
+
if (officialPrimitives !== null) return React.createElement(OfficialConfigEditor, { form: props.form, t: props.t })
|
|
1364
1526
|
return React.createElement(ConfigEditor, { form: props.form, t: props.t })
|
|
1365
1527
|
}
|
|
1366
1528
|
|
|
@@ -1645,7 +1807,7 @@ window.__ModuleLoader__.load({
|
|
|
1645
1807
|
locale: UI_NS,
|
|
1646
1808
|
}, ConfigPage)), 'sidebar-browser-agent: config page')
|
|
1647
1809
|
},
|
|
1648
|
-
__test: { hostsToText, textToHosts, configOps, draftOf, withTimeout, TIMEOUTS, CONFIG_ROW_KEY, CONFIG_ENTRY_ID },
|
|
1810
|
+
__test: { hostsToText, textToHosts, configOps, draftOf, settingsFormState, withTimeout, TIMEOUTS, CONFIG_ROW_KEY, CONFIG_ENTRY_ID, usesOfficialForm: officialPrimitives !== null },
|
|
1649
1811
|
}
|
|
1650
1812
|
},
|
|
1651
1813
|
})
|
|
@@ -20,7 +20,7 @@ import z from '../vendor/schemastery/index.mjs'
|
|
|
20
20
|
export const name = 'dsh-sidebar-browser-agent'
|
|
21
21
|
|
|
22
22
|
/** Plugin version reported by diagnostics; keep in sync with package.json. */
|
|
23
|
-
export const PLUGIN_VERSION = '0.2.
|
|
23
|
+
export const PLUGIN_VERSION = '0.2.8'
|
|
24
24
|
|
|
25
25
|
/** Services that must exist before this plugin mounts. */
|
|
26
26
|
export const inject = ['webServer', 'connection', 'tools']
|
package/lib/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import z from '../vendor/schemastery/index.mjs'
|
|
|
20
20
|
export const name = 'dsh-sidebar-browser-agent'
|
|
21
21
|
|
|
22
22
|
/** Plugin version reported by diagnostics; keep in sync with package.json. */
|
|
23
|
-
export const PLUGIN_VERSION = '0.2.
|
|
23
|
+
export const PLUGIN_VERSION = '0.2.8'
|
|
24
24
|
|
|
25
25
|
/** Services that must exist before this plugin mounts. */
|
|
26
26
|
export const inject = ['webServer', 'connection', 'tools']
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidebar-browser-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"icon": "./icon.svg",
|
|
5
6
|
"description": "Native agent control for the DSH right-sidebar browser: manage tabs, and read, click, type, scroll, run JavaScript in and screenshot the real page through its Electron guest.",
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"repository": {
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
"electron-webview"
|
|
20
21
|
],
|
|
21
22
|
"exports": {
|
|
22
|
-
".": "./lib/entry-0.2.
|
|
23
|
+
".": "./lib/entry-0.2.8.js",
|
|
23
24
|
"./client": "./lib/client.js",
|
|
24
25
|
"./locale/*.json": "./locale/*.json",
|
|
25
26
|
"./icon": "./icon.svg"
|
package/test/client-harness.mjs
CHANGED
|
@@ -574,6 +574,20 @@ async function main() {
|
|
|
574
574
|
&& pageNodes.filter((node) => node.type === 'input' && node.props.type === 'checkbox').length === 3
|
|
575
575
|
&& pageNodes.some((node) => node.type === 'textarea'), pageNodes.map((node) => node.type).join(','))
|
|
576
576
|
|
|
577
|
+
// --- official settings-form projection ---
|
|
578
|
+
check('official form flag is reported', testHook.usesOfficialForm === false, testHook.usesOfficialForm)
|
|
579
|
+
const readyModel = testHook.settingsFormState({ status: 'ready', writable: true, value: {} }, true, false, false)
|
|
580
|
+
check('settings form state projects a dirty editable form',
|
|
581
|
+
readyModel.available === true && readyModel.writable === true && readyModel.dirty === true
|
|
582
|
+
&& readyModel.invalid === false && readyModel.saving === false && readyModel.failed === false, JSON.stringify(readyModel))
|
|
583
|
+
const cleanModel = testHook.settingsFormState({ status: 'ready', writable: true, value: {} }, false, true, true)
|
|
584
|
+
check('settings form state projects saving and failure',
|
|
585
|
+
cleanModel.dirty === false && cleanModel.saving === true && cleanModel.failed === true, JSON.stringify(cleanModel))
|
|
586
|
+
const readOnlyModel = testHook.settingsFormState({ status: 'ready', writable: false, value: {} }, false, false, false)
|
|
587
|
+
check('settings form state projects read-only', readOnlyModel.writable === false && readOnlyModel.available === true, JSON.stringify(readOnlyModel))
|
|
588
|
+
const unavailableModel = testHook.settingsFormState({ status: 'loading' }, false, false, false)
|
|
589
|
+
check('settings form state projects unavailable', unavailableModel.available === false && unavailableModel.dirty === false && unavailableModel.writable === false, JSON.stringify(unavailableModel))
|
|
590
|
+
|
|
577
591
|
// --- tab management ---
|
|
578
592
|
const open = await send({ id: 'o1', action: 'open', sessionId: 'session-1', url: 'https://example.test/new' })
|
|
579
593
|
check('open result', open.ok === true && open.value.tabId === 'tab-2' && open.value.url === 'https://example.test/new', open.value)
|
package/test/host-harness.mjs
CHANGED
|
@@ -370,6 +370,14 @@ async function main() {
|
|
|
370
370
|
// --- version consistency and the versioned Host entry ---
|
|
371
371
|
check('package version matches host constant', packageJson.version === PLUGIN_VERSION, `${packageJson.version} vs ${PLUGIN_VERSION}`)
|
|
372
372
|
check('client version matches package', clientSource.includes(`PLUGIN_VERSION = '${packageJson.version}'`), packageJson.version)
|
|
373
|
+
const iconReference = packageJson.icon
|
|
374
|
+
check('package declares a relative icon', typeof iconReference === 'string' && iconReference.startsWith('./') && !iconReference.includes('..'), iconReference)
|
|
375
|
+
const iconFile = typeof iconReference === 'string' ? path.resolve(here, '..', iconReference) : ''
|
|
376
|
+
check('declared icon exists', iconFile !== '' && fs.existsSync(iconFile), iconFile)
|
|
377
|
+
const iconSource = iconFile !== '' && fs.existsSync(iconFile) ? fs.readFileSync(iconFile, 'utf8') : ''
|
|
378
|
+
check('declared icon is a 36x36 svg', iconSource.startsWith('<svg') && iconSource.includes('viewBox="0 0 36 36"'), iconSource.slice(0, 60))
|
|
379
|
+
check('declared icon ships with the package', Array.isArray(packageJson.files) && packageJson.files.includes('icon.svg'), JSON.stringify(packageJson.files))
|
|
380
|
+
|
|
373
381
|
const expectedEntry = `./lib/entry-${packageJson.version}.js`
|
|
374
382
|
check('package entry is versioned', packageJson.exports?.['.'] === expectedEntry, packageJson.exports?.['.'])
|
|
375
383
|
const entryFile = path.join(here, '..', 'lib', `entry-${packageJson.version}.js`)
|