dsh-molbio-panel 0.1.5 → 0.1.6

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 (2) hide show
  1. package/lib/client.js +198 -1
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -8517,6 +8517,44 @@ window.__ModuleLoader__.load({
8517
8517
  return undefined;
8518
8518
  }
8519
8519
 
8520
+ /** The credential reference the PubMed tools read first. */
8521
+ const NCBI_KEY_REF = 'NCBI_API_KEY';
8522
+
8523
+ /**
8524
+ * Whether a Plugins-page detail subject is this package's own entry.
8525
+ *
8526
+ * Bundles and rows both expose `pkg.name`; the key section is offered on the
8527
+ * bundle page and on every row page the bundle declares, and on nothing else.
8528
+ */
8529
+ function isMolbioDetailSubject(subject) {
8530
+ if (subject === null || typeof subject !== 'object') return false;
8531
+ if (subject.kind !== 'bundle' && subject.kind !== 'row') return false;
8532
+ const pkg = subject.pkg;
8533
+ return pkg !== null && typeof pkg === 'object' && pkg.name === 'dsh-molbio-tools';
8534
+ }
8535
+
8536
+ /**
8537
+ * The host failure message carried by a Remote result, or '' on success.
8538
+ * Remote calls answer `{ ok: true, value }` or `{ ok: false, error }`.
8539
+ */
8540
+ function remoteFailure(response) {
8541
+ if (response === null || typeof response !== 'object' || response.ok !== false) return '';
8542
+ const message = response.error?.message;
8543
+ return typeof message === 'string' && message !== '' ? message : 'the host refused the request';
8544
+ }
8545
+
8546
+ /** One reference's view from a `credentials.describe` response (never the value). */
8547
+ function credentialInfo(response, ref) {
8548
+ const view = response !== null && typeof response === 'object' && response.ok !== false
8549
+ ? response.value?.[ref]
8550
+ : undefined;
8551
+ return {
8552
+ configured: view?.configured === true,
8553
+ writable: view?.writable !== false,
8554
+ ...typeof view?.source === 'string' && view.source !== '' ? { source: view.source } : {},
8555
+ };
8556
+ }
8557
+
8520
8558
  exports.PLASMID_EXTENSIONS = PLASMID_EXTENSIONS;
8521
8559
  exports.ALIGNMENT_EXTENSIONS = ALIGNMENT_EXTENSIONS;
8522
8560
  exports.extensionOf = extensionOf;
@@ -8541,6 +8579,10 @@ window.__ModuleLoader__.load({
8541
8579
  exports.paperFields = paperFields;
8542
8580
  exports.paperSummary = paperSummary;
8543
8581
  exports.paperLink = paperLink;
8582
+ exports.NCBI_KEY_REF = NCBI_KEY_REF;
8583
+ exports.isMolbioDetailSubject = isMolbioDetailSubject;
8584
+ exports.remoteFailure = remoteFailure;
8585
+ exports.credentialInfo = credentialInfo;
8544
8586
  return exports;
8545
8587
  };
8546
8588
  __molbio_modules["build/client-entry.mjs"] = () => {
@@ -8577,7 +8619,7 @@ window.__ModuleLoader__.load({
8577
8619
  * inside the factory would also have to be torn down by hand on unload.
8578
8620
  */
8579
8621
  const { createElement: h, useEffect, useMemo, useRef, useState } = __ext0;
8580
- const { LIBRARY_FILE, childPath, classifyEntry, decodeWorkspaceBytes, filterPapers, isOpenable, libraryTags, logoSvg, mapCardSummary, mapCardView, paperFields, paperLink, paperSummary, parseAlignmentFile, parseLibrary, parsePlasmidFile, plasmidSvg, readWorkspaceText, sortEntries } = __molbio_require("build/panel-core.mjs");
8622
+ const { LIBRARY_FILE, NCBI_KEY_REF, childPath, classifyEntry, credentialInfo, isMolbioDetailSubject, remoteFailure, decodeWorkspaceBytes, filterPapers, isOpenable, libraryTags, logoSvg, mapCardSummary, mapCardView, paperFields, paperLink, paperSummary, parseAlignmentFile, parseLibrary, parsePlasmidFile, plasmidSvg, readWorkspaceText, sortEntries } = __molbio_require("build/panel-core.mjs");
8581
8623
  /** This implementation's identity, and the key its body/title register under. */
8582
8624
  const PANEL_ID = 'dsh-molbio-tools';
8583
8625
  /** Type discriminator the tab is opened by. */
@@ -9015,6 +9057,142 @@ window.__ModuleLoader__.load({
9015
9057
  h('div', { style: styles.main }, ...detail));
9016
9058
  }
9017
9059
 
9060
+ /** The state of the PubMed key, read from the host's credential store. */
9061
+ async function readNcbiKey(remote) {
9062
+ if (remote?.credentials === undefined || typeof remote.credentials.describe !== 'function') {
9063
+ return { phase: 'error', configured: false, writable: false, message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' };
9064
+ }
9065
+ let response;
9066
+ try {
9067
+ response = await remote.credentials.describe([NCBI_KEY_REF]);
9068
+ } catch (error) {
9069
+ response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
9070
+ }
9071
+ const failure = remoteFailure(response);
9072
+ if (failure !== '') return { phase: 'error', configured: false, writable: false, message: failure };
9073
+ return { phase: 'ready', ...credentialInfo(response, NCBI_KEY_REF), message: '' };
9074
+ }
9075
+
9076
+ const keyStyles = {
9077
+ section: { borderTop: '1px solid rgba(127, 127, 127, 0.28)', marginTop: 16, paddingTop: 12, display: 'grid', gap: 8 },
9078
+ title: { margin: 0, fontSize: 14 },
9079
+ note: { margin: 0, fontSize: 12, opacity: 0.75, lineHeight: 1.5 },
9080
+ status: { margin: 0, fontSize: 12 },
9081
+ row: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
9082
+ input: { flex: '1 1 240px', minWidth: 200, padding: '4px 8px' },
9083
+ message: { margin: 0, fontSize: 12 },
9084
+ };
9085
+
9086
+ /**
9087
+ * The Plugins → installed plugin → detail section that stores the NCBI key.
9088
+ *
9089
+ * It renders only for this package's own subject (bundle page, or one of the
9090
+ * row pages the bundle declares) and returns null everywhere else, which is the
9091
+ * contract `plugins.detail.section` asks a contribution to honour. The value is
9092
+ * write-only from here: `credentials.describe` answers whether a key is
9093
+ * configured and where it came from, never the key itself.
9094
+ */
9095
+ function NcbiKeySection({ subject, remote }) {
9096
+ const [draft, setDraft] = useState('');
9097
+ const [state, setState] = useState({ phase: 'loading', configured: false, writable: true, message: '' });
9098
+ const subjectToken = isMolbioDetailSubject(subject)
9099
+ ? `${subject.kind}:${subject.pkg.name}:${subject.row?.rowId ?? ''}`
9100
+ : '';
9101
+ useEffect(() => {
9102
+ if (subjectToken === '') return undefined;
9103
+ let live = true;
9104
+ const load = () => readNcbiKey(remote).then((next) => { if (live) setState(next); });
9105
+ load();
9106
+ // Another surface may rotate or clear the same reference (Settings, the
9107
+ // file watcher); follow the host event instead of showing a stale badge.
9108
+ const off = typeof remote?.$on === 'function'
9109
+ ? remote.$on('credentials/reference-updated', (ref) => { if (ref === NCBI_KEY_REF) load(); })
9110
+ : undefined;
9111
+ return () => {
9112
+ live = false;
9113
+ if (typeof off === 'function') off();
9114
+ };
9115
+ }, [remote, subjectToken]);
9116
+ if (!isMolbioDetailSubject(subject)) return null;
9117
+ const busy = state.phase === 'saving';
9118
+ const save = async () => {
9119
+ const value = draft.trim();
9120
+ if (value === '') return;
9121
+ if (remote?.credentials === undefined || typeof remote.credentials.set !== 'function') {
9122
+ setState((previous) => ({ ...previous, phase: 'error', message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' }));
9123
+ return;
9124
+ }
9125
+ setState((previous) => ({ ...previous, phase: 'saving', message: '' }));
9126
+ let response;
9127
+ try {
9128
+ response = await remote.credentials.set(NCBI_KEY_REF, value);
9129
+ } catch (error) {
9130
+ response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
9131
+ }
9132
+ const failure = remoteFailure(response);
9133
+ setDraft('');
9134
+ const next = await readNcbiKey(remote);
9135
+ setState(failure !== ''
9136
+ ? { ...next, phase: 'error', message: failure }
9137
+ : next.phase === 'error'
9138
+ ? next
9139
+ : next.configured === true
9140
+ ? { ...next, message: 'Saved. The next PubMed call uses this key.' }
9141
+ : { ...next, phase: 'error', message: 'The host accepted the write but still reports no key configured. Restart the profile and try again.' });
9142
+ };
9143
+ const remove = async () => {
9144
+ if (remote?.credentials === undefined || typeof remote.credentials.unset !== 'function') {
9145
+ setState((previous) => ({ ...previous, phase: 'error', message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' }));
9146
+ return;
9147
+ }
9148
+ setState((previous) => ({ ...previous, phase: 'saving', message: '' }));
9149
+ let response;
9150
+ try {
9151
+ response = await remote.credentials.unset(NCBI_KEY_REF);
9152
+ } catch (error) {
9153
+ response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
9154
+ }
9155
+ const failure = remoteFailure(response);
9156
+ const next = await readNcbiKey(remote);
9157
+ setState(failure !== ''
9158
+ ? { ...next, phase: 'error', message: failure }
9159
+ : next.phase === 'error'
9160
+ ? next
9161
+ : next.configured === false
9162
+ ? { ...next, message: 'Removed. PubMed search falls back to web search.' }
9163
+ : { ...next, phase: 'error', message: 'The host still reports a key configured after Remove.' });
9164
+ };
9165
+ const status = state.phase === 'loading'
9166
+ ? 'Checking the credential store…'
9167
+ : state.phase === 'error'
9168
+ ? 'Unavailable'
9169
+ : state.configured
9170
+ ? `Configured${state.source === undefined ? '' : ` (source: ${state.source})`}`
9171
+ : 'Not configured';
9172
+ return h('section', { 'data-molbio-ncbi-key': '', style: keyStyles.section },
9173
+ h('h4', { style: keyStyles.title }, 'PubMed API key'),
9174
+ h('p', { style: keyStyles.note },
9175
+ 'Stored in the harness credential store. molbio_pubmed_search and molbio_pubmed_abstract use it for the official PubMed E-utilities API; without it they fall back to web search.'),
9176
+ h('p', { 'data-molbio-key-status': state.phase, style: keyStyles.status }, status),
9177
+ h('div', { style: keyStyles.row },
9178
+ h('input', {
9179
+ type: 'password',
9180
+ value: draft,
9181
+ placeholder: 'NCBI API key (36 characters)',
9182
+ autoComplete: 'off',
9183
+ spellCheck: false,
9184
+ 'aria-label': 'NCBI API key',
9185
+ style: keyStyles.input,
9186
+ onChange: (event) => setDraft(event.target.value),
9187
+ }),
9188
+ h('button', { type: 'button', disabled: busy || state.writable === false || draft.trim() === '', onClick: save },
9189
+ busy ? 'Saving…' : 'Save'),
9190
+ h('button', { type: 'button', disabled: busy || state.writable === false || state.configured !== true, onClick: remove }, 'Remove')),
9191
+ state.message === '' ? null : h('p', { 'data-molbio-key-message': '', role: 'status', style: keyStyles.message }, state.message),
9192
+ h('p', { style: keyStyles.note },
9193
+ 'Create a key under NCBI → Account Settings → API Key Management. Prefer this over an environment variable: the store is not inherited by shell commands.'));
9194
+ }
9195
+
9018
9196
  /** Required browser services. */
9019
9197
  const inject = ['slots', 'sidebarRightTabs', 'remote'];
9020
9198
 
@@ -9098,6 +9276,25 @@ window.__ModuleLoader__.load({
9098
9276
  key: toolName,
9099
9277
  }, MolbioMapCard)), `molbio panel: ${toolName} card`);
9100
9278
  }
9279
+ // Plugins -> the installed plugin's detail page: the one place a user can
9280
+ // store the NCBI key without editing a YAML file or exporting a variable.
9281
+ //
9282
+ // `remote.credentials` is a FIBER INJECTION, not merely a property on the
9283
+ // Remote carrier: the shipped settings pages declare it in `inject` for the
9284
+ // same reason. Without the declaration `ctx.remote.credentials` is undefined,
9285
+ // which is exactly how the first build of this section made Save a no-op.
9286
+ // The gate keeps the panel working in deployments that expose no credentials
9287
+ // namespace: the section simply never registers there.
9288
+ ctx.effect(() => {
9289
+ const gate = ctx.inject(['remote.credentials'], (keyCtx) => {
9290
+ keyCtx.effect(() => keyCtx.slots.inject('plugins.detail.section', () => keyCtx.slots.register({
9291
+ name: 'plugins.detail.section',
9292
+ id: 'molbio-ncbi-key',
9293
+ order: 100,
9294
+ }, (props) => h(NcbiKeySection, { ...props, remote: keyCtx.remote }))), 'molbio panel: NCBI key section');
9295
+ });
9296
+ return () => { gate.dispose(); };
9297
+ }, 'molbio panel: NCBI key section gate');
9101
9298
  }
9102
9299
 
9103
9300
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-molbio-panel",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Browser panel for the DeepSeek Harness Web GUI: a Molbio tab in the right sidebar that draws plasmid maps and sequence logos from workspace sequence files, using the same computation modules as the dsh-molbio-tools plugin.",
5
5
  "type": "module",
6
6
  "main": "index.mjs",