@toddzheng024/dscode-desktop 0.7.34 → 0.7.36

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.
@@ -0,0 +1,50 @@
1
+ globalThis.__ModuleLoader__.load({
2
+ id: '@toddzheng024/dscode-desktop',
3
+ factory: require => {
4
+ const { createElement: h, useState } = require('react');
5
+ const packageName = '@toddzheng024/dscode-desktop';
6
+ const labels = { 'dscode-models': 'Models', 'dscode-accounts': 'Accounts', 'dscode-schedules': 'Schedules' };
7
+ function Mark() {
8
+ return h('svg', { width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
9
+ h('rect', { x: 2, y: 4, width: 20, height: 16, rx: 4 }), h('path', { d: 'm7 9 3 3-3 3m6 0h4' }));
10
+ }
11
+ function Setup() {
12
+ const steps = [
13
+ ['Enable and restart', 'Enable the DSCODE bundle in Plugins. After installing or updating, fully quit and reopen DeepSeek Harness. Closing a window is not enough.'],
14
+ ['Show agent presets', 'In Settings → General, enable Show coding view. If Agent presets is disabled in Plugins, enable it too.'],
15
+ ['Start a DSCODE session', 'Create a new session and choose DSCODE in the Agent preset picker. Existing sessions keep their original preset.'],
16
+ ['Use your existing account', 'Choose a model in the normal model picker. No second login or copied API key is required for models you already use.'],
17
+ ];
18
+ return h('section', { className: 'dscode-setup', 'aria-label': 'Start using DSCODE' },
19
+ h('h3', null, 'Start using DSCODE'),
20
+ h('ol', { className: 'dscode-steps' }, ...steps.map(([title, description]) =>
21
+ h('li', { key: title }, h('div', null, h('strong', null, title), h('p', null, description))))),
22
+ h('p', { className: 'dscode-note' }, 'Browser preview controls DSCODE’s external Chrome. The official Browser remains available for ordinary browsing.'));
23
+ }
24
+ function Settings({ pages, view }) {
25
+ const [selected, setSelected] = useState('overview');
26
+ if (view === 'summary') return h('span', null, 'Community DSCODE coding preset · Setup, additional providers and schedules');
27
+ const page = pages.find(page => page.entry.id === selected);
28
+ return h('section', { 'aria-label': 'DSCODE settings', className: 'dscode-ui dscode-settings' },
29
+ view !== 'page' && h('div', { className: 'dscode-heading' }, h(Mark), h('h2', null, 'DSCODE'), h('span', null, 'Community plugin')),
30
+ h('p', null, 'Coding workflows in Harness Desktop. Your existing models and accounts work here.'),
31
+ h('nav', { 'aria-label': 'DSCODE settings pages', className: 'dscode-tabs' },
32
+ ...[['overview', 'Get started'], ...pages.map(page => [page.entry.id, labels[page.entry.id]])].map(([id, label]) =>
33
+ h('button', { key: id, type: 'button', 'aria-pressed': selected === id, onClick: () => setSelected(id) }, label))),
34
+ page ? h(page.Component, { key: selected, ...page.entry.inject() }) : h(Setup));
35
+ }
36
+ function Activation({ onDismiss, onOpenDetails }) {
37
+ return h('section', { className: 'dscode-ui dscode-activation' }, h('h2', null, 'DSCODE enabled — restart to finish setup'), h(Setup),
38
+ h('div', { className: 'dscode-actions' },
39
+ h('button', { type: 'button', className: 'dscode-primary', onClick: onOpenDetails }, 'Open DSCODE setup'),
40
+ h('button', { type: 'button', onClick: onDismiss }, 'Close')));
41
+ }
42
+ return { inject: ['slots'], apply(ctx, { pages = [] } = {}) {
43
+ const props = () => ({ pages });
44
+ ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dscode', order: 11, label: () => 'DSCODE', inject: props }, Settings));
45
+ ctx.slots.inject('plugins.item', () => ctx.slots.register({ name: 'plugins.item', id: 'dscode', order: 90, label: () => 'DSCODE · Community', inject: props }, Settings));
46
+ ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({ name: 'plugins.bundle.config', key: packageName, inject: props }, Settings));
47
+ ctx.slots.inject('plugins.bundle.activation', () => ctx.slots.register({ name: 'plugins.bundle.activation', key: packageName }, Activation));
48
+ } };
49
+ },
50
+ });
@@ -4,6 +4,7 @@ import { apply as providerAccounts } from '../providers/desktop-host.mjs';
4
4
 
5
5
  export const inject = ['agents', 'commands'];
6
6
  export function apply(ctx) {
7
+ ctx.provide('dscodeDesktop', true);
7
8
  customSettings(ctx);
8
9
  browserPreview(ctx);
9
10
  providerAccounts(ctx);
@@ -0,0 +1,36 @@
1
+ /* Scoped to DSCODE. Tokens are supplied by Harness ui-theme (0.2.0-rc.2).
2
+ Do not target the host's hashed classes or change other plugins' controls. */
3
+ .dscode-ui { box-sizing: border-box; color: var(--dsw-alias-label-primary); font-family: inherit; font-size: 14px; line-height: 22px; min-width: 0; }
4
+ .dscode-ui *, .dscode-ui *::before, .dscode-ui *::after { box-sizing: border-box; }
5
+ .dscode-ui h2 { font-size: 16px; line-height: 24px; font-weight: 500; margin: 0; }
6
+ .dscode-ui h3, .dscode-ui legend { font-size: 14px; line-height: 22px; font-weight: 500; margin: 0; }
7
+ .dscode-ui p { margin: 0; color: var(--dsw-alias-label-tertiary); }
8
+ .dscode-ui button { cursor: pointer; min-height: 36px; padding: 6px 14px; border: .5px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-md); color: var(--dsw-alias-label-primary); background: transparent; font: inherit; }
9
+ .dscode-ui button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
10
+ .dscode-ui button:disabled { opacity: .4; cursor: default; }
11
+ .dscode-ui :is(button, input, select, textarea):focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
12
+ .dscode-ui :is(input:not([type=checkbox]), select, textarea) { min-width: 0; max-width: 100%; min-height: 36px; padding: 6px 10px; border: .5px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-md); color: var(--dsw-alias-label-primary); background: var(--dsw-alias-settings-card-fill); font: inherit; }
13
+ .dscode-ui input[type=checkbox] { accent-color: var(--dsw-alias-state-business-primary); margin-right: 8px; }
14
+ .dscode-ui label { color: var(--dsw-alias-label-secondary); font-size: 13px; }
15
+ .dscode-ui fieldset { min-width: 0; }
16
+ .dscode-ui [role=alert] { color: var(--dsw-alias-state-error-primary); overflow-wrap: anywhere; }
17
+ .dscode-ui [role=status] { font-size: 12px; color: var(--dsw-alias-label-secondary); }
18
+ .dscode-settings { padding: 8px 0; display: grid; gap: 16px; }
19
+ .dscode-heading { display: flex; align-items: center; gap: 8px; }
20
+ .dscode-heading svg { flex: none; width: 20px; height: 20px; }
21
+ .dscode-heading span { margin-left: auto; color: var(--dsw-alias-label-tertiary); font-size: 12px; }
22
+ .dscode-tabs { display: flex; flex-wrap: wrap; gap: 4px; padding-bottom: 12px; border-bottom: .5px solid var(--dsw-alias-border-l2); }
23
+ .dscode-tabs button { border: 0; padding: 6px 12px; }
24
+ .dscode-tabs button[aria-pressed=true] { background: var(--dsw-specific-sidebar-nav-item-active); }
25
+ .dscode-setup { display: grid; gap: 12px; }
26
+ .dscode-steps { list-style: none; margin: 0; padding: 0; counter-reset: setup; }
27
+ .dscode-steps li { counter-increment: setup; display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 12px; padding: 16px 0; border-bottom: .5px solid var(--dsw-alias-border-l2); }
28
+ .dscode-steps li::before { content: counter(setup); display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-secondary); font-size: 12px; }
29
+ .dscode-steps strong { display: block; font-weight: 500; margin-bottom: 4px; }
30
+ .dscode-steps p, .dscode-note { font-size: 13px; line-height: 20px; }
31
+ .dscode-activation { display: grid; gap: 16px; padding: 16px 0; }
32
+ .dscode-actions { display: flex; flex-wrap: wrap; gap: 8px; }
33
+ .dscode-ui .dscode-primary { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); border-color: transparent; }
34
+ .dscode-ui .dscode-primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
35
+
36
+ .dscode-ui .dscode-card { background: var(--dsw-alias-settings-card-fill); margin: 0; display: grid; gap: 12px; }
@@ -3,7 +3,7 @@ globalThis.__ModuleLoader__.load({
3
3
  factory: require => {
4
4
  const { createElement: h, useState, useEffect, useRef } = require('react');
5
5
  const columns = [['pending', 'Pending'], ['running', 'Running'], ['verifying', 'Verifying'], ['complete', 'Complete']];
6
- const control = { padding: '7px 10px', border: '1px solid #8886', borderRadius: 6, background: 'transparent', color: 'inherit', font: 'inherit' };
6
+ const control = { padding: '7px 10px', border: '.5px solid var(--dsw-alias-border-l3, #8886)', borderRadius: 'var(--dsw-radius-md, 6px)', background: 'transparent', color: 'inherit', font: 'inherit' };
7
7
  function Board({ execute }) {
8
8
  const [state, setState] = useState(null), [error, setError] = useState('');
9
9
  const active = useRef(false), alive = useRef(true);
@@ -15,7 +15,7 @@ globalThis.__ModuleLoader__.load({
15
15
  };
16
16
  useEffect(() => { alive.current = true; void refresh(); const timer = setInterval(refresh, 1000);
17
17
  return () => { alive.current = false; clearInterval(timer); }; }, []);
18
- const card = (task, column) => h('li', { key: task.id, style: { listStyle: 'none', border: '1px solid #8885', borderRadius: 6, padding: 10, marginTop: 8, overflowWrap: 'anywhere' } },
18
+ const card = (task, column) => h('li', { key: task.id, style: { listStyle: 'none', border: '.5px solid var(--dsw-alias-settings-card-stroke, #8885)', borderRadius: 'var(--dsw-radius-md, 6px)', padding: 10, marginTop: 8, overflowWrap: 'anywhere' } },
19
19
  h('strong', null, `${task.id} · ${task.title}`),
20
20
  h('p', { style: { margin: '6px 0', fontSize: 12 } }, `/${task.child} · ${task.priority} priority`),
21
21
  task.waiting && h('p', null, 'Waiting for the coordinator’s answer'),
@@ -25,7 +25,7 @@ globalThis.__ModuleLoader__.load({
25
25
  (task.detail || task.note || task.verification || task.worktree) && h('details', null, h('summary', null, 'Task details'),
26
26
  ...[['Scope and checks', task.detail], ['Reopened because', task.note], ['Verification evidence', task.verification], ['Worktree', task.worktree]]
27
27
  .filter(([, value]) => value).map(([label, value]) => h('div', { key: label, style: { marginTop: 8 } }, h('strong', null, label), h('p', { style: { whiteSpace: 'pre-wrap', margin: '4px 0' } }, value)))));
28
- return h('section', { 'aria-label': 'DSCODE delegation board', style: { padding: 16, height: '100%', overflow: 'auto', boxSizing: 'border-box' } },
28
+ return h('section', { className: 'dscode-ui', 'aria-label': 'DSCODE delegation board', style: { padding: 16, height: '100%', overflow: 'auto', boxSizing: 'border-box' } },
29
29
  h('h2', { style: { marginTop: 0 } }, 'Delegation board'),
30
30
  h('p', { style: { fontSize: 12 } }, 'Use /delegate <task> in the main session to coordinate work in isolated Git worktrees. The coordinator plans tasks and records verified results.'),
31
31
  h('button', { type: 'button', style: control, onClick: refresh }, 'Refresh board'),
@@ -58,7 +58,7 @@ const childLimit = owner => (owner.session.requestHeader?.()?.config ?? owner.op
58
58
  // repository there instead of pointing at a path that does not exist.
59
59
  const DOCS_DIR = fileURLToPath(new URL('../../docs/', import.meta.url));
60
60
  const DOCS_REPO = 'https://github.com/qiz029/dscode/blob/main/docs/';
61
- export const DOCS_SECTION = `To answer questions about DSCODE itself - what it supports, how a feature is configured, what a command does - treat the bundled user guides as the source of truth instead of guessing: ${existsSync(DOCS_DIR) ? `${DOCS_DIR} plus the repository root README.md (session-bridge.md, session-communication.md, session-cards.md, memory.md, exec.md, email.md, browser-use.md, plugin-hub.md, computer-use.md, skills.md, tui-commands.md, session-metrics.md, dscode-ultra.md, auto-review.md, account-login.md, opencode-go.md, custom-providers.md, demo.md, triggers.md)` : `the repository documentation at ${DOCS_REPO} plus the root README.md (session-bridge.md, session-communication.md, session-cards.md, memory.md, exec.md, email.md, browser-use.md, plugin-hub.md, computer-use.md, skills.md, tui-commands.md, session-metrics.md, dscode-ultra.md, auto-review.md, account-login.md, opencode-go.md, custom-providers.md, demo.md, triggers.md)`}. Read the relevant guide before answering rather than relying on memory of an earlier session. docs/CONTEXT-HANDOFF.md, docs/session-messaging-design.md, docs/cloud-webapp-host.md, docs/triggers-design.md, docs/verification.md, docs/maintainability.md and docs/vendored-tui-upgrade.md are internal development records, not user documentation: never quote them to a user or treat them as a specification. When the guides do not cover something, describe only what the running installation actually does and say plainly that it is not documented.`;
61
+ export const DOCS_SECTION = `To answer questions about DSCODE itself - what it supports, how a feature is configured, what a command does - treat the bundled user guides as the source of truth instead of guessing: ${existsSync(DOCS_DIR) ? `${DOCS_DIR} plus the repository root README.md (session-bridge.md, session-communication.md, session-cards.md, memory.md, exec.md, acp.md, email.md, browser-use.md, plugin-hub.md, computer-use.md, skills.md, tui-commands.md, session-metrics.md, dscode-ultra.md, auto-review.md, account-login.md, opencode-go.md, custom-providers.md, demo.md, triggers.md)` : `the repository documentation at ${DOCS_REPO} plus the root README.md (session-bridge.md, session-communication.md, session-cards.md, memory.md, exec.md, acp.md, email.md, browser-use.md, plugin-hub.md, computer-use.md, skills.md, tui-commands.md, session-metrics.md, dscode-ultra.md, auto-review.md, account-login.md, opencode-go.md, custom-providers.md, demo.md, triggers.md)`}. Read the relevant guide before answering rather than relying on memory of an earlier session. docs/CONTEXT-HANDOFF.md, docs/session-messaging-design.md, docs/cloud-webapp-host.md, docs/triggers-design.md, docs/verification.md, docs/maintainability.md and docs/vendored-tui-upgrade.md are internal development records, not user documentation: never quote them to a user or treat them as a specification. When the guides do not cover something, describe only what the running installation actually does and say plainly that it is not documented.`;
62
62
 
63
63
  export function apply(ctx, config = {}) {
64
64
  const viewer = config.desktop ? 'Delegation board in the right sidebar' : '/delegate-dashboard';
@@ -2,7 +2,7 @@ globalThis.__ModuleLoader__.load({
2
2
  id: '@toddzheng024/dscode-desktop',
3
3
  factory: require => {
4
4
  const { createElement: h, useState, useEffect, useRef } = require('react');
5
- const control = { padding: '7px 10px', border: '1px solid #8886', borderRadius: 6, background: 'transparent', color: 'inherit', font: 'inherit' };
5
+ const control = { padding: '7px 10px', border: '.5px solid var(--dsw-alias-border-l3, #8886)', borderRadius: 'var(--dsw-radius-md, 6px)', background: 'transparent', color: 'inherit', font: 'inherit' };
6
6
  function Inbox({ execute }) {
7
7
  const [state, setState] = useState(null), [mail, setMail] = useState(null), [offset, setOffset] = useState(0);
8
8
  const [error, setError] = useState(''), [notice, setNotice] = useState(''), [busy, setBusy] = useState(false), [sent, setSent] = useState(false);
@@ -49,7 +49,7 @@ globalThis.__ModuleLoader__.load({
49
49
  const value = await execute('read', { key });
50
50
  if (alive.current) { setMail(value); requestId.current = crypto.randomUUID(); }
51
51
  });
52
- return h('section', { 'aria-label': 'DSCODE email inbox', style: { padding: 16, overflow: 'auto', height: '100%', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12 } },
52
+ return h('section', { className: 'dscode-ui', 'aria-label': 'DSCODE email inbox', style: { padding: 16, overflow: 'auto', height: '100%', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12 } },
53
53
  h('strong', null, 'Email inbox'),
54
54
  h('p', { style: { margin: 0, fontSize: 12 } }, 'Shared local inbox. Reading mail does not start the agent. Add only the message you want this session to use as external context.'),
55
55
  error && h('div', { role: 'alert' }, error),
@@ -70,12 +70,12 @@ globalThis.__ModuleLoader__.load({
70
70
  try { await execute('connect-imap', { config: { ...form, port: Number(form.port) } }); }
71
71
  finally { if (alive.current) setForm(old => ({ ...old, password: '' })); }
72
72
  if (alive.current) await refresh(offset);
73
- }); } }, h('fieldset', { disabled: busy || !!state?.pending, style: { display: 'grid', gap: 8, border: '1px solid #8886', borderRadius: 6 } },
73
+ }); } }, h('fieldset', { disabled: busy || !!state?.pending, style: { display: 'grid', gap: 8, border: '.5px solid var(--dsw-alias-border-l3, #8886)', borderRadius: 'var(--dsw-radius-md, 6px)' } },
74
74
  h('legend', null, 'IMAP with an application password'),
75
75
  field('account', 'Mailbox login'), field('host', 'IMAP host'), field('port', 'TLS port'), field('mailbox', 'Mailbox folder'), field('password', 'Application password', 'password'),
76
76
  h('p', { style: { margin: 0, fontSize: 12 } }, 'Uses verified TLS and reads the selected folder without changing flags. The password is saved locally after a successful connection and never enters the conversation. Reconnect the same account and folder to update it.'),
77
77
  h('button', { style: control, type: 'submit', disabled: !form.account.trim() || !form.password }, 'Connect IMAP'))),
78
- h('fieldset', { disabled: busy || !!state?.pending, style: { display: 'grid', gap: 8, border: '1px solid #8886', borderRadius: 6 } },
78
+ h('fieldset', { disabled: busy || !!state?.pending, style: { display: 'grid', gap: 8, border: '.5px solid var(--dsw-alias-border-l3, #8886)', borderRadius: 'var(--dsw-radius-md, 6px)' } },
79
79
  h('legend', null, 'Optional Gmail OAuth'),
80
80
  h('label', null, 'Google Desktop client JSON path', h('input', { 'aria-label': 'Google Desktop client JSON path', value: path,
81
81
  onChange: event => setPath(event.target.value), style: { ...control, width: '100%', boxSizing: 'border-box' } })),
@@ -83,7 +83,7 @@ globalThis.__ModuleLoader__.load({
83
83
  h('p', { style: { margin: 0, fontSize: 12 } }, 'Authorization opens Google in your browser and requests mail read access plus label and filter management. It creates or reuses the ToAgent label and subject filter. Use your Google account settings to revoke access.'),
84
84
  button('Authorize Gmail', () => action('connect-gmail'), !state?.gmail.configured)))),
85
85
  h('div', { 'aria-label': 'Received emails', style: { display: 'grid', gap: 6, maxHeight: '26vh', overflow: 'auto' } },
86
- ...(state?.emails ?? []).map(row => h('button', { key: row.key, type: 'button', style: { ...control, textAlign: 'left', overflowWrap: 'anywhere', borderColor: mail?.key === row.key ? '#5794ff' : '#8886' },
86
+ ...(state?.emails ?? []).map(row => h('button', { key: row.key, type: 'button', style: { ...control, textAlign: 'left', overflowWrap: 'anywhere', borderColor: mail?.key === row.key ? 'var(--dsw-alias-state-business-primary)' : 'var(--dsw-alias-border-l3)' },
87
87
  disabled: busy, onClick: () => select(row.key) }, h('strong', null, row.subject), h('div', { style: { fontSize: 12 } }, `${row.from} · ${new Date(row.updatedAt).toLocaleString()}`))),
88
88
  state?.emails.length === 0 && h('p', null, 'No emails received.')),
89
89
  state && h('div', { style: { display: 'flex', gap: 8 } },
@@ -2,7 +2,7 @@ globalThis.__ModuleLoader__.load({
2
2
  id: '@toddzheng024/dscode-desktop',
3
3
  factory: require => {
4
4
  const { createElement: h, useState, useEffect, useRef } = require('react');
5
- const control = { padding: '8px 10px', border: '1px solid #8886', borderRadius: 6, background: 'transparent', color: 'inherit', font: 'inherit' };
5
+ const control = { boxSizing: 'border-box', minWidth: 0, maxWidth: '100%' };
6
6
  function Accounts({ execute }) {
7
7
  const [state, setState] = useState(null), [key, setKey] = useState(''), [busy, setBusy] = useState(false);
8
8
  const [error, setError] = useState(''), [notice, setNotice] = useState(''), [confirm, setConfirm] = useState(null);
@@ -43,27 +43,28 @@ globalThis.__ModuleLoader__.load({
43
43
  }, [state?.opencode.pending]);
44
44
  const button = (text, action, disabled = false) => h('button', { type: 'button', style: control, disabled: busy || disabled, onClick: action }, text);
45
45
  const destructive = (id, label) => button(confirm === id ? `Confirm ${label.toLowerCase()}` : label, () => confirm === id ? run(id) : setConfirm(id));
46
- return h('section', { 'aria-label': 'DSCODE account settings', style: { padding: 20, display: 'grid', gap: 16, maxWidth: 720 } },
47
- h('h2', { style: { margin: 0 } }, 'DSCODE accounts'),
46
+ return h('section', { className: 'dscode-ui', 'aria-label': 'DSCODE account settings', style: { padding: 0, display: 'grid', gap: 16, maxWidth: 720 } },
47
+ h('h2', { style: { margin: 0 } }, 'Accounts'),
48
48
  error && h('div', { role: 'alert' }, error),
49
49
  refreshError && h('div', { role: 'alert' }, refreshError),
50
50
  notice && h('div', { role: 'status', style: { whiteSpace: 'pre-wrap' } }, notice),
51
- button('Refresh account status', () => run('status')),
52
- state && h('fieldset', { disabled: busy, style: { border: '1px solid #8886', borderRadius: 8, padding: 16 } },
53
- h('legend', null, 'DSCODE OpenRouter'),
51
+ h('div', { className: 'dscode-actions' }, button('Refresh account status', () => run('status'))),
52
+ state && h('fieldset', { className: 'dscode-card', disabled: busy, style: { border: '.5px solid var(--dsw-alias-settings-card-stroke)', borderRadius: 'var(--dsw-radius-xl)', padding: 16 } },
53
+ h('h3', null, 'DSCODE OpenRouter'),
54
54
  h('p', null, state.openrouter.configured ? `Configured (${state.openrouter.source ?? 'saved'})` : 'No API key saved'),
55
- h('p', null, 'The key is shared with DSCODE terminal sessions and native routes using the same credential. Select DSCODE OpenRouter in the model picker for DSCODE reasoning controls.'),
55
+ h('p', null, 'Uses your existing Desktop key. Choose DSCODE OpenRouter in the model picker for DSCODE reasoning controls.'),
56
+ confirm === 'remove-openrouter' && h('p', { role: 'alert' }, 'Removal clears the Desktop key and any legacy terminal copy. Environment keys are unchanged.'),
56
57
  h('label', null, 'OpenRouter API key', h('input', { 'aria-label': 'OpenRouter API key', type: 'password', autoComplete: 'new-password', style: { ...control, display: 'block', width: '100%', boxSizing: 'border-box' },
57
58
  value: key, disabled: !state.openrouter.writable, onChange: event => { setKey(event.target.value); setConfirm(null); } })),
58
59
  h('div', { style: { display: 'flex', gap: 8, marginTop: 12 } },
59
60
  button('Save OpenRouter key', () => run('save-openrouter', { key }), !state.openrouter.writable || !key.trim()),
60
61
  state.openrouter.configured && state.openrouter.writable && destructive('remove-openrouter', 'Remove OpenRouter key'))),
61
- state && h('fieldset', { disabled: busy, style: { border: '1px solid #8886', borderRadius: 8, padding: 16 } },
62
- h('legend', null, 'Grok'),
62
+ state && h('fieldset', { className: 'dscode-card', disabled: busy, style: { border: '.5px solid var(--dsw-alias-settings-card-stroke)', borderRadius: 'var(--dsw-radius-xl)', padding: 16 } },
63
+ h('h3', null, 'Grok'),
63
64
  h('p', null, state.grok.configured ? 'Local Grok CLI login available' : 'No usable local Grok CLI login'),
64
65
  h('p', null, 'Run grok login in a terminal, then refresh this status. DSCODE reads ~/.grok/auth.json without changing or refreshing its tokens.')),
65
- state && h('fieldset', { disabled: busy, style: { border: '1px solid #8886', borderRadius: 8, padding: 16 } },
66
- h('legend', null, 'OpenCode Go'),
66
+ state && h('fieldset', { className: 'dscode-card', disabled: busy, style: { border: '.5px solid var(--dsw-alias-settings-card-stroke)', borderRadius: 'var(--dsw-radius-xl)', padding: 16 } },
67
+ h('h3', null, 'OpenCode Go'),
67
68
  h('p', { style: { whiteSpace: 'pre-wrap' } }, state.opencode.lines.join('\n')),
68
69
  h('p', null, 'Sign in opens the OpenCode consent page in your browser. It identifies the OpenCode CLI client used for this login.'),
69
70
  h('div', { style: { display: 'flex', gap: 8 } },
@@ -71,14 +72,20 @@ globalThis.__ModuleLoader__.load({
71
72
  state.opencode.pending && button('Cancel OpenCode login', () => run('cancel-opencode')),
72
73
  state.opencode.configured && destructive('logout-opencode', 'Sign out of OpenCode Go'))));
73
74
  }
74
- return { inject: ['slots', 'connection'], apply(ctx) {
75
- ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dscode-accounts', order: 12, label: () => 'DSCODE accounts',
75
+ return {
76
+ inject: ['slots', 'connection'],
77
+ settingsPage(ctx) { return { entry: { name: 'settings.section', id: 'dscode-accounts', order: 12, label: () => 'Accounts',
76
78
  inject: () => ({ execute: async (action, args = {}) => {
77
79
  const response = await ctx.connection.rpc.call('/api', 'dscode-accounts', { action, ...args });
78
80
  if (!response.ok) throw Error(response.error.message);
79
81
  return response.value;
80
82
  } }),
81
- }, Accounts));
82
- } };
83
+ }, Component: Accounts }; },
84
+ apply(ctx, options = {}) {
85
+ if (options.settings === false) return;
86
+ const page = this.settingsPage(ctx);
87
+ ctx.slots.inject('settings.section', () => ctx.slots.register(page.entry, page.Component));
88
+ },
89
+ };
83
90
  },
84
91
  });
@@ -5,7 +5,7 @@ globalThis.__ModuleLoader__.load({
5
5
  const money = (cost, partial = false) => cost === null || partial && cost === 0 ? 'Unknown' : `$${cost.toFixed(4)}${partial ? '+' : ''}`;
6
6
  const percent = value => value === null ? 'Unknown' : `${value.toFixed(1)}%`;
7
7
  const speed = value => value === null ? 'Unknown' : `${value.toFixed(1)} tokens/s`;
8
- const control = { padding: '7px 10px', border: '1px solid #8886', borderRadius: 6, background: 'transparent', color: 'inherit', font: 'inherit' };
8
+ const control = { padding: '7px 10px', border: '.5px solid var(--dsw-alias-border-l3, #8886)', borderRadius: 'var(--dsw-radius-md, 6px)', background: 'transparent', color: 'inherit', font: 'inherit' };
9
9
  function Usage({ execute }) {
10
10
  const [state, setState] = useState(null), [error, setError] = useState('');
11
11
  const active = useRef(false), alive = useRef(true);
@@ -17,9 +17,9 @@ globalThis.__ModuleLoader__.load({
17
17
  };
18
18
  useEffect(() => { alive.current = true; void refresh(); const timer = setInterval(refresh, 1000);
19
19
  return () => { alive.current = false; clearInterval(timer); }; }, []);
20
- const fact = (label, value) => h('div', { key: label, style: { padding: 10, border: '1px solid #8884', borderRadius: 6 } },
20
+ const fact = (label, value) => h('div', { key: label, style: { padding: 10, border: '.5px solid var(--dsw-alias-border-l2, #8884)', borderRadius: 'var(--dsw-radius-md, 6px)' } },
21
21
  h('dt', { style: { fontSize: 12, marginBottom: 5 } }, label), h('dd', { style: { margin: 0, fontVariantNumeric: 'tabular-nums' } }, value));
22
- return h('section', { 'aria-label': 'DSCODE session usage', style: { padding: 16, height: '100%', overflow: 'auto', boxSizing: 'border-box' } },
22
+ return h('section', { className: 'dscode-ui', 'aria-label': 'DSCODE session usage', style: { padding: 16, height: '100%', overflow: 'auto', boxSizing: 'border-box' } },
23
23
  h('h2', { style: { marginTop: 0 } }, 'Session usage'),
24
24
  h('p', { style: { fontSize: 12 } }, 'Recorded model usage for this DSCODE session, including attributed child and background calls. Costs are estimates, not an invoice.'),
25
25
  h('button', { type: 'button', style: control, onClick: refresh }, 'Refresh usage'),
@@ -2,7 +2,7 @@ globalThis.__ModuleLoader__.load({
2
2
  id: '@toddzheng024/dscode-desktop',
3
3
  factory: require => {
4
4
  const { createElement: h, useState, useEffect, useRef } = require('react');
5
- const control = { padding: '8px 10px', border: '1px solid #8886', borderRadius: 6, background: 'transparent', color: 'inherit', font: 'inherit', boxSizing: 'border-box', minWidth: 0, maxWidth: '100%' };
5
+ const control = { boxSizing: 'border-box', minWidth: 0, maxWidth: '100%' };
6
6
  const row = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
7
7
  const initial = () => ({ id: '', prompt: '', objective: '', kind: 'interval', seconds: '3600', cron: '0 9 * * 1-5', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
8
8
  command: '[]', scriptMode: 'poll', persistent: true, permission: 'read-only', enabled: true });
@@ -91,8 +91,8 @@ globalThis.__ModuleLoader__.load({
91
91
  // the user runs another task or changes workspace before retrying.
92
92
  await refresh(); eventKeys.current.delete(identity);
93
93
  });
94
- return h('section', { 'aria-label': 'DSCODE scheduling settings', style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', gap: 16, padding: 20, maxWidth: 960, minWidth: 0, boxSizing: 'border-box', overflowWrap: 'anywhere' } },
95
- h('h2', { style: { margin: 0 } }, 'DSCODE schedules'),
94
+ return h('section', { className: 'dscode-ui', 'aria-label': 'DSCODE scheduling settings', style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', gap: 16, padding: 0, maxWidth: 960, minWidth: 0, boxSizing: 'border-box', overflowWrap: 'anywhere' } },
95
+ h('h2', { style: { margin: 0 } }, 'Schedules'),
96
96
  h('p', null, 'Enabling delivery runs due tasks for every registered project and remembers this choice when Desktop opens again. Keep the application open. Stopping delivery interrupts active tasks and script sources; pending jobs stay queued.'),
97
97
  error && h('div', { role: 'alert', tabIndex: -1, ref: actionError }, error),
98
98
  refreshError && h('div', { role: 'alert' }, refreshError),
@@ -122,8 +122,8 @@ globalThis.__ModuleLoader__.load({
122
122
  button(`Read ${item.id} source log`, () => run(async () => { const result = await execute('source', { sessionId: selected.current, args: { action: 'logs', trigger_id: item.id } }); if (alive.current) setLog(result.log || 'No source output recorded.'); }))))),
123
123
  log && h('pre', { 'aria-label': 'Source log', style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, log),
124
124
  !workspace.definitions.length && h('p', null, 'No tasks in this workspace.'),
125
- h('fieldset', { disabled: busy || !workspace.writable, style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', minWidth: 0, gap: 12, border: '1px solid #8886', borderRadius: 8, padding: 16 } },
126
- h('legend', null, editing ? `Edit ${editing.id}` : 'Create task'), field('Task ID', 'id'),
125
+ h('fieldset', { className: 'dscode-card', disabled: busy || !workspace.writable, style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', minWidth: 0, gap: 12, border: '.5px solid var(--dsw-alias-settings-card-stroke)', borderRadius: 'var(--dsw-radius-xl)', padding: 16 } },
126
+ h('h3', null, editing ? `Edit ${editing.id}` : 'Create task'), field('Task ID', 'id'),
127
127
  h('label', null, 'Task instructions', h('textarea', { 'aria-label': 'Task instructions', style: { ...control, display: 'block', width: '100%', minHeight: 90 }, value: draft.prompt,
128
128
  onChange: event => setDraft(previous => ({ ...previous, prompt: event.target.value })) })),
129
129
  field('Goal (defaults to task instructions)', 'objective'),
@@ -142,14 +142,20 @@ globalThis.__ModuleLoader__.load({
142
142
  job.state === 'pending' && button(`Cancel ${job.triggerId} job`, () => mutate('jobs', { action: 'cancel', job_id: job.id }), !workspace.writable))),
143
143
  !workspace.jobs.length && h('p', null, 'No jobs in this workspace.')));
144
144
  }
145
- return { inject: ['slots', 'connection'], apply(ctx) {
146
- ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dscode-schedules', order: 13, label: () => 'DSCODE schedules',
145
+ return {
146
+ inject: ['slots', 'connection'],
147
+ settingsPage(ctx) { return { entry: { name: 'settings.section', id: 'dscode-schedules', order: 13, label: () => 'Schedules',
147
148
  inject: () => ({ execute: async (action, args = {}) => {
148
149
  const response = await ctx.connection.rpc.call('/api', 'dscode-triggers', { action, ...args });
149
150
  if (!response.ok) throw Error(response.error.message);
150
151
  return response.value;
151
152
  } }),
152
- }, Schedules));
153
- } };
153
+ }, Component: Schedules }; },
154
+ apply(ctx, options = {}) {
155
+ if (options.settings === false) return;
156
+ const page = this.settingsPage(ctx);
157
+ ctx.slots.inject('settings.section', () => ctx.slots.register(page.entry, page.Component));
158
+ },
159
+ };
154
160
  },
155
161
  });
@@ -1,82 +0,0 @@
1
- import { z } from 'zod';
2
- import { npmPackageNameSchema, exactSemverSchema, pluginRecordSchema, registrySearchResponseSchema } from '@dsh-plugin-hub/schemas';
3
- import semver from 'semver';
4
-
5
- export const hubApi = 'https://api.dshpluginhub.ai/api/v1';
6
- export const hubWebsite = 'https://dshpluginhub.ai';
7
- const searchInput = z.object({ query: z.string().max(300).default(''), category: z.string().max(60).default(''), cursor: z.string().max(2000).optional(), page: z.number().int().min(1).max(100000).default(1) }).strict();
8
- const categoriesSchema = z.object({ items: z.array(z.object({ name: z.string().min(1).max(60), displayName: z.string().max(120),
9
- displayNameZh: z.string().max(120).optional(), description: z.string().max(2000).optional(), descriptionZh: z.string().max(2000).optional(), count: z.number().int().nonnegative() })).max(100) });
10
-
11
- /** Read-only access to our public Hub. Never executes catalog-provided commands. */
12
- export class HubCatalog {
13
- constructor({ fetch: fetcher = globalThis.fetch, timeoutMs = 15000, maxBytes = 4 * 1024 * 1024 } = {}) {
14
- this.fetch = fetcher; this.timeoutMs = timeoutMs; this.maxBytes = maxBytes;
15
- }
16
- async get(path, schema, signal) {
17
- const response = await this.fetch(`${hubApi}${path}`, { headers: { accept: 'application/json' }, redirect: 'error',
18
- signal: AbortSignal.any([AbortSignal.timeout(this.timeoutMs), ...(signal ? [signal] : [])]) });
19
- if (!response.ok) { await response.body?.cancel(); throw Error(`Plugin Hub request failed (HTTP ${response.status}).`); }
20
- const chunks = []; let size = 0;
21
- const reader = response.body?.getReader();
22
- if (!reader) throw Error('Plugin Hub returned an empty response.');
23
- try {
24
- while (true) {
25
- const { done, value } = await reader.read(); if (done) break;
26
- size += value.byteLength;
27
- if (size > this.maxBytes) throw Error('Plugin Hub response is too large.');
28
- chunks.push(value);
29
- }
30
- } finally { await reader.cancel(); reader.releaseLock(); }
31
- let payload; try { payload = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { throw Error('Plugin Hub returned invalid JSON.'); }
32
- const parsed = schema.safeParse(payload);
33
- if (!parsed.success) throw Error('Plugin Hub returned unsupported catalog data.');
34
- return parsed.data;
35
- }
36
- categories(signal) { return this.get('/categories', categoriesSchema, signal); }
37
- async search(input = {}, signal) {
38
- const { query, category, cursor, page } = searchInput.parse(input);
39
- const params = new URLSearchParams({ q: query.trim(), limit: '20' });
40
- if (category) params.set('category', category);
41
- if (cursor) params.set('cursor', cursor);
42
- else params.set('page', String(page));
43
- const result = await this.get(`/packages?${params}`, registrySearchResponseSchema, signal);
44
- return { ...result, nextPage: !cursor && typeof result.total === 'number' && result.items.length > 0 && page * 20 < result.total ? page + 1 : null };
45
- }
46
- async package(packageName, signal) {
47
- return this.get(`/packages/resolve?name=${encodeURIComponent(npmPackageNameSchema.parse(packageName))}`, pluginRecordSchema, signal);
48
- }
49
- }
50
-
51
- /** Hub metadata is a preflight hint. Official pluginManager remains the installation authority. */
52
- export function desktopCandidate(record, environment, version = record.latestVersion) {
53
- exactSemverSchema.parse(version);
54
- const release = record.versions.find(item => item.version === version);
55
- const reasons = [];
56
- if (record.deprecated) reasons.push('This package is deprecated.');
57
- if (!release) return { version, spec: null, reasons: [...reasons, 'This version is absent from the Hub.'] };
58
- if (release.yanked) reasons.push('This version was withdrawn.');
59
- const source = release.source;
60
- const sameIdentity = release.manifest.name === record.packageName && release.manifest.version === version &&
61
- source.kind === 'npm' && source.packageName === record.packageName && source.version === version;
62
- if (!sameIdentity) reasons.push('Desktop installation requires matching npm package and version metadata.');
63
- const { dsh, node, platforms, surfaces } = release.compatibility;
64
- if (!surfaces.some(value => ['desktop', 'web', 'any'].includes(value))) reasons.push('This version does not support the Desktop interface.');
65
- if (platforms.length && !platforms.includes(environment.platform)) reasons.push(`This version does not support ${environment.platform}.`);
66
- if (!semver.validRange(dsh) || !semver.satisfies(environment.runtime, dsh, { includePrerelease: true })) reasons.push(`Requires DSH ${dsh}; this Desktop uses ${environment.runtime}.`);
67
- if (node && (!semver.validRange(node) || !semver.satisfies(environment.node, node))) reasons.push(`Requires Node ${node}; this Host uses ${environment.node}.`);
68
- // Construct the exact spec ourselves; never pass source.installSpec or tarballUrl to the installer.
69
- return { version, spec: sameIdentity ? `${record.packageName}@${version}` : null, reasons, compatibility: release.compatibility };
70
- }
71
-
72
- export function catalogSummary(record) {
73
- return { packageName: record.packageName, displayName: record.displayName, summary: record.summary,
74
- latestVersion: record.latestVersion, categories: record.categories, claimed: record.claimed, verified: record.verified,
75
- deprecated: record.deprecated, security: record.security ?? null, weeklyDownloads: record.weeklyDownloads,
76
- url: `${hubWebsite}/plugins/${encodeURIComponent(record.slug)}` };
77
- }
78
-
79
- export function catalogDetail(record, environment) {
80
- return { ...catalogSummary(record), description: record.description, repository: record.repository,
81
- license: record.license ?? null, candidate: desktopCandidate(record, environment) };
82
- }
@@ -1,114 +0,0 @@
1
- globalThis.__ModuleLoader__.load({
2
- id: '@toddzheng024/dscode-desktop',
3
- factory: require => {
4
- const { createElement: h, useState, useEffect, useRef } = require('react');
5
- const control = { padding: '8px 10px', border: '1px solid #8886', borderRadius: 6, background: 'transparent', color: 'inherit', font: 'inherit' };
6
- const box = { border: '1px solid #8886', borderRadius: 8, padding: 16 };
7
- function Hub({ execute, manage, close }) {
8
- const [query, setQuery] = useState(''), [category, setCategory] = useState(''), [categories, setCategories] = useState([]);
9
- const [results, setResults] = useState(null), [detail, setDetail] = useState(null), [preview, setPreview] = useState(null);
10
- const [operation, setOperation] = useState(null), [error, setError] = useState(''), [busy, setBusy] = useState(false);
11
- const alive = useRef(true), locked = useRef(false), pageQuery = useRef({ query: '', category: '' });
12
- const run = async fn => {
13
- if (locked.current) return;
14
- locked.current = true; setBusy(true); setError('');
15
- try { await fn(); } catch (error) { if (alive.current) setError(error.message); }
16
- finally { locked.current = false; if (alive.current) setBusy(false); }
17
- };
18
- const search = async (more = false) => {
19
- const input = more ? pageQuery.current : { query, category };
20
- if (!more) { setDetail(null); setPreview(null); setResults(null); }
21
- const value = await execute('search', { ...input, ...(more ? results.nextPage ? { page: results.nextPage } : { cursor: results.nextCursor } : {}) });
22
- if (!alive.current) return;
23
- pageQuery.current = input;
24
- setResults(more ? { ...value, items: [...results.items, ...value.items.filter(item => !results.items.some(previous => previous.packageName === item.packageName))] } : value);
25
- };
26
- const load = () => run(async () => {
27
- const [list, initial, status] = await Promise.all([execute('categories'), execute('search', { query: '', category: '' }), execute('status')]);
28
- if (alive.current) { setCategories(list.items); setResults(initial); setOperation(status.operation); pageQuery.current = { query: '', category: '' }; }
29
- });
30
- useEffect(() => { alive.current = true; void load(); return () => { alive.current = false; }; }, []);
31
- useEffect(() => {
32
- if (operation?.status !== 'running') return;
33
- let stopped = false, polling = false;
34
- const timer = setInterval(async () => {
35
- if (polling) return; polling = true;
36
- try {
37
- const value = await execute('operation', { requestId: operation.requestId });
38
- if (!stopped && alive.current) setOperation(value);
39
- } catch (error) { if (!stopped && alive.current) setError(`Could not refresh installation: ${error.message}`); }
40
- finally { polling = false; }
41
- }, 1000);
42
- return () => { stopped = true; clearInterval(timer); };
43
- }, [operation?.requestId, operation?.status]);
44
- const installing = operation?.status === 'running';
45
- const button = (label, action, disabled = false) => h('button', { type: 'button', style: control, disabled: busy || disabled, onClick: action }, label);
46
- const openManagement = packageName => run(async () => { manage(packageName); close?.(); });
47
- const show = name => run(async () => {
48
- setDetail(null); setPreview(null);
49
- const value = await execute('detail', { packageName: name }); if (alive.current) setDetail(value);
50
- });
51
- const installed = detail?.installed || (operation?.packageName === detail?.packageName && operation?.result?.application === 'applied' && operation.result.bundle === detail?.packageName);
52
- const security = item => `Hub scan: ${item.security?.status ?? 'not reported'}`;
53
- const result = operation?.result;
54
- return h('section', { 'aria-label': 'Plugin Hub', style: { padding: 20, display: 'grid', gap: 16, maxWidth: 900 } },
55
- h('h2', { style: { margin: 0 } }, 'Plugin Hub'),
56
- h('p', null, 'Discover community plugins from DSH Plugin Hub. Install here, then enable and configure in Settings → Plugins.'),
57
- h('form', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 }, onSubmit: event => { event.preventDefault(); void run(() => search()); } },
58
- h('input', { type: 'search', 'aria-label': 'Search plugins', placeholder: 'Search plugins or describe a capability', value: query, maxLength: 300, style: { ...control, flex: 1 }, disabled: busy || installing, onChange: event => setQuery(event.target.value) }),
59
- h('select', { 'aria-label': 'Plugin category', value: category, style: control, disabled: busy || installing, onChange: event => setCategory(event.target.value) },
60
- h('option', { value: '' }, 'All categories'), ...categories.map(item => h('option', { key: item.name, value: item.name }, `${item.displayName} (${item.count})`))),
61
- h('button', { type: 'submit', disabled: busy || installing, style: control }, busy ? 'Loading…' : 'Search')),
62
- error && h('div', { role: 'alert' }, error, ' ', button('Refresh catalog', load, installing)),
63
- operation && h('div', { role: 'status', style: box },
64
- h('strong', null, operation.spec),
65
- h('p', null, installing ? 'Installing with the official plugin manager…' : operation.error ? operation.error : result?.application === 'applied' ? 'Installation completed. Enable the plugin in Settings → Plugins.' :
66
- result?.application === 'restart-required' ? 'The plugin manager requires a Desktop restart. Check Settings → Plugins after restarting.' :
67
- result?.application === 'cancelled' ? 'Installation cancelled.' : `Installation outcome: ${result?.application ?? 'unknown'}. Check Settings → Plugins.`),
68
- result?.error && h('p', null, result.error.message),
69
- result?.warnings?.map((warning, index) => h('p', { key: index }, warning)),
70
- result?.pendingBuilds?.length > 0 && h('p', null, `Build approval is required for ${result.pendingBuilds.join(', ')}. Review it through the official plugin manager.`),
71
- installing ? button('Cancel installation', () => run(async () => { const value = await execute('cancel', { requestId: operation.requestId }); if (alive.current) setOperation(value); })) :
72
- button('Manage installed plugin', () => openManagement(operation.packageName))),
73
- detail && h('article', { style: box, 'aria-label': 'Plugin details' },
74
- h('h3', null, detail.displayName), h('code', null, `${detail.packageName}@${detail.latestVersion}`),
75
- h('p', null, detail.summary), h('p', { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, detail.description),
76
- h('p', null, `${detail.categories.join(' · ')} · ${detail.weeklyDownloads} weekly downloads`),
77
- h('p', null, `Repository: ${detail.repository} · License: ${detail.license ?? 'not reported'}`),
78
- h('p', null, `${detail.claimed ? 'Publisher claimed' : 'Publisher unclaimed'} · ${detail.verified ? 'Publisher verified' : 'Publisher unverified'}`),
79
- h('p', null, security(detail), '. Publisher verification and scans do not guarantee safety.'),
80
- detail.security && h('ul', null, ...[['dependencyInventoryComplete', 'Complete dependency inventory'], ['advisoryScanned', 'Vulnerability advisories checked'], ['behaviorAnalyzed', 'Behavior analysis completed']].map(([key, label]) => h('li', { key }, `${label}: ${detail.security[key] === true ? 'yes' : 'no'}`))),
81
- h('a', { href: detail.url, target: '_blank', rel: 'noreferrer' }, 'View on DSH Plugin Hub'),
82
- h('p', null, `Your Desktop: DSH ${detail.environment.runtime} · ${detail.environment.platform}`),
83
- detail.candidate.compatibility && h('p', null, `Declared compatibility: DSH ${detail.candidate.compatibility.dsh} · ${detail.candidate.compatibility.surfaces.join(', ')} · ${detail.candidate.compatibility.platforms.join(', ') || 'all platforms'}`),
84
- detail.candidate.reasons.length > 0 && h('ul', null, ...detail.candidate.reasons.map(reason => h('li', { key: reason }, reason))),
85
- installed ? button('Manage in Plugins', () => openManagement(detail.packageName), installing) :
86
- button('Review installation', () => run(async () => { const value = await execute('prepare', { packageName: detail.packageName }); if (alive.current) { setDetail(value.detail); setPreview(value); } }), installing || detail.candidate.reasons.length > 0),
87
- preview && h('div', { style: { ...box, marginTop: 16 }, role: 'group', 'aria-label': 'Confirm plugin installation' },
88
- h('p', null, `Install ${preview.detail.candidate.spec} from ${preview.inspection.registry ?? 'your configured npm registry'}?`),
89
- h('p', null, 'The official manager will download the package and its dependencies. It will remain disabled until you enable it in Plugins. Enabling third-party plugins runs their code in the Host.'),
90
- button('Confirm installation', () => run(async () => { const value = await execute('install', { token: preview.token, confirm: true }); if (alive.current) { setOperation(value); setPreview(null); } }), installing),
91
- ' ', button('Back', () => setPreview(null), installing))),
92
- results && h('div', { 'aria-label': 'Plugin search results', style: { display: 'grid', gap: 12 } },
93
- results.items.length === 0 && h('p', null, 'No plugins match this search. Try another phrase or category.'),
94
- ...results.items.map(item => h('article', { key: item.packageName, style: box },
95
- h('h3', { style: { margin: 0 } }, item.displayName), h('code', null, `${item.packageName}@${item.latestVersion}`),
96
- h('p', null, item.summary), h('p', null, `${security(item)}${item.deprecated ? ' · Deprecated' : ''}`),
97
- button('View details', () => show(item.packageName), installing))),
98
- (results.nextPage || results.nextCursor) && button('Load more plugins', () => run(() => search(true)), installing)));
99
- }
100
- return { inject: ['slots', 'connection'], apply(ctx) {
101
- ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dscode-hub', order: 13, label: () => 'Plugin Hub',
102
- inject: () => ({ execute: async (action, args = {}) => {
103
- const response = await ctx.connection.rpc.call('/api', 'dscode-hub', { ...args, action });
104
- if (!response.ok) throw Error(response.error.message);
105
- return response.value;
106
- }, manage: packageName => {
107
- const navigation = ctx.get('pluginNavigation');
108
- if (!navigation) throw Error('Plugin management is unavailable. Close settings and open Plugins from the sidebar.');
109
- navigation.openBundle(packageName);
110
- } }),
111
- }, Hub));
112
- } };
113
- },
114
- });