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 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.7'
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.hint }, t('config.hostsHint')),
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.7'
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.7'
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.7",
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.7.js",
23
+ ".": "./lib/entry-0.2.9.js",
24
24
  "./client": "./lib/client.js",
25
25
  "./locale/*.json": "./locale/*.json",
26
26
  "./icon": "./icon.svg"
@@ -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)