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 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Sidebar browser agent">
2
- <rect x="2" y="2" width="60" height="60" rx="14" fill="#1f6feb"/>
3
- <circle cx="31" cy="29" r="15" fill="none" stroke="#ffffff" stroke-width="2.4"/>
4
- <ellipse cx="31" cy="29" rx="6.5" ry="15" fill="none" stroke="#ffffff" stroke-width="2.4"/>
5
- <path d="M16 29h30" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
6
- <circle cx="47" cy="47" r="9.5" fill="#0b2a4a" stroke="#7ee787" stroke-width="2"/>
7
- <path d="M42.5 47h9M47 42.5v9" stroke="#7ee787" stroke-width="2" stroke-linecap="round"/>
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.6'
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.6'
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.6'
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.6",
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.6.js",
23
+ ".": "./lib/entry-0.2.8.js",
23
24
  "./client": "./lib/client.js",
24
25
  "./locale/*.json": "./locale/*.json",
25
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)
@@ -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`)