@aiwuk/team 0.0.0-stage → 5.0.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/LICENSE +21 -0
  3. package/README.md +149 -2
  4. package/README.zh-CN.md +150 -0
  5. package/access.js +93 -0
  6. package/activity-summary.js +36 -0
  7. package/allocation.js +34 -0
  8. package/board-dispatch.js +141 -0
  9. package/brand.js +41 -0
  10. package/client/00-core-zh.js +284 -0
  11. package/client/01-i18n-en.js +265 -0
  12. package/client/02-styles.js +336 -0
  13. package/client/02b-stage-styles.js +65 -0
  14. package/client/03-shared.js +82 -0
  15. package/client/03b-collapsible-list.js +34 -0
  16. package/client/03c-config-panel.js +605 -0
  17. package/client/04-tool-cards.js +110 -0
  18. package/client/04a-dispatch-confirm.js +162 -0
  19. package/client/04b-more-cards.js +78 -0
  20. package/client/04c-model-picker.js +68 -0
  21. package/client/04d-confirm-dock.js +40 -0
  22. package/client/05-policy.js +217 -0
  23. package/client/06-usage.js +195 -0
  24. package/client/06b-model-source.js +67 -0
  25. package/client/07-board-parts.js +312 -0
  26. package/client/07a-role-theme.js +25 -0
  27. package/client/08-artifacts-report.js +375 -0
  28. package/client/08a-task-control.js +126 -0
  29. package/client/08b-stage-sprites.js +97 -0
  30. package/client/08c-stage-colors.js +55 -0
  31. package/client/08c-stage-scene.js +238 -0
  32. package/client/08d-stage-adapter.js +65 -0
  33. package/client/08e-stage-activity.js +83 -0
  34. package/client/09-team-board.js +181 -0
  35. package/client/09b-role-view.js +95 -0
  36. package/client/09c-model-tabs.js +20 -0
  37. package/client/10-overview.js +364 -0
  38. package/client/10b-onboarding.js +121 -0
  39. package/client/11-register.js +249 -0
  40. package/client.js +5053 -0
  41. package/config-projection.js +40 -0
  42. package/config.js +411 -0
  43. package/cordis.patch.yml +110 -0
  44. package/defaults.js +222 -0
  45. package/dispatch-confirm.js +293 -0
  46. package/dispatch-routes-store.js +31 -0
  47. package/failover.js +85 -0
  48. package/file-limits.js +119 -0
  49. package/free-worker.mjs +125 -0
  50. package/http-guard.js +14 -0
  51. package/i18n/roles.en.json +23 -0
  52. package/i18n/roles.zh.json +23 -0
  53. package/i18n/server.en.json +659 -0
  54. package/i18n/server.zh.json +659 -0
  55. package/i18n.js +34 -0
  56. package/icon.svg +7 -0
  57. package/index.js +113 -0
  58. package/integrations-config.js +301 -0
  59. package/integrations-messages.js +62 -0
  60. package/integrations.js +17 -0
  61. package/lead-route.js +30 -0
  62. package/locale/en.json +6 -0
  63. package/locale/zh.json +6 -0
  64. package/long-context.js +30 -0
  65. package/managed-config-route.js +49 -0
  66. package/managed-ref.js +38 -0
  67. package/managed-resolve.js +47 -0
  68. package/member-models.js +84 -0
  69. package/member-task-control.js +242 -0
  70. package/memory-backends.js +89 -0
  71. package/memory-embed.js +70 -0
  72. package/memory-hub.js +140 -0
  73. package/memory-openviking.js +101 -0
  74. package/memory-redact.js +34 -0
  75. package/memory-store.js +270 -0
  76. package/memory-sync-queue.js +28 -0
  77. package/model-policy.js +85 -0
  78. package/model-prefs.js +104 -0
  79. package/observability-api.js +76 -0
  80. package/observability-projection.js +116 -0
  81. package/onboarding.js +47 -0
  82. package/opencode.js +74 -0
  83. package/package.json +155 -4
  84. package/plan-gate.js +92 -0
  85. package/preset-guard.js +47 -0
  86. package/request-evidence.js +41 -0
  87. package/roles.js +105 -0
  88. package/roster-defaults.js +70 -0
  89. package/route-health.js +39 -0
  90. package/runtime-observability.js +78 -0
  91. package/runtime-view.js +21 -0
  92. package/scripts/memory-cli.mjs +138 -0
  93. package/search-engines.js +119 -0
  94. package/search.js +164 -0
  95. package/session-policy.js +59 -0
  96. package/session-rotation.js +206 -0
  97. package/settings-schema.js +40 -0
  98. package/shared/managed-config.js +84 -0
  99. package/single-model.js +44 -0
  100. package/skills/project-role-theme/SKILL.md +46 -0
  101. package/task-bridge.js +32 -0
  102. package/task-control-api.js +36 -0
  103. package/task-control-audit.js +60 -0
  104. package/team.js +101 -0
  105. package/teammate-context.js +120 -0
  106. package/teammate-wait.js +18 -0
  107. package/template.js +46 -0
  108. package/theme-plugin.js +172 -0
  109. package/theme.js +115 -0
  110. package/token-discipline.js +116 -0
  111. package/token-metrics.js +37 -0
  112. package/tool-names.js +10 -0
  113. package/tool-schema.js +23 -0
  114. package/transcribe.js +104 -0
  115. package/ui.js +113 -0
  116. package/usage.js +96 -0
  117. package/workflow.js +246 -0
@@ -0,0 +1,605 @@
1
+ // @generated by shared-ui/build.mjs from shared-ui/*.js, catalog.json and locale/*.json — edit those, then run node shared-ui/build.mjs.
2
+ const aiwukConfig = (() => {
3
+ const BRAND_TEXT = {"mothership":{"en":"AIWUK Mothership","zh":"AI\u609f\u7a7a \u6bcd\u8230"}};
4
+ // Pure logic of the config panel (no DOM, no React, no host imports) so it is testable and reused verbatim in the
5
+ // browser bundle. Nothing here ever receives a secret value: secrets are described (configured / source) only.
6
+
7
+ /** Credentials `describe` accepts at most this many refs per call. */
8
+ const DESCRIBE_BATCH = 64;
9
+ const SECRET_STATES = Object.freeze(['unset', 'stored', 'env', 'managed']);
10
+
11
+ const chunk = (list, size = DESCRIBE_BATCH) => {
12
+ const out = [];
13
+ for (let i = 0; i < list.length; i += size) out.push(list.slice(i, i + size));
14
+ return out;
15
+ };
16
+
17
+ /** Relative (base-href aware) URL of an extension's values-free manifest route; mirrors managed-config-route.js. */
18
+ const managedConfigUrl = extension => `api/aiwuk.managed-config.${extension}`;
19
+
20
+ /** Remote replies are `{ok, value}` / `{ok:false, error}`; a bare value passes through. Throws on a refusal. */
21
+ function unwrapReply(reply) {
22
+ if (reply && typeof reply === 'object' && typeof reply.ok === 'boolean') {
23
+ if (!reply.ok) throw new Error(String(reply.error?.code ?? reply.error?.message ?? 'remote-error'));
24
+ return reply.value;
25
+ }
26
+ return reply;
27
+ }
28
+
29
+ /** Wire manifest → Map(ref → {locked, secret, value?}); null when the payload is not a manifest. A secret never keeps a value. */
30
+ function manifestFromWire(wire) {
31
+ if (!wire || typeof wire !== 'object' || !Array.isArray(wire.entries)) return null;
32
+ const entries = new Map();
33
+ for (const item of wire.entries) {
34
+ if (!item || typeof item.ref !== 'string') continue;
35
+ const secret = item.secret === true;
36
+ entries.set(item.ref, { locked: item.locked === true, secret, ...(!secret && typeof item.value === 'string' ? { value: item.value } : {}) });
37
+ }
38
+ return { revision: Number.isSafeInteger(wire.revision) ? wire.revision : 0, entries };
39
+ }
40
+
41
+ /** The Mothership's value of a non-secret managed ref (for display), or undefined. */
42
+ const managedValueOf = (manifest, ref) => (ref ? manifest?.entries?.get(ref)?.value : undefined);
43
+
44
+ /**
45
+ * Panel-level managed status: how many of the panel's integration fields the Mothership manages and how many it
46
+ * locks. `mode` is 'none' (self-configured), 'full' (every integration field managed) or 'partial'.
47
+ */
48
+ function managedStatus(fields, manifest) {
49
+ const refs = fields.filter(field => field.ref).map(field => field.ref);
50
+ const managed = refs.filter(ref => manifest?.entries?.has(ref));
51
+ const locked = managed.filter(ref => manifest.entries.get(ref).locked);
52
+ return { managed: managed.length, locked: locked.length, total: refs.length, mode: !managed.length ? 'none' : managed.length === refs.length ? 'full' : 'partial' };
53
+ }
54
+
55
+ /** 'locked' | 'default' (managed but editable) | 'none' for a ref. */
56
+ function lockMode(manifest, ref) {
57
+ const entry = ref ? manifest?.entries?.get(ref) : undefined;
58
+ return !entry ? 'none' : entry.locked ? 'locked' : 'default';
59
+ }
60
+
61
+ /**
62
+ * State of a secret field from the host's `describe` view (`{configured, source?, writable}`) and the lock mode.
63
+ * Locked managed refs read as managed whatever the store says; the value itself is never involved.
64
+ */
65
+ function secretState(info, mode = 'none') {
66
+ const readOnly = mode === 'locked' || info?.writable === false;
67
+ if (mode === 'locked') return { kind: 'managed', configured: info?.configured === true, editable: false, clearable: false };
68
+ const configured = info?.configured === true;
69
+ const kind = !configured ? 'unset' : info?.source === 'file' ? 'stored' : 'env';
70
+ // The environment is not writable by the product: Replace writes a stored value that then takes precedence,
71
+ // Clear only removes the stored value (it cannot unset the environment), so it is offered for stored values only.
72
+ return { kind, configured, editable: !readOnly, clearable: !readOnly && kind === 'stored' };
73
+ }
74
+
75
+ /** Text of an input for a stored settings value. */
76
+ function fieldText(field, value) {
77
+ if (value === undefined || value === null) return '';
78
+ return field.multiple && Array.isArray(value) ? value.join(',') : String(value);
79
+ }
80
+
81
+ /** Whether a (non-secret) field currently has a value, for the summary count. */
82
+ function hasValue(field, value) {
83
+ if (Array.isArray(value)) return value.length > 0;
84
+ return value !== undefined && value !== null && value !== '';
85
+ }
86
+
87
+ /** Validation key (a dictionary key under `err.`) for an edited value, or null when acceptable. */
88
+ function validateValue(field, raw) {
89
+ if (field.kind === 'number') {
90
+ const n = Number(raw);
91
+ return Number.isInteger(n) && n >= field.min && n <= field.max ? null : 'number';
92
+ }
93
+ if (field.kind === 'url') {
94
+ if (raw === '') return null;
95
+ try { const url = new URL(raw); return url.protocol === 'https:' || url.protocol === 'http:' ? null : 'url'; } catch { return 'url'; }
96
+ }
97
+ if (field.kind === 'enum' && field.multiple) return Array.isArray(raw) && raw.every(v => field.options.includes(v)) ? null : 'enum';
98
+ if (field.kind === 'enum') return raw === '' || field.options.includes(raw) ? null : 'enum';
99
+ return null;
100
+ }
101
+
102
+ /** Whether a field is visible given the current (draft over saved) values. */
103
+ const isShown = (field, current) => !field.showWhen || Object.entries(field.showWhen).every(([key, want]) => current[key] === want);
104
+
105
+ /**
106
+ * form.mutate operations for the edited fields. `values` are the saved settings, `drafts` maps field id → edited
107
+ * value (string, boolean or array). Locked managed fields are never written; an emptied field is unset so the layers
108
+ * below apply again. Returns `{ops, invalid}` where `invalid` maps field id → error key.
109
+ */
110
+ function buildOps(fields, values, drafts, manifest) {
111
+ const ops = [], invalid = {};
112
+ for (const field of fields) {
113
+ if (field.kind === 'secret' || !field.path || !Object.hasOwn(drafts, field.id)) continue;
114
+ if (lockMode(manifest, field.ref) === 'locked') continue;
115
+ let next = drafts[field.id];
116
+ if (typeof next === 'string') next = next.trim();
117
+ if (field.kind === 'number') {
118
+ if (next === '') { if (hasValue(field, values?.[field.path])) ops.push({ op: 'unset', path: [field.path] }); continue; }
119
+ const problem = validateValue(field, next);
120
+ if (problem) { invalid[field.id] = problem; continue; }
121
+ next = Number(next);
122
+ } else if (field.multiple && typeof next === 'string') {
123
+ next = next.split(',').map(v => v.trim()).filter(Boolean);
124
+ }
125
+ const problem = field.kind === 'number' ? null : validateValue(field, next);
126
+ if (problem) { invalid[field.id] = problem; continue; }
127
+ const saved = values?.[field.path];
128
+ const same = Array.isArray(next) ? Array.isArray(saved) && saved.length === next.length && saved.every((v, i) => v === next[i]) : saved === next;
129
+ if (same) continue;
130
+ if (next === '' || (Array.isArray(next) && !next.length)) { if (hasValue(field, saved)) ops.push({ op: 'unset', path: [field.path] }); continue; }
131
+ ops.push({ op: 'set', path: [field.path], value: next });
132
+ }
133
+ return { ops, invalid };
134
+ }
135
+
136
+ /**
137
+ * Summary numbers: how many integration fields (those with a ref) hold a value, and how many are managed.
138
+ * `states` maps secret ref → secretState(); `values` are saved settings.
139
+ */
140
+ function summaryCounts(fields, values, states, manifest) {
141
+ let configured = 0, total = 0, managed = 0;
142
+ for (const field of fields) {
143
+ if (!field.ref) continue;
144
+ total++;
145
+ const mode = lockMode(manifest, field.ref);
146
+ if (mode !== 'none') managed++;
147
+ const set = field.kind === 'secret' ? states?.[field.ref]?.configured === true : hasValue(field, values?.[field.path]);
148
+ if (set || mode === 'locked') configured++;
149
+ }
150
+ return { configured, total, managed };
151
+ }
152
+
153
+ /** Refs whose state the panel asks the host to describe. */
154
+ const secretRefs = fields => fields.filter(field => field.kind === 'secret').map(field => field.ref);
155
+
156
+ /**
157
+ * Describe the refs through `credentials.describe` in batches of DESCRIBE_BATCH. Returns `{ref: {configured, source?,
158
+ * writable}}`; a failed batch leaves its refs out (the panel then shows them as unknown, never as "set").
159
+ */
160
+ async function describeSecrets(credentials, refs) {
161
+ return (await describeSecretsResult(credentials, refs)).described;
162
+ }
163
+
164
+ /** One describe attempt must settle within this time; a hung call counts as a failure so the row never waits forever. */
165
+ const DESCRIBE_TIMEOUT_MS = 1200;
166
+ /** Pause before the single automatic retry of a failed describe. Worst case until "Could not check": 2 × timeout + this. */
167
+ const DESCRIBE_RETRY_DELAY_MS = 300;
168
+
169
+ /** Reject after `ms` when `work` has not settled; the timer is always cleared. */
170
+ function withTimeout(work, ms) {
171
+ return new Promise((resolve, reject) => {
172
+ const timer = setTimeout(() => reject(new Error('timeout')), ms);
173
+ Promise.resolve(work).then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Like describeSecrets, but also says whether any batch FAILED (thrown error, refusal, timeout, malformed reply, missing API).
179
+ * A ref absent from a successful reply is reported as not configured: the host answered, it just holds nothing for it.
180
+ * Refs of a failed batch are left out (unknown), never guessed.
181
+ */
182
+ async function describeSecretsResult(credentials, refs, timeoutMs = DESCRIBE_TIMEOUT_MS) {
183
+ const described = {};
184
+ let failed = false;
185
+ if (!refs.length) return { described, failed };
186
+ if (typeof credentials?.describe !== 'function') return { described, failed: true };
187
+ for (const batch of chunk(refs)) {
188
+ try {
189
+ const view = unwrapReply(await withTimeout(Promise.resolve().then(() => credentials.describe(batch)), timeoutMs));
190
+ if (!view || typeof view !== 'object') throw new Error('malformed-reply');
191
+ for (const ref of batch) {
192
+ const item = view[ref];
193
+ described[ref] = item && typeof item === 'object'
194
+ ? { configured: item.configured === true, source: item.source, writable: item.writable !== false }
195
+ : { configured: false, writable: true };
196
+ }
197
+ } catch { failed = true; /* leave this batch unknown */ }
198
+ }
199
+ return { described, failed };
200
+ }
201
+
202
+ /** Store a new value (write-only). Rejects an empty value, a locked ref and a missing API; resolves to true. */
203
+ async function writeSecret(credentials, ref, value, mode = 'none') {
204
+ if (mode === 'locked') throw new Error('locked');
205
+ if (typeof value !== 'string' || !value.trim()) throw new Error('empty');
206
+ if (typeof credentials?.set !== 'function') throw new Error('unavailable');
207
+ unwrapReply(await credentials.set(ref, value));
208
+ return true;
209
+ }
210
+
211
+ /** Remove the stored value of a ref. Rejects a locked ref and a missing API; resolves to true. */
212
+ async function clearSecret(credentials, ref, mode = 'none') {
213
+ if (mode === 'locked') throw new Error('locked');
214
+ if (typeof credentials?.unset !== 'function') throw new Error('unavailable');
215
+ unwrapReply(await credentials.unset(ref));
216
+ return true;
217
+ }
218
+ // Per-extension panel field lists (contract section E). A field is addressed by:
219
+ // path - key in the bundle's volatile DSH settings section (non-secret values, written with form.mutate)
220
+ // ref - env-style name shared with the managed manifest and the credential store (see catalog.js)
221
+ // Secret fields have no path: they are credentials (ctx.remote.credentials), never settings values.
222
+ // `enabled` is deliberately absent: on/off is DSH's own plugin switch on the Plugins page.
223
+ // Labels live in the dictionaries (locale/*.json, plus catalog.js for ref fields) under `field.<ref or id>`.
224
+ const field = (id, kind, extra = {}) => Object.freeze({ id, kind, ...extra });
225
+ const num = (id, min, max, step = 1) => field(id, 'number', { path: id, min, max, step });
226
+ const secret = ref => field(ref, 'secret', { ref });
227
+
228
+ const PANELS = Object.freeze({
229
+ autocomplete: Object.freeze({
230
+ extension: 'autocomplete', bundle: '@aiwuk/autocomplete', namespace: 'autocomplete',
231
+ fields: Object.freeze([
232
+ field('routeMode', 'enum', { path: 'routeMode', ref: 'AUTOCOMPLETE_ROUTE_MODE', options: ['session', 'fixed'] }),
233
+ field('provider', 'text', { path: 'provider', ref: 'AUTOCOMPLETE_PROVIDER', showWhen: { routeMode: 'fixed' } }),
234
+ field('model', 'text', { path: 'model', ref: 'AUTOCOMPLETE_MODEL', showWhen: { routeMode: 'fixed' } }),
235
+ num('maxSuggestions', 1, 5), num('typePauseMs', 200, 5000, 50), num('maxOutputTokens', 16, 2048),
236
+ num('timeoutMs', 1000, 120000, 500), num('maxMessages', 1, 50), num('maxChars', 500, 32000, 100),
237
+ field('mobileBar', 'switch', { path: 'mobileBar' }),
238
+ ]),
239
+ }),
240
+ memory: Object.freeze({
241
+ extension: 'memory', bundle: '@aiwuk/memory', namespace: 'global-memory',
242
+ fields: Object.freeze([
243
+ field('autoRecall', 'switch', { path: 'autoRecall' }),
244
+ num('injectChars', 200, 6000, 100), num('recallK', 1, 20),
245
+ field('memoryBackend', 'enum', { path: 'memoryBackend', ref: 'MEMORY_BACKEND', options: ['local', 'openviking'] }),
246
+ field('memoryUrl', 'url', { path: 'memoryUrl', ref: 'OPENVIKING_URL' }),
247
+ field('embeddingBaseUrl', 'url', { path: 'embeddingBaseUrl', ref: 'EMBEDDING_BASE_URL' }),
248
+ field('embeddingModel', 'text', { path: 'embeddingModel', ref: 'EMBEDDING_MODEL' }),
249
+ secret('OPENVIKING_API_KEY'), secret('EMBEDDING_API_KEY'),
250
+ ]),
251
+ }),
252
+ search: Object.freeze({
253
+ extension: 'search', bundle: '@aiwuk/search', namespace: 'omni-search',
254
+ // Selects this extension as DSH's web search provider: `searchProvider` on the host `web` entry (another namespace).
255
+ // Unsetting the user layer restores the host default (`deepseek-official` in the shipped base).
256
+ hostSelection: Object.freeze({ id: 'webSearch', entry: 'web', path: 'searchProvider', value: 'aiwuk-search' }),
257
+ fields: Object.freeze([
258
+ field('engines', 'enum', { path: 'engines', ref: 'OMNI_SEARCH_ENGINES', options: ['tavily', 'brave', 'searxng', 'custom'], multiple: true }),
259
+ field('fetchEngines', 'enum', { path: 'fetchEngines', ref: 'OMNI_FETCH_ENGINES', options: ['jina', 'tavily', 'custom'], multiple: true }),
260
+ field('baseUrl', 'url', { path: 'baseUrl', ref: 'OMNI_SEARCH_BASE_URL' }),
261
+ field('searxngUrl', 'url', { path: 'searxngUrl', ref: 'SEARXNG_URL' }),
262
+ field('audioBaseUrl', 'url', { path: 'audioBaseUrl', ref: 'OMNI_AUDIO_BASE_URL' }),
263
+ secret('OMNI_SEARCH_API_KEY'), secret('TAVILY_API_KEY'), secret('BRAVE_API_KEY'),
264
+ secret('SEARXNG_API_KEY'), secret('JINA_API_KEY'), secret('OMNI_AUDIO_API_KEY'),
265
+ ]),
266
+ }),
267
+ // The team bundle has no editable integration fields: its page summarizes the Memory and Search panels.
268
+ team: Object.freeze({ extension: 'team', bundle: '@aiwuk/team', namespace: 'capability-team-ui', fields: Object.freeze([]) }),
269
+ });
270
+ const DICTIONARIES = {"en":{"lock.locked":"Managed by {mothership}","lock.default":"Default from Mothership","lock.hint":"Change it in the Mothership","value.managed":"Managed","value.default":"Using Mothership default","secret.unset":"Not set","secret.stored":"Set (stored)","secret.env":"Set (environment)","secret.managed":"Managed by {mothership}","secret.unknown":"Checking...","secret.failed":"Could not check","secret.note.saved":"Saved. The value is not shown again.","secret.note.cleared":"Cleared.","secret.note.failed":"The change failed. Nothing was stored.","action.set":"Set","action.replace":"Replace","action.clear":"Clear","action.confirmClear":"Confirm clear","action.save":"Save","action.cancel":"Cancel","action.retry":"Retry","state.loading":"Loading...","state.unavailable":"Settings are unavailable: make sure the plugin is enabled.","state.saved":"Saved.","state.failed":"Save failed. Reload and retry.","summary.configured":"Configured {configured}/{total}","summary.managed":"Managed {managed}","err.number":"Enter a whole number in range.","err.url":"Enter a valid http(s) address.","err.enum":"Choose a supported value.","opt.default":"Default","opt.session":"Follow session","opt.fixed":"Fixed model","opt.local":"Local","opt.openviking":"OpenViking","opt.current":"Current project","opt.none":"Whole account","opt.tavily":"Tavily","opt.brave":"Brave","opt.searxng":"SearXNG","opt.jina":"Jina","opt.custom":"Custom gateway","field.maxSuggestions":"Max suggestions","field.typePauseMs":"Typing pause (ms)","field.maxOutputTokens":"Max output tokens","field.timeoutMs":"Timeout (ms)","field.maxMessages":"Context messages","field.maxChars":"Context characters","field.mobileBar":"Touch suggestion bar","field.autoRecall":"Auto recall","field.injectChars":"Injected characters","field.recallK":"Recall count","team.integrations":"Integrations","team.memory":"Memory","team.search":"Search","team.hint":"Memory and Search keep their own settings pages under Plugins.","team.unmanaged":"Nothing is managed by the Mothership.","team.managedCount":"{count} setting(s) managed by {mothership}.","host.webSearch.label":"Use AIWUK Search for DSH web search","host.webSearch.current":"Current provider: {provider}.","host.webSearch.auto":"automatic","host.webSearch.hint":"Turn it off to restore the DSH default.","host.webSearch.unavailable":"DSH web search settings are unavailable.","host.webSearch.failed":"Could not change the web search provider.","secret.managedSet":"Configured by {mothership}","secret.managedEmpty":"Managed by {mothership} (not set there)","banner.full":"Configured by {mothership}. Nothing to set up here.","banner.partial":"Partly configured by {mothership}.","banner.detail":"{managed} of {total} settings come from the Mothership ({locked} locked). Locked settings can only be changed in the Mothership.","optional.title":"Optional settings not sent by the Mothership ({count})","optional.hint":"Leave these empty unless you want an extra engine on this machine only."},"zh":{"lock.locked":"\u7531 {mothership}\u6258\u7ba1","lock.default":"\u6bcd\u8230\u9ed8\u8ba4\u503c","lock.hint":"\u8bf7\u5728\u6bcd\u8230\u4e2d\u4fee\u6539","value.managed":"\u5df2\u6258\u7ba1","value.default":"\u4f7f\u7528\u6bcd\u8230\u9ed8\u8ba4\u503c","secret.unset":"\u672a\u8bbe\u7f6e","secret.stored":"\u5df2\u8bbe\u7f6e\uff08\u5df2\u4fdd\u5b58\uff09","secret.env":"\u5df2\u8bbe\u7f6e\uff08\u73af\u5883\u53d8\u91cf\uff09","secret.managed":"\u7531 {mothership}\u6258\u7ba1","secret.unknown":"\u68c0\u67e5\u4e2d\u2026","secret.failed":"\u65e0\u6cd5\u68c0\u67e5","secret.note.saved":"\u5df2\u4fdd\u5b58\uff0c\u503c\u4e0d\u4f1a\u518d\u6b21\u663e\u793a\u3002","secret.note.cleared":"\u5df2\u6e05\u9664\u3002","secret.note.failed":"\u64cd\u4f5c\u5931\u8d25\uff0c\u672a\u4fdd\u5b58\u4efb\u4f55\u5185\u5bb9\u3002","action.set":"\u8bbe\u7f6e","action.replace":"\u66ff\u6362","action.clear":"\u6e05\u9664","action.confirmClear":"\u786e\u8ba4\u6e05\u9664","action.save":"\u4fdd\u5b58","action.cancel":"\u53d6\u6d88","action.retry":"\u91cd\u8bd5","state.loading":"\u8bfb\u53d6\u4e2d\u2026","state.unavailable":"\u8bbe\u7f6e\u6682\u4e0d\u53ef\u7528\uff1a\u8bf7\u786e\u8ba4\u5df2\u542f\u7528\u8be5\u63d2\u4ef6\u3002","state.saved":"\u5df2\u4fdd\u5b58\u3002","state.failed":"\u4fdd\u5b58\u5931\u8d25\uff0c\u8bf7\u5237\u65b0\u540e\u91cd\u8bd5\u3002","summary.configured":"\u5df2\u914d\u7f6e {configured}/{total}","summary.managed":"\u6258\u7ba1 {managed}","err.number":"\u8bf7\u8f93\u5165\u8303\u56f4\u5185\u7684\u6574\u6570\u3002","err.url":"\u8bf7\u8f93\u5165\u6709\u6548\u7684 http(s) \u5730\u5740\u3002","err.enum":"\u8bf7\u9009\u62e9\u53d7\u652f\u6301\u7684\u503c\u3002","opt.default":"\u9ed8\u8ba4","opt.session":"\u8ddf\u968f\u4f1a\u8bdd","opt.fixed":"\u56fa\u5b9a\u6a21\u578b","opt.local":"\u672c\u5730","opt.openviking":"OpenViking","opt.current":"\u5f53\u524d\u9879\u76ee","opt.none":"\u6574\u4e2a\u8d26\u6237","opt.tavily":"Tavily","opt.brave":"Brave","opt.searxng":"SearXNG","opt.jina":"Jina","opt.custom":"\u81ea\u5b9a\u4e49\u7f51\u5173","field.maxSuggestions":"\u5019\u9009\u6570","field.typePauseMs":"\u505c\u987f\u65f6\u95f4\uff08\u6beb\u79d2\uff09","field.maxOutputTokens":"\u6700\u5927\u8f93\u51fa","field.timeoutMs":"\u8d85\u65f6\uff08\u6beb\u79d2\uff09","field.maxMessages":"\u4e0a\u4e0b\u6587\u6d88\u606f\u6570","field.maxChars":"\u4e0a\u4e0b\u6587\u5b57\u7b26\u6570","field.mobileBar":"\u89e6\u5c4f\u5efa\u8bae\u680f","field.autoRecall":"\u81ea\u52a8\u53ec\u56de","field.injectChars":"\u6ce8\u5165\u5b57\u6570","field.recallK":"\u53ec\u56de\u6761\u6570","team.integrations":"\u96c6\u6210","team.memory":"\u5168\u5c40\u8bb0\u5fc6","team.search":"\u5168\u80fd\u641c\u7d22","team.hint":"\u5168\u5c40\u8bb0\u5fc6\u4e0e\u5168\u80fd\u641c\u7d22\u7684\u8bbe\u7f6e\u9875\u4f4d\u4e8e\u201c\u63d2\u4ef6\u201d\u9875\u5bf9\u5e94\u6761\u76ee\u4e0b\u3002","team.unmanaged":"\u6ca1\u6709\u4efb\u4f55\u8bbe\u7f6e\u7531\u6bcd\u8230\u6258\u7ba1\u3002","team.managedCount":"{count} \u9879\u8bbe\u7f6e\u7531 {mothership}\u6258\u7ba1\u3002","host.webSearch.label":"\u8ba9 DSH \u7f51\u9875\u641c\u7d22\u4f7f\u7528 AI\u609f\u7a7a \u5168\u80fd\u641c\u7d22","host.webSearch.current":"\u5f53\u524d\u63d0\u4f9b\u65b9\uff1a{provider}\u3002","host.webSearch.auto":"\u81ea\u52a8\u9009\u62e9","host.webSearch.hint":"\u5173\u95ed\u540e\u6062\u590d DSH \u9ed8\u8ba4\u3002","host.webSearch.unavailable":"DSH \u7f51\u9875\u641c\u7d22\u8bbe\u7f6e\u4e0d\u53ef\u7528\u3002","host.webSearch.failed":"\u65e0\u6cd5\u66f4\u6539\u7f51\u9875\u641c\u7d22\u63d0\u4f9b\u65b9\u3002","secret.managedSet":"\u5df2\u7531 {mothership}\u914d\u7f6e","secret.managedEmpty":"\u7531 {mothership}\u6258\u7ba1\uff08\u6bcd\u8230\u672a\u8bbe\u7f6e\uff09","banner.full":"\u5df2\u901a\u8fc7 {mothership}\u914d\u7f6e\u5b8c\u6210\uff0c\u65e0\u9700\u5728\u8fd9\u91cc\u8bbe\u7f6e\u3002","banner.partial":"\u90e8\u5206\u8bbe\u7f6e\u5df2\u7531 {mothership}\u914d\u7f6e\u3002","banner.detail":"{total} \u9879\u8bbe\u7f6e\u4e2d\u6709 {managed} \u9879\u6765\u81ea\u6bcd\u8230\uff08\u5176\u4e2d {locked} \u9879\u5df2\u9501\u5b9a\uff09\u3002\u9501\u5b9a\u7684\u8bbe\u7f6e\u53ea\u80fd\u5728\u6bcd\u8230\u4e2d\u4fee\u6539\u3002","optional.title":"\u6bcd\u8230\u672a\u4e0b\u53d1\u7684\u53ef\u9009\u8bbe\u7f6e\uff08{count} \u9879\uff09","optional.hint":"\u9664\u975e\u53ea\u60f3\u5728\u8fd9\u53f0\u673a\u5668\u4e0a\u989d\u5916\u542f\u7528\u67d0\u4e2a\u5f15\u64ce\uff0c\u5426\u5219\u4fdd\u6301\u4e3a\u7a7a\u5373\u53ef\u3002"}};
271
+ const CATALOG = [{"ref":"OMNI_SEARCH_BASE_URL","extension":"search","label":{"en":"Search gateway URL","zh":"\u641c\u7d22\u7f51\u5173\u5730\u5740"},"kind":"url","secret":false,"defaultLocked":true},{"ref":"SEARXNG_URL","extension":"search","label":{"en":"SearXNG URL","zh":"SearXNG \u5730\u5740"},"kind":"url","secret":false,"defaultLocked":true},{"ref":"OMNI_SEARCH_ENGINES","extension":"search","label":{"en":"Search engines","zh":"\u641c\u7d22\u5f15\u64ce"},"kind":"enum","options":["tavily","brave","searxng","custom"],"multiple":true,"secret":false,"defaultLocked":true},{"ref":"OMNI_FETCH_ENGINES","extension":"search","label":{"en":"Fetch engines","zh":"\u6293\u53d6\u5f15\u64ce"},"kind":"enum","options":["jina","tavily","custom"],"multiple":true,"secret":false,"defaultLocked":true,"default":"jina"},{"ref":"OMNI_SEARCH_PROVIDERS_PATH","extension":"search","label":{"en":"Search gateway providers path","zh":"\u641c\u7d22\u7f51\u5173\u63d0\u4f9b\u65b9\u8def\u5f84"},"kind":"text","secret":false,"defaultLocked":true},{"ref":"OMNI_AUDIO_BASE_URL","extension":"search","label":{"en":"Speech-to-text URL","zh":"\u8bed\u97f3\u8f6c\u5199\u5730\u5740"},"kind":"url","secret":false,"defaultLocked":true},{"ref":"OMNI_SEARCH_API_KEY","extension":"search","label":{"en":"Search gateway API key","zh":"\u641c\u7d22\u7f51\u5173\u5bc6\u94a5"},"kind":"secret","secret":true,"defaultLocked":true},{"ref":"TAVILY_API_KEY","extension":"search","label":{"en":"Tavily API key","zh":"Tavily \u5bc6\u94a5"},"kind":"secret","secret":true,"defaultLocked":true},{"ref":"BRAVE_API_KEY","extension":"search","label":{"en":"Brave API key","zh":"Brave \u5bc6\u94a5"},"kind":"secret","secret":true,"defaultLocked":true},{"ref":"SEARXNG_API_KEY","extension":"search","label":{"en":"SearXNG API key","zh":"SearXNG \u5bc6\u94a5"},"kind":"secret","secret":true,"defaultLocked":true},{"ref":"JINA_API_KEY","extension":"search","label":{"en":"Jina API key","zh":"Jina \u5bc6\u94a5"},"kind":"secret","secret":true,"defaultLocked":true},{"ref":"OMNI_AUDIO_API_KEY","extension":"search","label":{"en":"Speech-to-text API key","zh":"\u8bed\u97f3\u8f6c\u5199\u5bc6\u94a5"},"kind":"secret","secret":true,"defaultLocked":true},{"ref":"EMBEDDING_BASE_URL","extension":"memory","label":{"en":"Embedding URL","zh":"\u5411\u91cf\u670d\u52a1\u5730\u5740"},"kind":"url","secret":false,"defaultLocked":true,"legacyRefs":["CPA_EMBED_BASE_URL"]},{"ref":"EMBEDDING_MODEL","extension":"memory","label":{"en":"Embedding model","zh":"\u5411\u91cf\u6a21\u578b"},"kind":"text","secret":false,"defaultLocked":true,"legacyRefs":["CPA_EMBED_MODEL"]},{"ref":"EMBEDDING_DIMENSIONS","extension":"memory","label":{"en":"Embedding dimensions","zh":"\u5411\u91cf\u7ef4\u5ea6"},"kind":"number","secret":false,"defaultLocked":true,"legacyRefs":["CPA_EMBED_DIMENSIONS"]},{"ref":"OPENVIKING_URL","extension":"memory","label":{"en":"Memory URL","zh":"\u8bb0\u5fc6\u670d\u52a1\u5730\u5740"},"kind":"url","secret":false,"defaultLocked":true},{"ref":"MEMORY_BACKEND","extension":"memory","label":{"en":"Backend","zh":"\u5b58\u50a8\u540e\u7aef"},"kind":"enum","options":["local","openviking"],"secret":false,"defaultLocked":true,"default":"local"},{"ref":"MEMORY_SHARED_RECALL","extension":"memory","label":{"en":"Shared recall scope","zh":"\u5171\u4eab\u53ec\u56de\u8303\u56f4"},"kind":"enum","options":["current","none"],"secret":false,"defaultLocked":true,"default":"current"},{"ref":"EMBEDDING_API_KEY","extension":"memory","label":{"en":"Embedding API key","zh":"\u5411\u91cf\u670d\u52a1\u5bc6\u94a5"},"kind":"secret","secret":true,"defaultLocked":true,"legacyRefs":["CPA_EMBED_API_KEY"]},{"ref":"OPENVIKING_API_KEY","extension":"memory","label":{"en":"Memory API key","zh":"\u8bb0\u5fc6\u670d\u52a1\u5bc6\u94a5"},"kind":"secret","secret":true,"defaultLocked":true},{"ref":"AUTOCOMPLETE_ROUTE_MODE","extension":"autocomplete","label":{"en":"Model routing","zh":"\u6a21\u578b\u8def\u7531"},"kind":"enum","options":["session","fixed"],"secret":false,"defaultLocked":true,"default":"session"},{"ref":"AUTOCOMPLETE_PROVIDER","extension":"autocomplete","label":{"en":"Provider","zh":"\u63d0\u4f9b\u65b9"},"kind":"text","secret":false,"defaultLocked":true},{"ref":"AUTOCOMPLETE_MODEL","extension":"autocomplete","label":{"en":"Model","zh":"\u6a21\u578b"},"kind":"text","secret":false,"defaultLocked":true}];
272
+ /** en / zh dictionaries of one panel: shared texts, catalog labels (ref.<REF>) and the Mothership name filled in. */
273
+ function panelDictionaries(panel) {
274
+ const out = {};
275
+ for (const lang of ['en', 'zh']) {
276
+ const dict = { ...DICTIONARIES[lang] };
277
+ for (const field of panel.fields) {
278
+ const entry = CATALOG.find(item => item.ref === field.ref);
279
+ if (entry) dict['ref.' + entry.ref] = entry.label[lang];
280
+ }
281
+ for (const key of Object.keys(dict)) dict[key] = dict[key].replaceAll('{mothership}', BRAND_TEXT.mothership[lang]);
282
+ out[lang] = dict;
283
+ }
284
+ return out;
285
+ }
286
+ // Data-driven React config panel for the AIWUK extensions (contract section E). React comes from the host module
287
+ // table and is passed in; nothing here imports a DSH client package. The browser bundle is produced by
288
+ // shared-ui/build.mjs, which strips the `export`/`import` keywords so this file shares one factory scope with
289
+ // config-logic.js. Secrets are shown as a STATE only: values are never fetched, rendered or logged, and the
290
+ // write-only input is cleared after every save attempt. Styling uses --dsw-* tokens only.
291
+
292
+ function createPanelKit(React) {
293
+ const h = React.createElement;
294
+ const fmt = (text, values) => String(text).replace(/\{(\w+)\}/g, (_, key) => values?.[key] ?? '');
295
+ const ui = {
296
+ col: { display: 'flex', flexDirection: 'column', gap: '8px' },
297
+ row: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px', padding: '6px 0', borderBottom: '1px solid var(--dsw-alias-border-l2)' },
298
+ text: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 },
299
+ label: { color: 'var(--dsw-alias-label-primary)', fontSize: '1em' },
300
+ muted: { color: 'var(--dsw-alias-label-secondary)', fontSize: '0.85em' },
301
+ err: { color: 'var(--dsw-alias-state-error-primary)', fontSize: '0.85em' },
302
+ input: { width: '220px', color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 'var(--dsw-radius-lg)', padding: '4px 8px', fontSize: '0.9em' },
303
+ button: { color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-layer-2)', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 'var(--dsw-radius-lg)', padding: '4px 10px', fontSize: '0.9em', cursor: 'pointer' },
304
+ primary: { color: 'var(--dsw-alias-label-primary-inverted)', background: 'var(--dsw-alias-brand-primary)', border: '1px solid var(--dsw-alias-brand-primary)' },
305
+ badge: { borderRadius: '999px', padding: '0 8px', fontSize: '0.85em', whiteSpace: 'nowrap' },
306
+ controls: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap', justifyContent: 'flex-end' },
307
+ banner: { display: 'flex', flexDirection: 'column', gap: '4px', padding: '10px 12px', borderRadius: 'var(--dsw-radius-md)', border: '1px solid var(--dsw-alias-state-success-primary)', background: 'var(--dsw-alias-state-success-tertiary, var(--dsw-alias-bg-layer-2))' },
308
+ details: { marginTop: '4px' },
309
+ summary: { cursor: 'pointer', color: 'var(--dsw-alias-label-secondary)', fontSize: '0.9em', padding: '6px 0' },
310
+ };
311
+ const badgeStyle = mode => {
312
+ const tone = mode === 'locked' ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-success-primary)';
313
+ return { ...ui.badge, color: tone, border: `1px solid ${tone}`, background: 'var(--dsw-alias-bg-layer-2)' };
314
+ };
315
+
316
+ /** Re-render on a `{getSnapshot, subscribe}` store (a ConfigForm). */
317
+ function useStoreSnapshot(store) {
318
+ const [, redraw] = React.useState(0);
319
+ React.useEffect(() => (typeof store?.subscribe === 'function' ? store.subscribe(() => redraw(n => n + 1)) : undefined), [store]);
320
+ return store?.getSnapshot?.() ?? { status: 'unavailable', value: undefined, revision: undefined, writable: false };
321
+ }
322
+
323
+ /**
324
+ * Fetch the values-free managed manifest. Resolves to `{manifest}` (null = the route is missing, an unmanaged
325
+ * install) or to `undefined` when the answer is unknown (network error, 5xx): the caller then keeps what it knew,
326
+ * so a transient failure never drops a lock.
327
+ */
328
+ async function fetchManifest(extension, services) {
329
+ try {
330
+ const fetcher = services.fetch ?? (typeof fetch === 'function' ? fetch : null);
331
+ const res = fetcher ? await fetcher(managedConfigUrl(extension), { headers: { accept: 'application/json' } }) : null;
332
+ if (res?.ok) return { manifest: manifestFromWire(await res.json()) };
333
+ return res?.status === 404 ? { manifest: null } : undefined;
334
+ } catch { return undefined; }
335
+ }
336
+
337
+ /**
338
+ * The managed manifest, refreshed when a credential changes (the Connector writes managed values there) and when the
339
+ * window regains focus. `reload()` returns the freshest known manifest so every write can re-check the lock first.
340
+ * `services` is created once per registration (a stable object), so the callback identity does not change.
341
+ */
342
+ function useManifest(extension, services) {
343
+ const [manifest, setManifest] = React.useState(null);
344
+ const known = React.useRef(null), latest = React.useRef(0);
345
+ const reload = React.useCallback(async () => {
346
+ const ticket = ++latest.current;
347
+ const answer = await fetchManifest(extension, services);
348
+ if (answer && ticket === latest.current) { known.current = answer.manifest; setManifest(answer.manifest); }
349
+ return answer ? answer.manifest : known.current;
350
+ }, [extension, services]);
351
+ React.useEffect(() => {
352
+ void reload();
353
+ const off = services.on?.('credentials/reference-updated', () => { void reload(); });
354
+ const onFocus = () => { void reload(); };
355
+ if (typeof window !== 'undefined') window.addEventListener?.('focus', onFocus);
356
+ return () => {
357
+ if (typeof off === 'function') off();
358
+ if (typeof window !== 'undefined') window.removeEventListener?.('focus', onFocus);
359
+ };
360
+ }, [reload, services.on]);
361
+ return [manifest, reload];
362
+ }
363
+
364
+ /**
365
+ * Secret states from `credentials.describe`, refreshed on `credentials/reference-updated`. Returns
366
+ * `[described, refresh, failed]`. Every row leaves "checking" in bounded time: a result from a superseded or
367
+ * unmounted run is dropped (ticket), the credentials service is read at call time (never captured stale), a failed
368
+ * run is retried once automatically, and after that `failed` is true so rows offer Retry instead of waiting forever.
369
+ */
370
+ function useSecretStates(services, refs) {
371
+ const [described, setDescribed] = React.useState({});
372
+ const [failed, setFailed] = React.useState(false);
373
+ const key = refs.join(',');
374
+ const ticket = React.useRef(0), alive = React.useRef(true), timer = React.useRef(null), retried = React.useRef(false), refsRef = React.useRef(refs);
375
+ refsRef.current = refs;
376
+ const run = React.useCallback(async () => {
377
+ const mine = ++ticket.current;
378
+ clearTimeout(timer.current);
379
+ const list = refsRef.current;
380
+ if (!list.length) return;
381
+ const result = await describeSecretsResult(services.credentials, list);
382
+ if (!alive.current || mine !== ticket.current) return; // stale: a newer run or an unmount took over
383
+ if (!result.failed) { retried.current = false; setDescribed(result.described); setFailed(false); return; }
384
+ setDescribed(prev => ({ ...prev, ...result.described }));
385
+ if (retried.current) { setFailed(true); return; }
386
+ retried.current = true;
387
+ timer.current = setTimeout(() => { if (alive.current && mine === ticket.current) void run(); }, DESCRIBE_RETRY_DELAY_MS);
388
+ }, [services]);
389
+ const reload = React.useCallback(() => { retried.current = false; setFailed(false); return run(); }, [run]);
390
+ React.useEffect(() => {
391
+ alive.current = true;
392
+ retried.current = false;
393
+ setFailed(false);
394
+ void run();
395
+ const off = services.on?.('credentials/reference-updated', ref => { if (refsRef.current.includes(ref)) void run(); });
396
+ return () => { alive.current = false; ticket.current++; clearTimeout(timer.current); if (typeof off === 'function') off(); };
397
+ }, [key, services.credentials, run]);
398
+ return [described, run, failed, reload];
399
+ }
400
+
401
+ /**
402
+ * Switch that makes this extension the host's provider for a capability (DSH web search): it writes
403
+ * `spec.path` on the host entry `spec.entry` through `ctx.configForms.get(entry)`, and unsets it to restore the
404
+ * host default. The current selection is always shown.
405
+ */
406
+ function HostSelectionRow({ spec, services, t }) {
407
+ const store = services.forms?.get?.(spec.entry) ?? null;
408
+ const snapshot = useStoreSnapshot(store);
409
+ const [note, setNote] = React.useState(null);
410
+ const [pending, setPending] = React.useState(false);
411
+ const ready = snapshot.status === 'ready' && snapshot.writable !== false && typeof store?.mutate === 'function';
412
+ const current = snapshot.value?.[spec.path];
413
+ const on = current === spec.value;
414
+ const toggle = async next => {
415
+ setNote(null); setPending(true);
416
+ let ok = false;
417
+ try { ok = await store.mutate([next ? { op: 'set', path: [spec.path], value: spec.value } : { op: 'unset', path: [spec.path] }], snapshot.revision); } catch { ok = false; }
418
+ setPending(false); setNote(ok ? 'saved' : 'failed');
419
+ };
420
+ const shown = typeof current === 'string' && current ? current : t(`host.${spec.id}.auto`);
421
+ return h(FieldRow, { id: `host.${spec.id}`, label: t(`host.${spec.id}.label`), t,
422
+ hint: snapshot.status === 'unavailable' ? t(`host.${spec.id}.unavailable`) : `${fmt(t(`host.${spec.id}.current`), { provider: shown })} ${t(`host.${spec.id}.hint`)}`,
423
+ error: note === 'failed' ? t(`host.${spec.id}.failed`) : undefined },
424
+ note === 'saved' ? h('span', { role: 'status', style: ui.muted }, t('state.saved')) : null,
425
+ h('input', { type: 'checkbox', role: 'switch', name: `host.${spec.id}`, 'data-host-selection': on ? 'on' : 'off', checked: on, disabled: !ready || pending, onChange: event => { void toggle(event.target.checked); } }));
426
+ }
427
+
428
+ function LockBadge({ mode, t }) {
429
+ if (!mode || mode === 'none') return null;
430
+ return h('span', { 'data-lock': mode, title: mode === 'locked' ? t('lock.hint') : undefined, style: badgeStyle(mode) }, t(mode === 'locked' ? 'lock.locked' : 'lock.default'));
431
+ }
432
+
433
+ function FieldRow({ id, label, hint, mode, error, children, t }) {
434
+ return h('div', { 'data-field': id, style: ui.row },
435
+ h('span', { style: ui.text }, h('span', { style: ui.label }, label), hint ? h('span', { style: ui.muted }, hint) : null, error ? h('span', { role: 'alert', style: ui.err }, error) : null),
436
+ h('span', { style: ui.controls }, h(LockBadge, { mode, t }), children));
437
+ }
438
+
439
+ /** State line plus Set / Replace / Clear. The typed value lives only in this component and is dropped after save. */
440
+ function SecretField({ field, label, info, mode, failed, t, onSet, onClear, onRetry }) {
441
+ const [typed, setTyped] = React.useState(null); // null = input hidden
442
+ const [confirming, setConfirming] = React.useState(false);
443
+ const [note, setNote] = React.useState(null);
444
+ const state = secretState(info, mode);
445
+ const save = async () => {
446
+ const value = typed;
447
+ setTyped(null); // cleared first: the value is never kept after the attempt
448
+ try { await onSet(field.ref, value); setNote('saved'); } catch { setNote('failed'); }
449
+ };
450
+ const clear = async () => { setConfirming(false); try { await onClear(field.ref); setNote('cleared'); } catch { setNote('failed'); } };
451
+ const unresolved = info === undefined && mode !== 'locked';
452
+ const phase = unresolved ? (failed ? 'failed' : 'unknown') : state.kind;
453
+ const statusText = unresolved ? t(failed ? 'secret.failed' : 'secret.unknown')
454
+ : state.kind === 'managed' ? t(info === undefined || state.configured ? 'secret.managedSet' : 'secret.managedEmpty') : t(`secret.${state.kind}`);
455
+ return h(FieldRow, { id: field.id, label, mode, t, hint: note ? t(`secret.note.${note}`) : undefined },
456
+ h('span', { 'data-secret-state': phase, style: unresolved && failed ? ui.err : ui.muted }, statusText),
457
+ unresolved && failed ? h('button', { type: 'button', 'data-action': 'retry-secret', style: ui.button, onClick: onRetry }, t('action.retry')) : null,
458
+ typed !== null ? h('input', { type: 'password', name: field.id, autoComplete: 'new-password', value: typed, style: ui.input, 'aria-label': label, onChange: event => setTyped(event.target.value) }) : null,
459
+ typed !== null ? h('button', { type: 'button', 'data-action': 'save-secret', style: { ...ui.button, ...ui.primary }, disabled: !typed.trim(), onClick: save }, t('action.save')) : null,
460
+ typed !== null ? h('button', { type: 'button', 'data-action': 'cancel-secret', style: ui.button, onClick: () => setTyped(null) }, t('action.cancel')) : null,
461
+ typed === null && !confirming && mode !== 'locked' && state.editable ? h('button', { type: 'button', 'data-action': 'set-secret', style: ui.button, onClick: () => { setNote(null); setTyped(''); } }, t(state.configured ? 'action.replace' : 'action.set')) : null,
462
+ typed === null && !confirming && state.clearable ? h('button', { type: 'button', 'data-action': 'clear-secret', style: ui.button, onClick: () => setConfirming(true) }, t('action.clear')) : null,
463
+ confirming ? h('button', { type: 'button', 'data-action': 'confirm-clear', style: { ...ui.button, ...ui.primary }, onClick: clear }, t('action.confirmClear')) : null,
464
+ confirming ? h('button', { type: 'button', 'data-action': 'cancel-clear', style: ui.button, onClick: () => setConfirming(false) }, t('action.cancel')) : null);
465
+ }
466
+
467
+ function control({ field, value, readOnly, mode, onChange, t }) {
468
+ const common = { name: field.id, disabled: readOnly, readOnly };
469
+ if (field.kind === 'switch') return h('input', { ...common, type: 'checkbox', role: 'switch', checked: value === true, onChange: event => onChange(event.target.checked) });
470
+ if (field.kind === 'enum' && field.multiple) {
471
+ const chosen = Array.isArray(value) ? value : String(value ?? '').split(',').filter(Boolean);
472
+ return h('span', { style: ui.controls }, field.options.map(option => h('label', { key: option, style: ui.muted },
473
+ h('input', { type: 'checkbox', name: `${field.id}.${option}`, disabled: readOnly, checked: chosen.includes(option),
474
+ onChange: event => onChange(event.target.checked ? [...chosen, option] : chosen.filter(v => v !== option)) }), ` ${t(`opt.${option}`)}`)));
475
+ }
476
+ if (field.kind === 'enum') return h('select', { ...common, value: value ?? '', style: ui.input, onChange: event => onChange(event.target.value) },
477
+ h('option', { value: '' }, t('opt.default')), field.options.map(option => h('option', { key: option, value: option }, t(`opt.${option}`))));
478
+ const props = { ...common, style: ui.input, value: value ?? '', title: readOnly && value ? String(value) : undefined, placeholder: mode === 'locked' ? t('value.managed') : mode === 'default' ? t('value.default') : undefined, onChange: event => onChange(event.target.value) };
479
+ return field.kind === 'number' ? h('input', { ...props, type: 'number', min: field.min, max: field.max, step: field.step }) : h('input', { ...props, type: field.kind === 'url' ? 'url' : 'text' });
480
+ }
481
+
482
+ /** "Configured by the Mothership" notice: shown when the Mothership manages any integration field of this panel. */
483
+ function ManagedBanner({ status, t }) {
484
+ if (status.mode === 'none') return null;
485
+ return h('div', { 'data-managed-banner': status.mode, role: 'note', style: ui.banner },
486
+ h('span', { style: { ...ui.label, fontWeight: 600 } }, t(status.mode === 'full' ? 'banner.full' : 'banner.partial')),
487
+ h('span', { style: ui.muted }, fmt(t('banner.detail'), status)));
488
+ }
489
+
490
+ function ConfigPanel(props) {
491
+ const { panel, view, services, t } = props;
492
+ const store = props.form ? null : (services.forms?.get?.(panel.namespace) ?? null);
493
+ const live = useStoreSnapshot(store);
494
+ const snapshot = props.form ? props.form.state : live;
495
+ const mutate = props.form ? props.form.mutate : store?.mutate?.bind(store);
496
+ const [manifest, reloadManifest] = useManifest(panel.extension, services);
497
+ const [described, refresh, describeFailed, retryDescribe] = useSecretStates(services, secretRefs(panel.fields));
498
+ // Every write re-reads the lock first: a Mothership lock applied while this page is open must win.
499
+ const freshMode = async ref => lockMode(await reloadManifest(), ref);
500
+ const [drafts, setDrafts] = React.useState({});
501
+ const [result, setResult] = React.useState(null);
502
+ const values = snapshot?.value ?? {};
503
+ const states = Object.fromEntries(Object.entries(described).map(([ref, info]) => [ref, secretState(info, lockMode(manifest, ref))]));
504
+ if (view === 'summary') {
505
+ const c = summaryCounts(panel.fields, values, states, manifest);
506
+ return `${fmt(t('summary.configured'), c)}${c.managed ? ` · ${fmt(t('summary.managed'), c)}` : ''}`;
507
+ }
508
+ const ready = snapshot?.status === 'ready';
509
+ const readOnly = !ready || snapshot.writable === false || typeof mutate !== 'function';
510
+ const current = { ...values, ...drafts };
511
+ const { ops, invalid } = buildOps(panel.fields, values, drafts, manifest);
512
+ const save = async () => {
513
+ let ok = false;
514
+ try {
515
+ // Rebuild the ops against the fresh manifest so a newly locked field is dropped, not written.
516
+ const fresh = buildOps(panel.fields, values, drafts, await reloadManifest());
517
+ ok = fresh.ops.length > 0 && Object.keys(fresh.invalid).length === 0 && await mutate(fresh.ops, snapshot.revision);
518
+ } catch { ok = false; }
519
+ setResult(ok ? 'saved' : 'failed');
520
+ if (ok) setDrafts({});
521
+ };
522
+ const labelOf = field => t(field.ref ? `ref.${field.ref}` : `field.${field.id}`);
523
+ const status = managedStatus(panel.fields, manifest);
524
+ // A locked Mothership value also decides which dependent rows show (e.g. provider/model under a fixed route).
525
+ const effective = { ...current };
526
+ for (const field of panel.fields) {
527
+ const fromHub = managedValueOf(manifest, field.ref);
528
+ if (field.path && fromHub !== undefined && lockMode(manifest, field.ref) === 'locked' && !Object.hasOwn(drafts, field.id)) effective[field.path] = fromHub;
529
+ }
530
+ const visible = panel.fields.filter(field => isShown(field, effective));
531
+ // Once the Mothership configures this extension, integration fields it does not send are optional extras.
532
+ const isOptional = field => status.mode !== 'none' && field.ref && lockMode(manifest, field.ref) === 'none';
533
+ const main = visible.filter(field => !isOptional(field)), optional = visible.filter(isOptional);
534
+ const renderField = field => {
535
+ const mode = lockMode(manifest, field.ref);
536
+ if (field.kind === 'secret') {
537
+ return h(SecretField, { key: field.id, field, label: labelOf(field), info: described[field.ref], mode, failed: describeFailed, t, onRetry: retryDescribe,
538
+ onSet: async (ref, value) => { await writeSecret(services.credentials, ref, value, await freshMode(ref)); await refresh(); },
539
+ onClear: async ref => { await clearSecret(services.credentials, ref, await freshMode(ref)); await refresh(); } });
540
+ }
541
+ const fromHub = managedValueOf(manifest, field.ref);
542
+ const shown = Object.hasOwn(drafts, field.id) ? drafts[field.id] : field.kind === 'switch' ? values[field.path]
543
+ : mode === 'locked' ? (fromHub === undefined ? '' : field.multiple ? fromHub.split(',').map(v => v.trim()).filter(Boolean) : fromHub)
544
+ : field.multiple ? values[field.path] : fieldText(field, values[field.path]);
545
+ return h(FieldRow, { key: field.id, id: field.id, label: labelOf(field), mode, t, error: invalid[field.id] ? t(`err.${invalid[field.id]}`) : undefined },
546
+ control({ field, value: shown, readOnly: readOnly || mode === 'locked', mode, t, onChange: next => { setResult(null); setDrafts(prev => ({ ...prev, [field.id]: next })); } }));
547
+ };
548
+ return h('div', { 'data-config-panel': panel.extension, style: ui.col },
549
+ panel.hostSelection && view !== 'summary' ? h(HostSelectionRow, { spec: panel.hostSelection, services, t }) : null,
550
+ !ready ? h('p', { style: ui.muted, 'data-unavailable': snapshot?.status === 'unavailable' ? '' : undefined }, t(snapshot?.status === 'loading' ? 'state.loading' : 'state.unavailable')) : null,
551
+ h(ManagedBanner, { status, t }),
552
+ main.map(renderField),
553
+ optional.length ? h('details', { 'data-optional-fields': '', style: ui.details },
554
+ h('summary', { style: ui.summary }, fmt(t('optional.title'), { count: optional.length })),
555
+ h('p', { style: ui.muted }, t('optional.hint')),
556
+ optional.map(renderField)) : null,
557
+ panel.fields.some(field => field.kind !== 'secret') ? h('div', { style: ui.controls },
558
+ result ? h('span', { role: 'status', style: result === 'saved' ? ui.muted : ui.err }, t(`state.${result}`)) : null,
559
+ h('button', { type: 'button', 'data-action': 'save', style: { ...ui.button, ...ui.primary }, disabled: readOnly || !ops.length || Object.keys(invalid).length > 0, onClick: save }, t('action.save'))) : null);
560
+ }
561
+
562
+ /** Team bundle entry: how many settings the Mothership manages, with a pointer to the Memory and Search pages. */
563
+ function IntegrationsSummary({ view, services, t }) {
564
+ const [manifest] = useManifest('team', services);
565
+ const count = manifest ? manifest.entries.size : 0;
566
+ const line = count ? fmt(t('team.managedCount'), { count }) : t('team.unmanaged');
567
+ if (view === 'summary') return `${t('team.integrations')}: ${line}`;
568
+ return h('div', { 'data-integrations-summary': '', style: ui.col },
569
+ h('span', { style: ui.label }, t('team.integrations')), h('span', { style: ui.muted }, line), h('span', { style: ui.muted }, t('team.hint')));
570
+ }
571
+
572
+ /** Contract entry point: the element for one bundle's `plugins.bundle.config` slot. */
573
+ function renderConfigPanel({ services, panel, view, form, t }) {
574
+ return h(ConfigPanel, { services, panel, view, form, t });
575
+ }
576
+
577
+ /**
578
+ * Register dictionaries and the `plugins.bundle.config` slot entry of one panel. The host services
579
+ * (`remote.credentials`, `configForms`) come from the plugin's static `inject` list; null checks keep a
580
+ * test or odd host mounting (non-secret fields then read-only).
581
+ */
582
+ function registerConfigPanel(ctx, { panel, ns, dictionaries, summaryOnly = false }) {
583
+ const services = { credentials: null, on: null, forms: null };
584
+ // A summary-only entry reuses the host plugin's own namespace (its dictionaries are merged by the caller).
585
+ if (!summaryOnly) ctx.effect?.(() => ctx.locale.register(ns, dictionaries), `${ns}: dictionaries`);
586
+ // The plugin declares `remote.credentials` and `configForms` statically in its `inject` list (a dynamic
587
+ // ctx.inject callback never fired in the real host), so both are read directly from the context here.
588
+ const remote = ctx.remote;
589
+ if (remote?.credentials) {
590
+ services.credentials = remote.credentials;
591
+ services.on = typeof remote.$on === 'function' ? (event, listener) => remote.$on(event, listener) : null;
592
+ }
593
+ if (ctx.configForms) services.forms = ctx.configForms;
594
+ if (!summaryOnly) ctx.effect?.(() => () => { services.credentials = null; services.on = null; services.forms = null; }, `${ns}: services`);
595
+ const t = ctx.locale.bind(ns);
596
+ ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({ name: 'plugins.bundle.config', key: panel.bundle, locale: ns },
597
+ props => (summaryOnly ? h(IntegrationsSummary, { view: props?.view, services, t: props?.t ?? t })
598
+ : renderConfigPanel({ services, panel, view: props?.view, form: props?.form, t: props?.t ?? t }))));
599
+ return services;
600
+ }
601
+
602
+ return { IntegrationsSummary, ConfigPanel, FieldRow, SecretField, LockBadge, renderConfigPanel, registerConfigPanel, useManifest, unwrapReply, validateValue };
603
+ }
604
+ return { createPanelKit, PANELS, panelDictionaries };
605
+ })();