@toddzheng024/dscode-desktop 0.7.33 → 0.7.35

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 (54) hide show
  1. package/README.md +8 -7
  2. package/cordis.patch.yml +5 -4
  3. package/docs/CHANGELOG.md +18 -1
  4. package/docs/browser-use.md +38 -21
  5. package/docs/custom-providers.md +3 -3
  6. package/docs/demo.md +77 -11
  7. package/docs/hub-distribution.md +33 -6
  8. package/docs/memory.md +1 -1
  9. package/docs/plugin-hub.md +26 -59
  10. package/docs/releases/0.7.33.md +12 -5
  11. package/docs/releases/0.7.34.md +24 -0
  12. package/docs/releases/0.7.35.md +31 -0
  13. package/docs/triggers.md +1 -1
  14. package/locale/en.json +1 -1
  15. package/locale/zh-cn.json +1 -1
  16. package/package.json +3 -8
  17. package/plugins/auto-review/index.mjs +2 -2
  18. package/plugins/browser/config.mjs +2 -2
  19. package/plugins/browser/desktop-client.mjs +22 -9
  20. package/plugins/browser/desktop-host.mjs +5 -0
  21. package/plugins/code-review/baseline.mjs +2 -2
  22. package/plugins/compaction/threshold.mjs +2 -1
  23. package/plugins/credentials/index.mjs +11 -0
  24. package/plugins/custom/desktop-client.mjs +12 -9
  25. package/plugins/desktop/client.mjs +135 -161
  26. package/plugins/desktop/desktop-client.mjs +50 -0
  27. package/plugins/desktop/index.mjs +1 -0
  28. package/plugins/desktop/settings.css +36 -0
  29. package/plugins/desktop-workspace/index.mjs +2 -2
  30. package/plugins/dscode/desktop-client.mjs +3 -3
  31. package/plugins/dscode/index.mjs +2 -2
  32. package/plugins/email/desktop-client.mjs +5 -5
  33. package/plugins/email/desktop-host.mjs +2 -2
  34. package/plugins/grok/billing.mjs +2 -1
  35. package/plugins/grok/index.mjs +2 -1
  36. package/plugins/grok/status.mjs +2 -1
  37. package/plugins/jev/index.mjs +2 -1
  38. package/plugins/memory/index.mjs +2 -2
  39. package/plugins/opencode-go/index.mjs +2 -1
  40. package/plugins/opencode-go/usage.mjs +2 -1
  41. package/plugins/openrouter/index.mjs +2 -1
  42. package/plugins/providers/desktop-client.mjs +22 -15
  43. package/plugins/session-bridge/index.mjs +2 -3
  44. package/plugins/session-cards/index.mjs +2 -2
  45. package/plugins/session-metrics/desktop-client.mjs +3 -3
  46. package/plugins/session-metrics/index.mjs +2 -1
  47. package/plugins/session-metrics/view.mjs +4 -3
  48. package/plugins/triggers/desktop-client.mjs +15 -9
  49. package/plugins/triggers/desktop-host.mjs +2 -2
  50. package/plugins/tui-tools/doctor.mjs +2 -1
  51. package/plugins/tui-tools/index.mjs +2 -1
  52. package/plugins/hub/catalog.mjs +0 -82
  53. package/plugins/hub/desktop-client.mjs +0 -114
  54. package/plugins/hub/desktop-host.mjs +0 -129
@@ -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; }
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { createHash } from 'node:crypto';
2
3
  import { homedir } from 'node:os';
3
4
  import { join, resolve } from 'node:path';
@@ -13,8 +14,7 @@ export const inject = ['agents', 'agentPresets', 'skills', 'sessionProjections',
13
14
 
14
15
  /** Session-owned discovery; Desktop's process cwd is not a workspace identity. */
15
16
  export function apply(ctx) {
16
- const stateDir = process.env.DSH_HOME;
17
- if (!stateDir) throw Error('Desktop workspace discovery requires DSH_HOME');
17
+ const stateDir = resolveDshHome();
18
18
  mkdirSync(join(stateDir, 'config'), { recursive: true });
19
19
  const installationHooks = join(stateDir, 'config/hooks.local.json');
20
20
  if (!existsSync(installationHooks)) writeFileSync(installationHooks, '{"hooks":{}}\n', { mode: 0o600, flag: 'wx' });
@@ -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'),
@@ -1,5 +1,5 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { existsSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { AsyncLocalStorage } from 'node:async_hooks';
@@ -108,7 +108,7 @@ export function apply(ctx, config = {}) {
108
108
  return { ...assembled, tools: assembled.tools.filter(tool => !hidden.has(tool.name)), sections: assembled.sections.filter(section => !hidden.has(section.name.replace(/^tool:/, ''))) };
109
109
  });
110
110
  const runningChildren = id => ctx.agents.list().filter(a => a.session.header.origin === 'subagent' && a.session.header.parentSession === id && a.status === 'running').length;
111
- const home = process.env.DSH_HOME ?? process.env.DSCODE_HOME ?? join(homedir(), '.local/share/dscode-hub');
111
+ const home = resolveDshHome();
112
112
  const board = new DelegateBoard({ root: join(home, 'delegate-boards'), childName: CHILD_NAME });
113
113
  const boardView = sessionId => board.view(sessionId, childId => ctx.agents.get(childId)?.status);
114
114
  ctx.effect(() => setBoardSource(sessionId => {
@@ -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 } },
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { clientRequestSchema } from '@deepseek-ai/dsh-client-connection';
2
3
  import { DesktopEmail } from './desktop-service.mjs';
3
4
  import { createEmailContacts } from './contacts.mjs';
@@ -6,8 +7,7 @@ import { registerEmailTools } from '../email-tools/index.mjs';
6
7
 
7
8
  export const inject = ['agents', 'agentPresets', 'sessions'];
8
9
  export async function apply(ctx) {
9
- if (!process.env.DSH_HOME) throw Error('Desktop email requires DSH_HOME.');
10
- const service = new DesktopEmail({ home: process.env.DSH_HOME });
10
+ const service = new DesktopEmail({ home: resolveDshHome() });
11
11
  const contacts = createEmailContacts({ directory: service.inbox.directory });
12
12
  const sender = createEmailSender({ directory: service.inbox.directory,
13
13
  configurationHint: 'Open Email inbox → Mailbox connection and connect Gmail IMAP with an application password before sending. Gmail OAuth supports receiving only.' });
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
 
@@ -136,7 +137,7 @@ export function setGrokSubscription(next, at = Date.now()) {
136
137
  * instead of falling back to a bare tier name. One file read per refresh window keeps the
137
138
  * render path off the disk.
138
139
  */
139
- export function grokSubscriptionNow(home = process.env.DSH_HOME) {
140
+ export function grokSubscriptionNow(home = resolveDshHome()) {
140
141
  if (state !== undefined) return state;
141
142
  if (home === undefined) return undefined;
142
143
  if (disk !== undefined && disk.home === home && Date.now() - disk.at < GROK_SUBSCRIPTION_TTL_MS) return disk.value;
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import z from "@deepseek-ai/schemastery";
2
3
  import { RetryPolicySchema, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
3
4
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
@@ -72,7 +73,7 @@ export function apply(ctx, config = {}) {
72
73
  }
73
74
  };
74
75
  options();
75
- const home = process.env.DSH_HOME;
76
+ const home = resolveDshHome();
76
77
  /** The credential the route calls with, or undefined: the catalog must load without a login. */
77
78
  const resolveTokenSafe = async () => {
78
79
  const ref = options().apiKeyEnv;
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { grokAuthState, minutesLeft } from "./auth.mjs";
2
3
  import { grokSubscriptionNow, readGrokSubscription } from "./billing.mjs";
3
4
 
@@ -10,7 +11,7 @@ import { grokSubscriptionNow, readGrokSubscription } from "./billing.mjs";
10
11
  * @returns `{ status: { kind, expiresIn? }, subscription? }`; `kind` is the local login state
11
12
  * (`ready`, `expired`, `missing`, `malformed`) and `expiresIn` is minutes on a ready login.
12
13
  */
13
- export function grokStatusSnapshot({ home = process.env.DSH_HOME, now = Date.now() } = {}) {
14
+ export function grokStatusSnapshot({ home = resolveDshHome(), now = Date.now() } = {}) {
14
15
  const auth = grokAuthState({ now });
15
16
  const stored = grokSubscriptionNow() ?? (home === undefined ? undefined : readGrokSubscription(home));
16
17
  return {
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import z from '@deepseek-ai/schemastery';
2
3
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
3
4
  import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment';
@@ -60,7 +61,7 @@ export function apply(ctx, config = {}) {
60
61
  const apiKey = await resolveKey();
61
62
  if (!apiKey || signal?.aborted) return undefined;
62
63
  const started = Date.now();
63
- const home = process.env.DSH_HOME, id = randomUUID();
64
+ const home = resolveDshHome(), id = randomUUID();
64
65
  const recipients = sessionId && home ? metricRecipients(ctx.get('agents'), sessionId) : [];
65
66
  const save = entry => {
66
67
  for (const recipient of recipients) {
@@ -1,4 +1,4 @@
1
- import { homedir } from 'node:os';
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
2
2
  import { resolve, join } from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
@@ -37,7 +37,7 @@ export function applyDesktop(ctx, options = {}) {
37
37
 
38
38
  function install(ctx, options, desktop = false) {
39
39
  const config = resolveConfig(options);
40
- const root = resolve(config.root ?? process.env.DSCODE_MEMORY_HOME ?? join(process.env.DSCODE_HOME ?? process.env.DSH_HOME ?? join(homedir(), '.local/share/dscode-hub'), 'memories'));
40
+ const root = resolve(config.root ?? process.env.DSCODE_MEMORY_HOME ?? join(resolveDshHome(), 'memories'));
41
41
  const store = new MemoryStore(root);
42
42
  const controller = new AbortController(), owner = randomUUID(), live = new Set(), started = new Set();
43
43
  let running, lastRoute, lastResult, lastSession;
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import z from '@deepseek-ai/schemastery';
2
3
  import { LlmError, RetryPolicySchema, resolveImageAttachmentAccess, resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
3
4
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
@@ -65,7 +66,7 @@ export function apply(ctx, config = {}) {
65
66
  }
66
67
  };
67
68
  options();
68
- const home = process.env.DSH_HOME;
69
+ const home = resolveDshHome();
69
70
  // The login lives in the shared credential store beside the provider keys.
70
71
  const grants = {
71
72
  read: async () => (await ctx.get('credentials')?.resolve(GRANT_REF))?.value || undefined,
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
 
@@ -81,7 +82,7 @@ export async function currentGoUsage({ home, resolveAuth, fetch, now = Date.now(
81
82
  }
82
83
 
83
84
  /** The snapshot the status line renders: this process's own, else the file another process wrote. */
84
- export function goUsageNow(home = process.env.DSH_HOME, now = Date.now()) {
85
+ export function goUsageNow(home = resolveDshHome(), now = Date.now()) {
85
86
  if (state !== undefined) return state;
86
87
  if (home === undefined) return undefined;
87
88
  if (disk !== undefined && disk.home === home && now - disk.at < GO_USAGE_TTL_MS) return disk.value;
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import z from '@deepseek-ai/schemastery';
2
3
  import { LlmError, RetryPolicySchema, assertUsableApiKey, resolveImageAttachmentAccess, resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
3
4
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
@@ -73,7 +74,7 @@ export function apply(ctx, config = {}) {
73
74
  if (credentials !== undefined) return (await credentials.resolve(ref))?.value || undefined;
74
75
  return launchEnvironmentOf(ctx).get(ref)?.value || undefined;
75
76
  };
76
- const home = process.env.DSH_HOME;
77
+ const home = resolveDshHome();
77
78
  const ensureModels = () => ensureOpenRouterModels({ home });
78
79
  const adapter = new OpenRouterAdapter({
79
80
  displayName,
@@ -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
  });
@@ -1,5 +1,4 @@
1
- import { homedir } from 'node:os';
2
- import { join } from 'node:path';
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
3
2
  import { communicationPanel, createCommunicationFeed, foldCommunication } from './tasks.mjs';
4
3
  import { SessionBridge } from './server.mjs';
5
4
  import { CommunicationService } from './communication.mjs';
@@ -8,7 +7,7 @@ import { discover, request } from './client.mjs';
8
7
  export const name = 'dscode-session-bridge';
9
8
  export const inject = ['agents', 'sessions', 'sessionTitle', 'sessionCards', 'commands', 'systemPrompt', 'tools'];
10
9
  export async function apply(ctx) {
11
- const home = process.env.DSH_HOME ?? process.env.DSCODE_HOME ?? join(homedir(), '.local/share/dscode-hub');
10
+ const home = resolveDshHome();
12
11
  const homeOption = `--home '${home.replaceAll("'", "'\\''")}'`;
13
12
  const bridge = new SessionBridge(ctx, home);
14
13
  await bridge.start();
@@ -1,5 +1,5 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { join } from 'node:path';
2
- import { homedir } from 'node:os';
3
3
  import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
4
4
  import { SessionCards } from './manager.mjs';
5
5
  import { TOPIC_PROMPT } from './content.mjs';
@@ -8,7 +8,7 @@ import { effortFor } from '../providers/effort.mjs';
8
8
  export const name = 'dscode-session-cards';
9
9
  export const inject = ['sessions', 'llm'];
10
10
  export function apply(ctx, config = {}) {
11
- const home = process.env.DSH_HOME ?? process.env.DSCODE_HOME ?? join(homedir(), '.local/share/dscode-hub');
11
+ const home = resolveDshHome();
12
12
  const cards = new SessionCards({ root: join(home, 'session-cards'), config, generate: async (input, route, signal, sessionId) => {
13
13
  const assembler = new BlockAssembler(); let finished = false, usage;
14
14
  // Cards need little reasoning whatever the session runs at: low, or the nearest level the model offers.
@@ -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'),
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { appendMetric } from './store.mjs';
3
4
  import { estimateCost, priceVersionFor } from './pricing.mjs';
@@ -30,7 +31,7 @@ export function apply(ctx) {
30
31
  snapshots.set(session, { seq: session.seq, value });
31
32
  return { ...value, currentTps: smoothedRate.get(session), requestActive };
32
33
  }));
33
- const home = process.env.DSH_HOME;
34
+ const home = resolveDshHome();
34
35
  // Remaining balance is best-effort decoration: resolve the key lazily (never
35
36
  // inject the credentials service, so a missing one cannot fail startup), keep
36
37
  // the request on a five-minute cache, and never let it reach the render path.
@@ -1,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { readMetrics } from './store.mjs';
2
3
  import { t } from '../i18n/messages.mjs';
3
4
  import { estimateCost, peakEmoji } from './pricing.mjs';
@@ -67,7 +68,7 @@ export function summarize(rows, events = [], corrupt = false) {
67
68
  * @returns `{ turn, cost, calls, unknown }` per completed or running turn.
68
69
  */
69
70
  export function turnCostsFor(id, events = []) {
70
- const entries = id && process.env.DSH_HOME ? readMetrics(process.env.DSH_HOME, id).rows : [];
71
+ const entries = id ? readMetrics(resolveDshHome(), id).rows : [];
71
72
  return attributeCostByTurn(entries, events).turns;
72
73
  }
73
74
  /**
@@ -229,7 +230,7 @@ export function formatFooter(metrics, context, columns = 80, rates, locale = 'en
229
230
  */
230
231
  export function sessionSpend(id) {
231
232
  try {
232
- const ledger = id && process.env.DSH_HOME ? readMetrics(process.env.DSH_HOME, id) : { rows: [], corrupt: false };
233
+ const ledger = id ? readMetrics(resolveDshHome(), id) : { rows: [], corrupt: false };
233
234
  const data = id ? source?.(id) : undefined;
234
235
  const summary = summarize(ledger.rows, data?.events ?? [], ledger.corrupt);
235
236
  return { cost: summary.cost, unknown: summary.unknown, pending: summary.pending };
@@ -245,7 +246,7 @@ export function footerFor(id, stats, columns, provider = 'deepseek-official', lo
245
246
  // Keep the durable prompt reading visible instead of losing all telemetry.
246
247
  let data;
247
248
  try { data = id ? source?.(id) : undefined; } catch { /* use the TUI's durable projection */ }
248
- const ledger = id && process.env.DSH_HOME ? readMetrics(process.env.DSH_HOME, id) : { rows: [], corrupt: false };
249
+ const ledger = id ? readMetrics(resolveDshHome(), id) : { rows: [], corrupt: false };
249
250
  const events = data?.events ?? [];
250
251
  // Identity alone is not enough: a session event list may be appended to in place.
251
252
  const hit = events.length > 0 ? footerCache.get(events) : undefined;
@@ -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,3 +1,4 @@
1
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
1
2
  import { clientRequestSchema } from '@deepseek-ai/dsh-client-connection';
2
3
  import { DesktopTriggerControl, DesktopTriggerManagement } from './desktop-management.mjs';
3
4
  import { registerTriggerTools } from './tools.mjs';
@@ -33,8 +34,7 @@ export async function triggerDesktopRequest(ctx, { control, management }, payloa
33
34
  }
34
35
 
35
36
  export function apply(ctx) {
36
- const home = process.env.DSH_HOME;
37
- if (!home) throw Error('Desktop scheduling requires DSH_HOME');
37
+ const home = resolveDshHome();
38
38
  const control = new DesktopTriggerControl(ctx, { home });
39
39
  const management = new DesktopTriggerManagement({ home, control });
40
40
  const service = { control, management };