dsh-sidebar-browser-agent 0.2.7 → 0.2.9
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 +15 -0
- package/lib/client.js +178 -3
- package/lib/{entry-0.2.7.js → entry-0.2.9.js} +1 -1
- package/lib/index.js +1 -1
- package/package.json +2 -2
- package/test/client-harness.mjs +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.9
|
|
4
|
+
|
|
5
|
+
- Polished the official-style plugin page rhythm: field/row spacing, tertiary
|
|
6
|
+
12px hints, the same input surface and radius the built-in settings pages
|
|
7
|
+
use, and the shared form footer as the only save action.
|
|
8
|
+
|
|
9
|
+
## 0.2.8
|
|
10
|
+
|
|
11
|
+
- The plugin page now follows the official plugin-page style: the configuration
|
|
12
|
+
form is built on the platform's shared settings primitives (`SettingsForm`,
|
|
13
|
+
`Switch`), with the official footer save action, token-based typography and
|
|
14
|
+
spacing, and a switch list plus allowlist field. A carrier without those
|
|
15
|
+
primitives keeps the previous self-contained form as a fallback, and
|
|
16
|
+
`sidebar_browser status` reports which path is active.
|
|
17
|
+
|
|
3
18
|
## 0.2.7
|
|
4
19
|
|
|
5
20
|
- Redrew the plugin icon in the official plugin style: a transparent 36x36
|
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.9'
|
|
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,53 @@ window.__ModuleLoader__.load({
|
|
|
1206
1226
|
cursor: 'pointer',
|
|
1207
1227
|
font: 'inherit',
|
|
1208
1228
|
},
|
|
1229
|
+
// --- official-style settings page (shared SettingsForm + Switch) ---
|
|
1230
|
+
// Mirrors the built-in settings page rhythm: the shared form only stacks
|
|
1231
|
+
// children, so each block below brings its own 12px vertical padding, the
|
|
1232
|
+
// same scale the official fields and rows use.
|
|
1233
|
+
formBody: { display: 'flex', flexDirection: 'column' },
|
|
1234
|
+
noticeOfficial: {
|
|
1235
|
+
margin: '0 0 4px',
|
|
1236
|
+
color: 'var(--dsw-alias-label-secondary)',
|
|
1237
|
+
fontSize: '13px',
|
|
1238
|
+
lineHeight: 1.5,
|
|
1239
|
+
},
|
|
1240
|
+
switchList: { display: 'flex', flexDirection: 'column', padding: '12px 0' },
|
|
1241
|
+
switchRow: {
|
|
1242
|
+
display: 'flex',
|
|
1243
|
+
alignItems: 'center',
|
|
1244
|
+
gap: '16px',
|
|
1245
|
+
minHeight: '40px',
|
|
1246
|
+
padding: '8px 0',
|
|
1247
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2)',
|
|
1248
|
+
},
|
|
1249
|
+
switchRowLast: {
|
|
1250
|
+
display: 'flex',
|
|
1251
|
+
alignItems: 'center',
|
|
1252
|
+
gap: '16px',
|
|
1253
|
+
minHeight: '40px',
|
|
1254
|
+
padding: '8px 0',
|
|
1255
|
+
},
|
|
1256
|
+
switchText: { display: 'flex', flexDirection: 'column', gap: '2px', flex: '1 1 auto', minWidth: 0 },
|
|
1257
|
+
switchLabel: { color: 'var(--dsw-alias-label-primary)', fontSize: '13px', lineHeight: 1.5, fontWeight: 500 },
|
|
1258
|
+
switchHint: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px', lineHeight: 1.5 },
|
|
1259
|
+
field: { display: 'flex', flexDirection: 'column', gap: '6px', padding: '12px 0' },
|
|
1260
|
+
fieldLabel: { color: 'var(--dsw-alias-label-primary)', fontSize: '13px', lineHeight: 1.5, fontWeight: 500 },
|
|
1261
|
+
fieldHint: { color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px', lineHeight: 1.5 },
|
|
1262
|
+
textareaOfficial: {
|
|
1263
|
+
boxSizing: 'border-box',
|
|
1264
|
+
width: '100%',
|
|
1265
|
+
minHeight: '96px',
|
|
1266
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1267
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1268
|
+
border: '0.5px solid var(--dsw-alias-border-l4)',
|
|
1269
|
+
borderRadius: 'var(--dsw-radius-md, 8px)',
|
|
1270
|
+
font: 'inherit',
|
|
1271
|
+
fontSize: '13px',
|
|
1272
|
+
lineHeight: 1.5,
|
|
1273
|
+
padding: '8px 12px',
|
|
1274
|
+
resize: 'vertical',
|
|
1275
|
+
},
|
|
1209
1276
|
}
|
|
1210
1277
|
|
|
1211
1278
|
/**
|
|
@@ -1322,7 +1389,7 @@ window.__ModuleLoader__.load({
|
|
|
1322
1389
|
switchRow(t, { label: 'config.enabled', hint: 'config.enabledHint', checked: draft.enabled, disabled: readOnly, onChange: (next) => setDraft({ ...draft, enabled: next }) }),
|
|
1323
1390
|
React.createElement('div', { style: configStyles.field },
|
|
1324
1391
|
React.createElement('span', { style: configStyles.checkLabel }, t('config.hosts')),
|
|
1325
|
-
React.createElement('span', { style: configStyles.
|
|
1392
|
+
React.createElement('span', { style: configStyles.fieldHint }, t('config.hostsHint')),
|
|
1326
1393
|
React.createElement('textarea', {
|
|
1327
1394
|
style: configStyles.textarea,
|
|
1328
1395
|
rows: 4,
|
|
@@ -1353,6 +1420,113 @@ window.__ModuleLoader__.load({
|
|
|
1353
1420
|
)
|
|
1354
1421
|
}
|
|
1355
1422
|
|
|
1423
|
+
/**
|
|
1424
|
+
* Project the staged row form onto the shared Plugins settings-form model.
|
|
1425
|
+
* @param formState - `form.state` handed to `plugins.row.config`.
|
|
1426
|
+
* @param dirty - whether the local draft differs from Host values.
|
|
1427
|
+
* @param busy - whether a save is in flight.
|
|
1428
|
+
* @param failed - whether the last save was rejected.
|
|
1429
|
+
* @returns the `state` prop the shared `SettingsForm` renders.
|
|
1430
|
+
*/
|
|
1431
|
+
function settingsFormState(formState, dirty, busy, failed) {
|
|
1432
|
+
const ready = formState !== null && typeof formState === 'object'
|
|
1433
|
+
&& formState.status === 'ready' && formState.value !== undefined
|
|
1434
|
+
return {
|
|
1435
|
+
available: ready,
|
|
1436
|
+
writable: ready && formState.writable !== false,
|
|
1437
|
+
dirty: ready && dirty === true,
|
|
1438
|
+
invalid: false,
|
|
1439
|
+
saving: busy === true,
|
|
1440
|
+
failed: ready && failed === true,
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Official-style configuration page: the shared SettingsForm footer plus a
|
|
1446
|
+
* switch list and the allowlist field, matching the built-in plugin pages.
|
|
1447
|
+
* @param props - `{ form, t }` from the Plugins page.
|
|
1448
|
+
*/
|
|
1449
|
+
function OfficialConfigEditor(props) {
|
|
1450
|
+
const t = props.t
|
|
1451
|
+
const SettingsForm = officialPrimitives.SettingsForm
|
|
1452
|
+
const Switch = officialPrimitives.Switch
|
|
1453
|
+
const formState = props.form.state
|
|
1454
|
+
const accepted = formState.value ?? {}
|
|
1455
|
+
const [draft, setDraft] = React.useState(() => draftOf(accepted))
|
|
1456
|
+
const [busy, setBusy] = React.useState(false)
|
|
1457
|
+
const [failed, setFailed] = React.useState(false)
|
|
1458
|
+
React.useEffect(() => {
|
|
1459
|
+
setDraft(draftOf(accepted))
|
|
1460
|
+
setFailed(false)
|
|
1461
|
+
}, [formState.revision, formState.status])
|
|
1462
|
+
const ready = formState !== null && typeof formState === 'object' && formState.status === 'ready' && formState.value !== undefined
|
|
1463
|
+
const writable = ready && formState.writable !== false
|
|
1464
|
+
const ops = configOps(draft, accepted)
|
|
1465
|
+
const model = settingsFormState(formState, ops.length > 0, busy, failed)
|
|
1466
|
+
const labels = {
|
|
1467
|
+
unavailable: formState?.status === 'loading' ? t('config.loading') : t('config.unavailable'),
|
|
1468
|
+
readOnly: t('config.readOnly'),
|
|
1469
|
+
save: t('config.save'),
|
|
1470
|
+
saving: t('config.saving'),
|
|
1471
|
+
saveFailed: t('config.failed'),
|
|
1472
|
+
}
|
|
1473
|
+
const save = async () => {
|
|
1474
|
+
if (busy === true || ops.length === 0) return
|
|
1475
|
+
setBusy(true)
|
|
1476
|
+
setFailed(false)
|
|
1477
|
+
try {
|
|
1478
|
+
const acceptedChange = await props.form.mutate(ops, formState.revision)
|
|
1479
|
+
if (acceptedChange !== true) setFailed(true)
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
void error
|
|
1482
|
+
setFailed(true)
|
|
1483
|
+
} finally {
|
|
1484
|
+
setBusy(false)
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
const discard = () => {
|
|
1488
|
+
setDraft(draftOf(accepted))
|
|
1489
|
+
setFailed(false)
|
|
1490
|
+
}
|
|
1491
|
+
const toggleRow = (field, labelKey, hintKey, last) => React.createElement('div', {
|
|
1492
|
+
style: last === true ? configStyles.switchRowLast : configStyles.switchRow,
|
|
1493
|
+
},
|
|
1494
|
+
React.createElement('div', { style: configStyles.switchText },
|
|
1495
|
+
React.createElement('span', { style: configStyles.switchLabel }, t(labelKey)),
|
|
1496
|
+
React.createElement('span', { style: configStyles.switchHint }, t(hintKey)),
|
|
1497
|
+
),
|
|
1498
|
+
React.createElement(Switch, {
|
|
1499
|
+
checked: draft[field] !== false,
|
|
1500
|
+
onChange: (next) => setDraft({ ...draft, [field]: next }),
|
|
1501
|
+
label: t(labelKey),
|
|
1502
|
+
disabled: writable !== true,
|
|
1503
|
+
}),
|
|
1504
|
+
)
|
|
1505
|
+
return React.createElement(SettingsForm, { labels, state: model, onSave: save, onDiscard: discard },
|
|
1506
|
+
React.createElement('div', { style: configStyles.formBody },
|
|
1507
|
+
React.createElement('p', { style: configStyles.noticeOfficial }, t('config.intro')),
|
|
1508
|
+
React.createElement('div', { style: configStyles.switchList },
|
|
1509
|
+
toggleRow('enabled', 'config.enabled', 'config.enabledHint', false),
|
|
1510
|
+
toggleRow('allowEval', 'config.allowEval', 'config.allowEvalHint', false),
|
|
1511
|
+
toggleRow('allowNativeInput', 'config.allowNativeInput', 'config.allowNativeInputHint', true),
|
|
1512
|
+
),
|
|
1513
|
+
React.createElement('div', { style: configStyles.field },
|
|
1514
|
+
React.createElement('span', { style: configStyles.fieldLabel }, t('config.hosts')),
|
|
1515
|
+
React.createElement('textarea', {
|
|
1516
|
+
style: configStyles.textareaOfficial,
|
|
1517
|
+
rows: 4,
|
|
1518
|
+
spellCheck: false,
|
|
1519
|
+
disabled: writable !== true,
|
|
1520
|
+
placeholder: t('config.hostsPlaceholder'),
|
|
1521
|
+
value: draft.hostsText,
|
|
1522
|
+
onChange: (event) => setDraft({ ...draft, hostsText: event.target.value }),
|
|
1523
|
+
}),
|
|
1524
|
+
React.createElement('span', { style: configStyles.fieldHint }, t('config.hostsHint')),
|
|
1525
|
+
),
|
|
1526
|
+
),
|
|
1527
|
+
)
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1356
1530
|
/**
|
|
1357
1531
|
* The `plugins.row.config` page: a summary line and the staged form.
|
|
1358
1532
|
* @param props - `{ view, form, t }` from the Plugins page.
|
|
@@ -1361,6 +1535,7 @@ window.__ModuleLoader__.load({
|
|
|
1361
1535
|
function ConfigPage(props) {
|
|
1362
1536
|
if (props.view === 'summary') return props.t('config.summary')
|
|
1363
1537
|
if (props.form === undefined) return React.createElement('p', { style: configStyles.hint }, props.t('config.unavailable'))
|
|
1538
|
+
if (officialPrimitives !== null) return React.createElement(OfficialConfigEditor, { form: props.form, t: props.t })
|
|
1364
1539
|
return React.createElement(ConfigEditor, { form: props.form, t: props.t })
|
|
1365
1540
|
}
|
|
1366
1541
|
|
|
@@ -1645,7 +1820,7 @@ window.__ModuleLoader__.load({
|
|
|
1645
1820
|
locale: UI_NS,
|
|
1646
1821
|
}, ConfigPage)), 'sidebar-browser-agent: config page')
|
|
1647
1822
|
},
|
|
1648
|
-
__test: { hostsToText, textToHosts, configOps, draftOf, withTimeout, TIMEOUTS, CONFIG_ROW_KEY, CONFIG_ENTRY_ID },
|
|
1823
|
+
__test: { hostsToText, textToHosts, configOps, draftOf, settingsFormState, withTimeout, TIMEOUTS, CONFIG_ROW_KEY, CONFIG_ENTRY_ID, usesOfficialForm: officialPrimitives !== null },
|
|
1649
1824
|
}
|
|
1650
1825
|
},
|
|
1651
1826
|
})
|
|
@@ -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.9'
|
|
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.9'
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sidebar-browser-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
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.",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"electron-webview"
|
|
21
21
|
],
|
|
22
22
|
"exports": {
|
|
23
|
-
".": "./lib/entry-0.2.
|
|
23
|
+
".": "./lib/entry-0.2.9.js",
|
|
24
24
|
"./client": "./lib/client.js",
|
|
25
25
|
"./locale/*.json": "./locale/*.json",
|
|
26
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)
|