dsh-github-router 0.3.1 → 0.4.0

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/lib/client.js CHANGED
@@ -1,28 +1,28 @@
1
1
  /**
2
2
  * dsh-github-router — client half (hand-written factory bundle, no build step).
3
3
  *
4
- * One plugin configuration CARD inside the framework's Settings → Plugins →
5
- * configurable tab, registered into the `settings.plugin.item` slot keyed by
6
- * the settings namespace — the framework mechanism for plugins distributed
7
- * outside the repository (DSH ≥ 0.1.0-rc.7): the Host registers the
8
- * namespace, the browser registers the card under the same key, and the tab
9
- * pairs the two. No plugin-owned HTTP route exists anymore; reads and writes
10
- * ride the framework settings transport (`ctx.settingsScope`).
4
+ * One configuration page for the plugin's Loader row, registered into the
5
+ * Plugins page's `plugins.row.config` slot under the row key
6
+ * `<package name>#<row id>` (DSH ≥ 0.1.7-rc.2). The Plugins page owns the
7
+ * frame — title, icon, crumb, and the row's Configure control — and asks this
8
+ * entry for `view: 'summary'` (the row's one-liner) or `view: 'page'` (the
9
+ * form). No plugin-owned HTTP route exists: reads and writes ride the shared
10
+ * configuration form the settings domain provides per Host entry
11
+ * (`ctx.configForms.get(namespace)`), whose writes are revision-fenced and
12
+ * recovered by the framework.
11
13
  *
12
14
  * The form keeps the staged-editing model: edits are staged locally and
13
- * written only on save; each write goes through the bound settings scope
14
- * (revision fencing, recovery reads on failure) and the card verifies the
15
- * user layer afterwards, keeping drafts that did not land. Secret fields
16
- * (the GitHub token) never ride a response, so the token control is
17
- * write-only: it starts blank, a typed value writes the token, and a blank
18
- * draft clears a configured one — the configured state comes from the
19
- * describe mirror's secret slot list.
15
+ * written only on save; leaving the page drops them. The card verifies the
16
+ * user layer after a write and keeps drafts that did not land. Secret fields
17
+ * (the GitHub token) never ride a response, so the token control starts blank
18
+ * and a blank draft writes nothing — the describe mirror's secret-slot list is
19
+ * the only signal that one is configured, and an explicit clear control stages
20
+ * the `unset` that removes it.
20
21
  *
21
- * The module requires only `react` and
22
- * `@deepseek-ai/dsh-client-store` (the shared browser snapshot-store seed —
23
- * the only runtime API it uses is `createSnapshotStore`). All copy goes
24
- * through the framework locale service under the `dsh-github-router`
25
- * namespace.
22
+ * The bundle requires only `react` and `@deepseek-ai/dsh-client-store`, both
23
+ * members of the shell's frozen platform module table, so it declares no
24
+ * `dsh.client.inject` rows. All copy goes through the framework locale service
25
+ * under the `dsh-github-router` namespace.
26
26
  */
27
27
  window.__ModuleLoader__.load({
28
28
  id: 'dsh-github-router',
@@ -34,6 +34,10 @@ window.__ModuleLoader__.load({
34
34
 
35
35
  var NAMESPACE = 'dsh-github-router';
36
36
  var NS = 'dsh-github-router';
37
+ // `plugins.row.config` is keyed by `<package name>#<row id>`; this bundle's
38
+ // cordis.patch.yml declares both as `dsh-github-router`, so the row page
39
+ // and the settings namespace address the same entry.
40
+ var ROW_CONFIG_KEY = 'dsh-github-router#dsh-github-router';
37
41
 
38
42
  // ------------------------------------------------------------ specs
39
43
  function textSpec(field) {
@@ -114,17 +118,18 @@ window.__ModuleLoader__.load({
114
118
 
115
119
  // ------------------------------------------------------------- i18n
116
120
  var zhDict = {
117
- 'nav': 'GitHub 路由',
118
121
  'description': 'GitHub 读取的路由与缓存配置。',
119
122
  'advanced': '高级设置',
120
123
  'save': '保存',
121
124
  'saving': '保存中…',
122
- 'discard': '放弃修改',
123
125
  'failed': '保存失败,请重试。',
124
126
  'readOnly': '当前部署不允许修改设置。',
127
+ 'unavailable': '该插件当前未加载,暂时无法配置。',
125
128
  'invalid': '无效值',
126
129
  'overridden': '已覆盖',
127
130
  'configured': '已配置',
131
+ 'clearToken': '清除已保存的 token',
132
+ 'willClear': '保存后清除',
128
133
  'field.token': 'GitHub Token',
129
134
  'field.tokenEnv': 'Token 环境变量',
130
135
  'field.proxy': '代理地址',
@@ -142,7 +147,7 @@ window.__ModuleLoader__.load({
142
147
  'field.mirrors': '镜像基址(逗号分隔)',
143
148
  'field.repos': '本地仓库白名单(逗号分隔)',
144
149
  'field.gitCacheDir': 'git 缓存目录',
145
- 'hint.token': '仅写入:留空并保存 = 清除已配置的 token;优先使用 tokenEnv。',
150
+ 'hint.token': '只写字段:留空保存表示保持当前 token 不变;用「清除」按钮删除已保存的 token。优先使用 tokenEnv。',
146
151
  'hint.tokenEnv': '指向 token 的环境变量名,例如 GITHUB_TOKEN。',
147
152
  'hint.proxy': '空 = 继承环境代理;direct = 永不代理;其余为代理 URL。',
148
153
  'hint.maxBytes': '每个响应体的字节上限(16384..8388608)。',
@@ -154,17 +159,18 @@ window.__ModuleLoader__.load({
154
159
  'hint.gitCacheDir': '空 = <DSH_HOME>/storages/dsh-github-router/git。',
155
160
  };
156
161
  var enDict = {
157
- 'nav': 'GitHub Router',
158
162
  'description': 'Routing and cache configuration for GitHub reads.',
159
163
  'advanced': 'Advanced settings',
160
164
  'save': 'Save',
161
165
  'saving': 'Saving…',
162
- 'discard': 'Discard',
163
166
  'failed': 'The save failed. Please retry.',
164
167
  'readOnly': 'This deployment does not allow settings edits.',
168
+ 'unavailable': 'This plugin is not loaded, so it cannot be configured right now.',
165
169
  'invalid': 'invalid',
166
170
  'overridden': 'overridden',
167
171
  'configured': 'configured',
172
+ 'clearToken': 'Clear stored token',
173
+ 'willClear': 'clears on save',
168
174
  'field.token': 'GitHub token',
169
175
  'field.tokenEnv': 'Token env var',
170
176
  'field.proxy': 'Proxy URL',
@@ -182,7 +188,7 @@ window.__ModuleLoader__.load({
182
188
  'field.mirrors': 'Mirror bases (comma-separated)',
183
189
  'field.repos': 'Local repo allowlist (comma-separated)',
184
190
  'field.gitCacheDir': 'Git cache directory',
185
- 'hint.token': 'Write-only: a blank save clears the configured token; prefer tokenEnv.',
191
+ 'hint.token': 'Write-only: a blank draft keeps the stored token; the Clear control removes it. Prefer tokenEnv.',
186
192
  'hint.tokenEnv': 'Environment variable naming the token, e.g. GITHUB_TOKEN.',
187
193
  'hint.proxy': "Empty = inherit ambient proxy env; 'direct' = never proxy; anything else is a proxy URL.",
188
194
  'hint.maxBytes': 'Byte cap per response body (16384..8388608).',
@@ -201,16 +207,20 @@ window.__ModuleLoader__.load({
201
207
  }
202
208
 
203
209
  /**
204
- * Stages one card's edits over the plugin's settings namespace and writes
205
- * them on save. The scope is the framework-bound settings scope
206
- * (`ctx.settingsScope.bind({ namespace })`): its snapshot carries the
207
- * redacted resolved section (`status`/`value`/`base`/`user`/`revision`/
208
- * `writable`), and `set`/`unset` write one field each with revision
209
- * fencing and recovery reads. The describe mirror supplements the
210
- * snapshot with the secret slot list, the only signal that a redacted
211
- * field (the token) is configured.
210
+ * Stages one page's edits over the plugin's settings namespace and writes
211
+ * them on save.
212
+ *
213
+ * The scope is the shared configuration form the settings domain provides
214
+ * for this Host entry (`ctx.configForms.get(namespace)`): its snapshot
215
+ * carries the redacted resolved section
216
+ * (`status`/`value`/`base`/`user`/`revision`/`writable`/`mode`), and
217
+ * `set`/`unset` queue one revision-fenced, recovered write each. The
218
+ * describe mirror supplements the snapshot with the secret-slot list, the
219
+ * only signal that a redacted field (the token) is configured.
220
+ * @param scope - the shared configuration form of this entry.
221
+ * @param describe - the deployment-wide describe face, or null when absent.
212
222
  */
213
- function GithubRouterForm(scope, mirror) {
223
+ function GithubRouterForm(scope, describe) {
214
224
  var staged = new Map(); // field -> { text, clear }
215
225
  var saving = false;
216
226
  var failed = false;
@@ -237,14 +247,25 @@ window.__ModuleLoader__.load({
237
247
  var s = snap();
238
248
  return !!(s.user && typeof s.user === 'object' && Object.prototype.hasOwnProperty.call(s.user, field));
239
249
  }
240
- /** Whether a redacted secret slot holds a value (from the describe mirror). */
250
+ /**
251
+ * Whether a redacted secret slot holds a value. Secret fields never ride
252
+ * the namespace value, so the describe mirror's per-namespace `secrets`
253
+ * list is the only source for this fact.
254
+ */
241
255
  function secretSet(field) {
242
- if (mirror === null || typeof mirror.namespace !== 'function') return false;
243
- var row = mirror.namespace(NAMESPACE);
244
- if (row === undefined || !Array.isArray(row.secrets)) return false;
245
- for (var i = 0; i < row.secrets.length; i++) {
246
- var slot = row.secrets[i];
247
- if (Array.isArray(slot.path) && slot.path.length === 1 && slot.path[0] === field) return slot.set === true;
256
+ if (describe === null || typeof describe.getSnapshot !== 'function') return false;
257
+ var mirror = describe.getSnapshot();
258
+ var view = mirror === null || mirror === undefined ? undefined : mirror.view;
259
+ var rows = view !== undefined && view !== null && Array.isArray(view.namespaces) ? view.namespaces : [];
260
+ for (var i = 0; i < rows.length; i++) {
261
+ var row = rows[i];
262
+ if (row === null || row === undefined || row.ns !== NAMESPACE) continue;
263
+ var secrets = Array.isArray(row.secrets) ? row.secrets : [];
264
+ for (var j = 0; j < secrets.length; j++) {
265
+ var slot = secrets[j];
266
+ if (Array.isArray(slot.path) && slot.path.length === 1 && slot.path[0] === field) return slot.set === true;
267
+ }
268
+ return false;
248
269
  }
249
270
  return false;
250
271
  }
@@ -257,10 +278,12 @@ window.__ModuleLoader__.load({
257
278
  var stagedEntry = staged.get(f.field);
258
279
  var text;
259
280
  var overridden;
281
+ var clearing = false;
260
282
  var invalidField = false;
261
283
  if (stagedEntry !== undefined) {
262
284
  text = stagedEntry.text;
263
- if (stagedEntry.clear) {
285
+ clearing = stagedEntry.clear === true;
286
+ if (clearing) {
264
287
  overridden = false;
265
288
  } else {
266
289
  var parsedStaged = f.spec.parse(stagedEntry.text);
@@ -271,14 +294,15 @@ window.__ModuleLoader__.load({
271
294
  }
272
295
  } else {
273
296
  text = f.kind === 'secret' ? '' : f.spec.format(sectionValue(f.field));
274
- overridden = f.kind === 'secret' ? secretSet(f.field) : stored(f.field);
297
+ overridden = f.kind === 'secret' ? false : stored(f.field);
275
298
  }
276
299
  if (invalidField) invalid = true;
277
300
  fields[f.field] = {
278
301
  text: text,
279
302
  overridden: overridden,
280
303
  invalid: invalidField,
281
- configured: f.kind === 'secret' && stagedEntry === undefined ? secretSet(f.field) : undefined,
304
+ configured: f.kind === 'secret' ? secretSet(f.field) : false,
305
+ clearing: clearing,
282
306
  };
283
307
  });
284
308
  store.set({
@@ -291,12 +315,16 @@ window.__ModuleLoader__.load({
291
315
  fields: fields,
292
316
  });
293
317
  }
294
- scope.subscribe(publish);
295
- if (mirror !== null && typeof mirror.subscribe === 'function') mirror.subscribe(publish);
318
+ var offScope = scope.subscribe(publish);
319
+ var offDescribe = describe !== null && typeof describe.subscribe === 'function' ? describe.subscribe(publish) : null;
296
320
  publish();
297
321
 
298
322
  function edit(field, text) {
299
- staged.set(field, { text: String(text), clear: false });
323
+ var value = String(text);
324
+ // A blank secret draft carries no write: the stored secret stays, and
325
+ // the explicit clear control is what removes it.
326
+ if (field === 'token' && value.trim() === '') staged.delete(field);
327
+ else staged.set(field, { text: value, clear: false });
300
328
  failed = false;
301
329
  publish();
302
330
  }
@@ -317,29 +345,10 @@ window.__ModuleLoader__.load({
317
345
  var blocked = false;
318
346
  staged.forEach(function (stagedEntry, field) {
319
347
  var spec = SPECS[field];
320
- if (stagedEntry.clear) {
321
- if (field === 'token') {
322
- // A blank draft clears the token only when one is configured.
323
- if (secretSet(field)) {
324
- writes.push({
325
- field: field,
326
- run: function () { return scope.unset(field).then(function () { return !secretSet(field); }); },
327
- });
328
- }
329
- return;
330
- }
331
- if (stored(field)) {
332
- writes.push({
333
- field: field,
334
- run: function () { return scope.unset(field).then(function () { return !stored(field); }); },
335
- });
336
- }
337
- return;
338
- }
339
348
  if (field === 'token') {
340
- var tokenText = stagedEntry.text.trim();
341
- if (tokenText === '') {
342
- // A blank draft clears the token when one is configured.
349
+ // A blank draft never clears a configured token; only the clear
350
+ // control stages the `unset`, and only while one is configured.
351
+ if (stagedEntry.clear) {
343
352
  if (secretSet(field)) {
344
353
  writes.push({
345
354
  field: field,
@@ -348,14 +357,24 @@ window.__ModuleLoader__.load({
348
357
  }
349
358
  return;
350
359
  }
360
+ if (stagedEntry.text.trim() === '') return;
351
361
  writes.push({
352
362
  field: field,
353
363
  run: (function (value) {
354
364
  return function () { return scope.set(field, value).then(function () { return secretSet(field); }); };
355
- })(tokenText),
365
+ })(stagedEntry.text.trim()),
356
366
  });
357
367
  return;
358
368
  }
369
+ if (stagedEntry.clear) {
370
+ if (stored(field)) {
371
+ writes.push({
372
+ field: field,
373
+ run: function () { return scope.unset(field).then(function () { return !stored(field); }); },
374
+ });
375
+ }
376
+ return;
377
+ }
359
378
  if (stagedEntry.text === spec.format(sectionValue(field))) return;
360
379
  var write = spec.parse(stagedEntry.text);
361
380
  if (write === undefined) {
@@ -363,10 +382,12 @@ window.__ModuleLoader__.load({
363
382
  return;
364
383
  }
365
384
  if (write.kind === 'clear') {
366
- writes.push({
367
- field: field,
368
- run: function () { return scope.unset(field).then(function () { return !stored(field); }); },
369
- });
385
+ if (stored(field)) {
386
+ writes.push({
387
+ field: field,
388
+ run: function () { return scope.unset(field).then(function () { return !stored(field); }); },
389
+ });
390
+ }
370
391
  } else {
371
392
  writes.push({
372
393
  field: field,
@@ -399,25 +420,28 @@ window.__ModuleLoader__.load({
399
420
  publish();
400
421
  });
401
422
  }
423
+ function dispose() {
424
+ if (typeof offScope === 'function') offScope();
425
+ if (typeof offDescribe === 'function') offDescribe();
426
+ }
402
427
 
403
- function inject(t) {
428
+ function inject() {
404
429
  return {
405
- // The slots runtime synthesizes a `use<Key>` hook for every entry
406
- // of this `hooks` object (the shipped plugin cards' `hooks` →
407
- // `useBashCard` contract).
408
- hooks: { settings: store },
409
- t: t,
430
+ // The slots runtime synthesizes a `use<Key>` hook for every entry of
431
+ // this `hooks` object (the shipped plugin pages' `hooks` →
432
+ // `useShellCard` contract).
433
+ hooks: { gitHubRouterCard: store },
410
434
  edit: edit,
411
435
  resetField: resetField,
412
436
  save: save,
413
437
  discard: discard,
414
438
  };
415
439
  }
416
- return { inject: inject, store: store, save: save, edit: edit, discard: discard, resetField: resetField };
440
+ return { inject: inject, store: store, dispose: dispose };
417
441
  }
418
442
 
419
- // ------------------------------------------------------------- card
420
- var cardStyle = { padding: '4px 0 16px' };
443
+ // ------------------------------------------------------------- page
444
+ var pageStyle = { padding: '4px 0 16px' };
421
445
  var gridStyle = {
422
446
  display: 'grid',
423
447
  gridTemplateColumns: 'minmax(180px, 1fr) minmax(180px, 1fr)',
@@ -433,6 +457,7 @@ window.__ModuleLoader__.load({
433
457
  color: 'inherit',
434
458
  };
435
459
  var boolRowStyle = { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' };
460
+ var markerStyle = { marginLeft: '6px', opacity: 0.6 };
436
461
  var footerStyle = { display: 'flex', alignItems: 'center', gap: '8px', marginTop: '12px' };
437
462
  var buttonStyle = {
438
463
  padding: '4px 12px',
@@ -442,17 +467,23 @@ window.__ModuleLoader__.load({
442
467
  color: 'inherit',
443
468
  cursor: 'pointer',
444
469
  };
470
+ var clearButtonStyle = Object.assign({}, buttonStyle, { padding: '2px 8px', fontSize: '12px' });
445
471
 
446
472
  function GithubRouterCard(props) {
447
- var state = props.useSettings(function (s) { return s; });
448
473
  var t = props.t;
449
-
450
- // The framework cards render nothing until their namespace is served;
451
- // this card follows the same posture.
452
- if (!state.available) return null;
474
+ // Hooks run before the view branches so one occupied slot keeps a
475
+ // constant hook count whether it renders the one-liner or the page.
476
+ var state = props.useGitHubRouterCard(function (s) { return s; });
477
+ // Leaving the page drops every staged edit, the same posture the shipped
478
+ // settings pages follow.
479
+ React.useEffect(function () {
480
+ return function () { props.discard(); };
481
+ }, []);
482
+ // The Plugins page asks for the row's one-liner before it asks for the page.
483
+ if (props.view === 'summary') return t('description');
453
484
 
454
485
  function renderField(f) {
455
- var fieldState = state.fields[f.field] || { text: '', overridden: false, invalid: false, configured: false };
486
+ var fieldState = state.fields[f.field] || { text: '', overridden: false, invalid: false, configured: false, clearing: false };
456
487
  if (f.kind === 'bool') {
457
488
  return React.createElement(
458
489
  'label',
@@ -468,34 +499,46 @@ window.__ModuleLoader__.load({
468
499
  }
469
500
  var marker;
470
501
  if (f.kind === 'secret') {
471
- marker = fieldState.configured === true
472
- ? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, t('configured'))
473
- : null;
502
+ marker = fieldState.clearing
503
+ ? React.createElement('span', { style: markerStyle }, t('willClear'))
504
+ : fieldState.configured
505
+ ? React.createElement('span', { style: markerStyle }, t('configured'))
506
+ : null;
474
507
  } else {
475
508
  marker = fieldState.overridden
476
- ? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, t('overridden'))
509
+ ? React.createElement('span', { style: markerStyle }, t('overridden'))
477
510
  : null;
478
511
  }
512
+ var clearControl = f.kind === 'secret' && fieldState.configured && !fieldState.clearing
513
+ ? React.createElement('button', {
514
+ type: 'button',
515
+ style: clearButtonStyle,
516
+ disabled: !state.writable || state.saving,
517
+ onClick: function () { props.resetField(f.field); },
518
+ }, t('clearToken'))
519
+ : null;
479
520
  return React.createElement(
480
521
  'label',
481
522
  { key: f.field, style: rowStyle, title: f.hintKey ? t(f.hintKey) : undefined },
482
523
  React.createElement('span', { style: labelStyle }, t(f.labelKey), marker),
483
524
  React.createElement('input', {
484
525
  type: f.kind === 'number' ? 'number' : f.kind === 'secret' ? 'password' : 'text',
526
+ autoComplete: f.kind === 'secret' ? 'new-password' : undefined,
485
527
  value: fieldState.text,
486
528
  disabled: !state.writable,
487
529
  style: inputStyle,
488
530
  onChange: function (event) { props.edit(f.field, event.target.value); },
489
531
  }),
490
532
  fieldState.invalid ? React.createElement('span', { style: { color: '#c00' } }, t('invalid')) : null,
533
+ clearControl,
491
534
  );
492
535
  }
493
536
 
494
537
  return React.createElement(
495
538
  'section',
496
- { style: cardStyle },
497
- React.createElement('p', { style: { margin: '0 0 12px', opacity: 0.75, fontSize: '13px' } }, t('description')),
498
- !state.writable ? React.createElement('div', { style: { color: '#c00', fontSize: '13px', marginBottom: '8px' } }, t('readOnly')) : null,
539
+ { style: pageStyle },
540
+ !state.available ? React.createElement('p', { style: labelStyle }, t('unavailable')) : null,
541
+ !state.writable ? React.createElement('p', { style: { color: '#c00', fontSize: '13px', marginBottom: '8px' } }, t('readOnly')) : null,
499
542
  React.createElement('div', { style: gridStyle }, PRIMARY_FIELDS.map(renderField)),
500
543
  React.createElement(
501
544
  'details',
@@ -506,22 +549,17 @@ window.__ModuleLoader__.load({
506
549
  React.createElement('footer', { style: footerStyle },
507
550
  React.createElement('button', {
508
551
  style: buttonStyle,
509
- disabled: !state.dirty || state.invalid || state.saving,
552
+ disabled: !state.writable || !state.dirty || state.invalid || state.saving,
510
553
  onClick: props.save,
511
554
  }, state.saving ? t('saving') : t('save')),
512
- React.createElement('button', {
513
- style: buttonStyle,
514
- disabled: !state.dirty || state.saving,
515
- onClick: props.discard,
516
- }, t('discard')),
517
555
  state.failed ? React.createElement('span', { style: { color: '#c00', fontSize: '13px' } }, t('failed')) : null,
518
556
  ),
519
557
  );
520
558
  }
521
559
 
522
- // ------------------------------------------------------------- plugin
560
+ // ------------------------------------------------------------ plugin
523
561
  var name = 'dsh-github-router';
524
- var inject = ['slots', 'locale', 'settingsScope', 'connection'];
562
+ var inject = ['slots', 'locale', 'configForms'];
525
563
  function apply(ctx) {
526
564
  console.info('[dsh-github-router] client bundle loaded');
527
565
  try {
@@ -529,18 +567,29 @@ window.__ModuleLoader__.load({
529
567
  ctx.effect(function () {
530
568
  ctx.locale.register(NS, { zh: zhDict, en: enDict });
531
569
  }, 'dsh-github-router: locale dictionaries');
532
- var scope = ctx.settingsScope.bind({ namespace: NAMESPACE });
533
- var mirror = typeof ctx.settingsScope.describe === 'function' ? ctx.settingsScope.describe() : null;
534
- var form = new GithubRouterForm(scope, mirror);
535
- ctx.slots.inject('settings.plugin.item', function* () {
536
- yield ctx.slots.register({
537
- name: 'settings.plugin.item',
538
- key: NAMESPACE,
539
- locale: NS,
540
- inject: function () { return form.inject(t); },
541
- }, GithubRouterCard);
542
- });
543
- console.info('[dsh-github-router] settings card registered');
570
+ // The shared form for this Host entry, plus the deployment-wide describe
571
+ // face the secret-slot state is read from.
572
+ var scope = ctx.configForms.get(NAMESPACE);
573
+ var describe = typeof ctx.configForms.describe === 'function' ? ctx.configForms.describe() : null;
574
+ var form = new GithubRouterForm(scope, describe);
575
+ ctx.effect(function () {
576
+ return function () { form.dispose(); };
577
+ }, 'dsh-github-router: form subscriptions');
578
+ // The page exists while the Host serves this namespace, so a
579
+ // deployment whose host half is missing shows no trace of it.
580
+ ctx.effect(function () {
581
+ return ctx.configForms.whileServed([NAMESPACE], function () {
582
+ return ctx.slots.inject('plugins.row.config', function () {
583
+ return ctx.slots.register({
584
+ name: 'plugins.row.config',
585
+ key: ROW_CONFIG_KEY,
586
+ locale: NS,
587
+ inject: function () { return form.inject(); },
588
+ }, GithubRouterCard);
589
+ });
590
+ });
591
+ }, 'dsh-github-router: settings page');
592
+ console.info('[dsh-github-router] settings page registered');
544
593
  } catch (error) {
545
594
  console.error('[dsh-github-router] client apply failed', error);
546
595
  }