@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,14 @@
1
+ # DSCODE 0.7.36
2
+
3
+ This release exposes the DSCODE agent preset to ACP clients through the source and npm launchers.
4
+
5
+ - **ACP entry point:** `dscode acp` runs DSH's native stdio bridge with DSCODE tools and provider adapters. It uses the saved model selection or `--model PROVIDER/ID`; configure credentials and custom services in the terminal interface first. Configure clients with executable `dscode` and arguments `["acp"]`. See the [ACP guide](../acp.md) for setup.
6
+ - **Client approvals and session lifecycle:** ACP sessions use the `ask` permission preset and send approval requests to the client. Creation, prompts, cancellation, model configuration, listing, resume, close and MCP attachment inherit the pinned native bridge. Stdout carries protocol frames, terminal-only generation is disabled, and closing stdin exits the runtime while preserving the installation's session state.
7
+ - **Support boundary:** the bridge retains the `deepseek-harness-acp` identity. It does not add transcript replay through `session/load`, modes, slash commands, client filesystem/terminal delegation or interactive question forms. This is a terminal-launcher transport; the Desktop plugin retains its existing support boundary.
8
+ - **Distribution checks:** the npm launcher includes its ACP routing code, the bundle includes the bridge, and Hub publication requires a successful protocol probe against the installed artifacts. Native tool-result regression cases cover all three custom API formats using upstream's serializer. Linux CI uses the official Node image's ECR Public mirror to avoid Docker Hub anonymous pull limits.
9
+
10
+ Update the global launcher with `npm install -g @toddzheng024/dscode@latest`, then run `dscode update` to refresh an existing Hub profile. A fresh installation provisions its matching profile on first use.
11
+
12
+ Verification of the merged feature code passed `npm run check` on Node 22.19.0 and 24.19.0: 1,087 unit/component tests, both TypeScript checks, 80.16% complete source line coverage, integration/package checks and 63 offline evaluation cases. Real Chrome checks and the scripted browser evaluation passed. Linux end-to-end verification passed all seven steps, including installed ACP, Hub installation, agent execution, failed upgrade and rollback; the GitHub macOS and Linux gates also passed. Documentation and lint checks run for this version change, and the release workflow requalifies the exact versioned terminal and Desktop archives before publication.
13
+
14
+ The ACP probe covers initialization, a fixture custom API, DSCODE tool calls, allow/reject approvals, cancellation, list/resume/close and EOF shutdown. Specific editor compatibility and paid model endpoints have no automated case in this release. No live-model quality claim follows from the fixture probes.
package/docs/triggers.md CHANGED
@@ -7,7 +7,7 @@ A trigger is not a way to reach a session that is already open: that is [session
7
7
  ## Experimental Desktop scheduling
8
8
 
9
9
  The [combined Desktop package](browser-use.md#install-the-experimental-combined-desktop-package)
10
- adds **Settings → DSCODE schedules**. Open a DSCODE session in the target project,
10
+ adds **Settings → DSCODE → Schedules**. Open a DSCODE session in the target project,
11
11
  then select that session in the settings page. The form creates or edits an
12
12
  interval, calendar, external-event or script task. New form entries default to
13
13
  read-only execution and a persistent task-owned session. You can pause a task,
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "DSCODE",
4
- "description": "Coding collaboration, session memory, browser workflows and community plugin discovery for Harness Desktop."
4
+ "description": "Coding collaboration, session memory and browser workflows for Harness Desktop."
5
5
  }
6
6
  }
package/locale/zh-cn.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "DSCODE",
4
- "description": "为 Harness Desktop 提供编码协作、会话记忆、浏览器工作流和社区插件发现。"
4
+ "description": "为 Harness Desktop 提供编码协作、会话记忆和浏览器工作流。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toddzheng024/dscode-desktop",
3
- "version": "0.7.34",
3
+ "version": "0.7.36",
4
4
  "private": false,
5
5
  "description": "DSCODE for Harness Desktop: coding collaboration, session memory, browser workflows and task management.",
6
6
  "license": "MIT",
@@ -46,7 +46,6 @@
46
46
  "./workspace": "./plugins/desktop-workspace/index.mjs",
47
47
  "./credentials": "./plugins/credentials/index.mjs",
48
48
  "./custom": "./plugins/custom/index.mjs",
49
- "./hub": "./plugins/hub/desktop-host.mjs",
50
49
  "./openrouter": "./plugins/openrouter/index.mjs",
51
50
  "./grok": "./plugins/grok/index.mjs",
52
51
  "./opencode-go": "./plugins/opencode-go/index.mjs",
@@ -92,8 +91,6 @@
92
91
  "@modelcontextprotocol/sdk": "1.30.0",
93
92
  "chrome-devtools-mcp": "1.10.1",
94
93
  "ws": "8.21.3",
95
- "@dsh-plugin-hub/schemas": "0.5.0",
96
- "semver": "7.8.5",
97
94
  "imapflow": "2.0.5",
98
95
  "mailparser": "3.9.28",
99
96
  "nodemailer": "10.0.10",
@@ -112,7 +109,7 @@
112
109
  "hub": {
113
110
  "schemaVersion": 1,
114
111
  "displayName": "DSCODE Desktop",
115
- "summary": "Coding collaboration, memory, browser workflows and community plugin discovery for Harness Desktop.",
112
+ "summary": "Coding collaboration, memory, browser workflows and task management for Harness Desktop.",
116
113
  "description": "Adds DSCODE to the official Harness Desktop application. Requires the exact DSH runtime 0.2.0-rc.2. Qualified on macOS Apple Silicon. Install the plugin, select the DSCODE agent preset and configure a model account. This package does not include a standalone application or the terminal UI.",
117
114
  "homepage": "https://github.com/qiz029/dscode#readme",
118
115
  "categories": [
@@ -123,8 +120,7 @@
123
120
  "coding",
124
121
  "desktop",
125
122
  "browser",
126
- "collaboration",
127
- "plugin-hub"
123
+ "collaboration"
128
124
  ],
129
125
  "compatibility": {
130
126
  "dsh": "0.2.0-rc.2",
@@ -139,7 +135,6 @@
139
135
  },
140
136
  "entryIds": [
141
137
  "dscode-desktop",
142
- "dscode-hub",
143
138
  "dscode-browser"
144
139
  ],
145
140
  "before": [],
@@ -0,0 +1,45 @@
1
+ export const USAGE = `Usage: dscode acp [options]
2
+ Serve Agent Client Protocol over stdio using the DSCODE agent preset.
3
+ The client supplies each session's workspace. Stdout contains only ACP frames.
4
+
5
+ Options:
6
+ --model PROVIDER/ID initial model (default: the saved default model)
7
+ --patch FILE additional composition overlay (repeatable)
8
+ -h, --help show this help
9
+
10
+ Configure credentials and custom providers in the TUI before connecting.
11
+ Permission requests are answered by the ACP client. Disconnecting exits.`;
12
+
13
+ export function parseAcpArgs(argv) {
14
+ const options = { patches: [], help: false };
15
+ for (let index = 0; index < argv.length; index++) {
16
+ const arg = argv[index];
17
+ if (arg === '--help' || arg === '-h') { options.help = true; continue; }
18
+ if (!['--model', '--patch'].includes(arg)) throw Error(`Unknown ACP option: ${arg}`);
19
+ const value = argv[++index];
20
+ if (!value || value.startsWith('-')) throw Error(`${arg} requires a value`);
21
+ if (arg === '--patch') options.patches.push(value);
22
+ else {
23
+ if (!/^[^/\s]+\/\S+$/.test(value)) throw Error('--model expects provider/model');
24
+ options.model = value;
25
+ }
26
+ }
27
+ return options;
28
+ }
29
+
30
+ export function acpOverlay(entry, options = {}) {
31
+ return `${['tui-startup', 'tui-runner', 'session-title-llm'].map(id => `- id: ${id}\n disabled: true`).join('\n')}
32
+ - id: dscode-session-cards
33
+ config:
34
+ enabled: false
35
+ - id: dscode-memory
36
+ config:
37
+ generate: false
38
+ - insert:
39
+ - id: acp-app-startup
40
+ name: '@deepseek-ai/dsh-acp-app'
41
+ - id: dscode-acp
42
+ name: ${JSON.stringify(entry)}
43
+ config: ${JSON.stringify(options.model ? { model: options.model } : {})}
44
+ `;
45
+ }
@@ -0,0 +1,44 @@
1
+ import { apply as applyAcp } from '@deepseek-ai/dsh-acp';
2
+ import z from '@deepseek-ai/schemastery';
3
+
4
+ export const name = 'dscode-acp';
5
+ export const inject = ['acpAppStartup', 'agents', 'agentPresets', 'agentDefaultModel', 'permissionPresets', 'llm', 'sessionPersistence', 'sessions', 'dscodeCustom'];
6
+ export const Config = z.object({ model: z.string() });
7
+
8
+ // Scope the factory to this bridge: TUI and delegated agents keep their own
9
+ // composition. ACP still owns model selection, MCP attachment and disposal.
10
+ export function presetAgents(ctx) {
11
+ const wrap = method => async (options) => {
12
+ const handle = await ctx.agents[method]({
13
+ ...options,
14
+ ...(method === 'create' ? { meta: { ...options.meta, agentPreset: 'dscode' } } : {}),
15
+ setup: async (agentCtx, agent) => {
16
+ await ctx.agentPresets.mount(agentCtx, 'dscode');
17
+ await options.setup?.(agentCtx, agent);
18
+ },
19
+ });
20
+ ctx.permissionPresets.set(handle.agent.session, 'ask');
21
+ return handle;
22
+ };
23
+ const overrides = { create: wrap('create'), resume: wrap('resume') };
24
+ return new Proxy(ctx.agents, {
25
+ get(target, key) {
26
+ if (Object.hasOwn(overrides, key)) return overrides[key];
27
+ const value = Reflect.get(target, key, target);
28
+ return typeof value === 'function' ? value.bind(target) : value;
29
+ },
30
+ });
31
+ }
32
+
33
+ export function startAcp(ctx, config = {}, bridge = applyAcp) {
34
+ const selection = config.model
35
+ ? { provider: config.model.slice(0, config.model.indexOf('/')), model: config.model.slice(config.model.indexOf('/') + 1) }
36
+ : ctx.agentDefaultModel.currentSelection();
37
+ const scoped = ctx.extend({ agents: presetAgents(ctx) });
38
+ bridge(scoped, { provider: selection.provider, model: selection.model });
39
+ }
40
+
41
+ export async function apply(ctx, config) {
42
+ await ctx.get('dscodeCustom').refresh();
43
+ startAcp(ctx, config);
44
+ }
@@ -4,7 +4,7 @@ globalThis.__ModuleLoader__.load({
4
4
  factory: require => {
5
5
  const { createElement: h, useState, useEffect, useRef } = require('react');
6
6
  const id = '@toddzheng024/dscode-browser-desktop';
7
- const button = { padding: '7px 11px', border: '1px solid #8886', borderRadius: 6, background: 'transparent', color: 'inherit', cursor: 'pointer' };
7
+ const button = { padding: '7px 11px', border: '.5px solid var(--dsw-alias-border-l3, #8886)', borderRadius: 'var(--dsw-radius-md, 6px)', background: 'transparent', color: 'inherit', cursor: 'pointer', font: 'inherit' };
8
8
  function previewButton(props, label) {
9
9
  return h('button', { ...props, style: { ...button, ...props.style, ...(props.disabled ? { opacity: 0.5, cursor: 'not-allowed' } : {}) } }, label);
10
10
  }
@@ -197,9 +197,9 @@ globalThis.__ModuleLoader__.load({
197
197
  setNotice('Browser control resumed. Your draft is kept. Capture a new preview before sending.');
198
198
  await list(current, { preserveDraft: true });
199
199
  });
200
- return h('section', { 'aria-label': 'DSCODE browser preview', style: { padding: 12, boxSizing: 'border-box', overflow: 'auto', height: '100%', display: 'flex', flexDirection: 'column', gap: 8, color: 'inherit' } },
200
+ return h('section', { className: 'dscode-ui', 'aria-label': 'DSCODE browser preview', style: { padding: 12, boxSizing: 'border-box', overflow: 'auto', height: '100%', display: 'flex', flexDirection: 'column', gap: 8, color: 'inherit' } },
201
201
  h('strong', null, 'Browser preview'),
202
- handoff && h('div', { 'aria-label': 'Browser handoff', role: 'status', style: { border: '1px solid #8886', borderRadius: 8, padding: 10, overflowWrap: 'anywhere' } },
202
+ handoff && h('div', { 'aria-label': 'Browser handoff', role: 'status', style: { border: '.5px solid var(--dsw-alias-border-l3, #8886)', borderRadius: 8, padding: 10, overflowWrap: 'anywhere' } },
203
203
  h('strong', null, 'Waiting for you'),
204
204
  h('p', null, handoff.reason),
205
205
  handoff.invalidated && h('p', null, 'Chrome restarted. The original tab is unavailable; choose a live tab after resuming.'),
@@ -213,7 +213,7 @@ globalThis.__ModuleLoader__.load({
213
213
  previewButton({ style: button, disabled: busy || controlBusy || !pageId || !!handoff, onClick: () => run(refresh) }, 'Refresh preview'),
214
214
  frame && previewButton({ style: button, onClick: () => setExpanded(!expanded) }, expanded ? 'Fit image' : 'Expand image')),
215
215
  h('select', { 'aria-label': 'Browser tab', value: pageId, disabled: busy || controlBusy, onChange: e => { setPageId(e.target.value); setFrame(null); setPoint(null); setText(''); setLive(false); }, style: { ...button, width: '100%' } }, h('option', { value: '' }, 'Choose a tab'), ...pages.map(page => h('option', { key: page.id, value: String(page.id) }, `${page.id} · ${page.url}`))),
216
- origin && h('details', { 'aria-label': 'Browser site permissions', style: { border: '1px solid #8886', borderRadius: 8, padding: '8px 10px', margin: 0, minWidth: 0 } },
216
+ origin && h('details', { 'aria-label': 'Browser site permissions', style: { border: '.5px solid var(--dsw-alias-border-l3, #8886)', borderRadius: 8, padding: '8px 10px', margin: 0, minWidth: 0 } },
217
217
  h('summary', { 'aria-label': `Site permissions · ${access}`, style: { cursor: 'pointer' } }, 'Site permissions · ', h('span', { 'aria-label': 'Site access status' }, access)),
218
218
  h('div', { style: { fontSize: 12, overflowWrap: 'anywhere', margin: '8px 0' } }, origin),
219
219
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } },
@@ -232,7 +232,7 @@ globalThis.__ModuleLoader__.load({
232
232
  frame && h('div', null,
233
233
  h('div', { style: { fontSize: 12, overflowWrap: 'anywhere', marginBottom: 8 } }, `${frame.url} · ${new Date(frame.capturedAt).toLocaleTimeString()}`),
234
234
  h('div', { style: { position: 'relative', lineHeight: 0, display: 'inline-block', maxWidth: '100%', ...(expanded ? { width: '100%' } : {}) } },
235
- h('img', { src: `data:${frame.image.mimeType};base64,${frame.image.data}`, alt: expired ? 'Expired browser preview; refresh before annotating' : 'Captured browser page; click a point to annotate', role: 'button', tabIndex: 0, 'aria-disabled': busy || controlBusy || expired, 'aria-description': 'Enter or Space selects the center. Arrow keys move one pixel; Shift moves ten. Escape clears the point.', onClick: selectPoint, onKeyDown: selectPointKey, style: { maxWidth: '100%', maxHeight: expanded ? 'none' : '30vh', width: expanded ? '100%' : 'auto', height: 'auto', display: 'block', cursor: expired ? 'default' : 'crosshair', borderRadius: 6 } }),
235
+ h('img', { src: `data:${frame.image.mimeType};base64,${frame.image.data}`, alt: expired ? 'Expired browser preview; refresh before annotating' : 'Captured browser page; click a point to annotate', role: 'button', tabIndex: 0, 'aria-disabled': busy || controlBusy || expired, 'aria-description': 'Enter or Space selects the center. Arrow keys move one pixel; Shift moves ten. Escape clears the point.', onClick: selectPoint, onKeyDown: selectPointKey, style: { maxWidth: '100%', maxHeight: expanded ? 'none' : '30vh', width: expanded ? '100%' : 'auto', height: 'auto', display: 'block', cursor: expired ? 'default' : 'crosshair', borderRadius: 'var(--dsw-radius-md, 6px)' } }),
236
236
  point && h('span', { 'aria-label': 'Selected annotation point', style: { position: 'absolute', left: `${point.x * 100}%`, top: `${point.y * 100}%`, width: 20, height: 20, border: '3px solid #f97316', borderRadius: '50%', transform: 'translate(-50%, -50%)', pointerEvents: 'none', boxShadow: '0 0 0 2px white' } }))),
237
237
  h('p', { style: { fontSize: 12, margin: 0, opacity: 0.75 } }, 'Preview shows the controlled Chrome tab. Click a point, or focus the image and press Enter or Space to select the center. Arrow keys move one pixel; Shift moves ten. Escape clears the point. Refresh after navigation or after one minute.'),
238
238
  frame && point && h('p', { 'aria-label': 'Annotation coordinates', 'aria-live': 'polite', style: { fontSize: 12, margin: 0 } }, `Selected pixel: (${Math.round(point.x * (frame.width - 1))}, ${Math.round(point.y * (frame.height - 1))}) in ${frame.width}×${frame.height}.`),
@@ -259,18 +259,31 @@ globalThis.__ModuleLoader__.load({
259
259
  const draftRevision = useRef(0);
260
260
  return visible ? h(SessionPreview, { execute, draft: { pageId, setPageId, text, setText, draftRevision } }) : null;
261
261
  }
262
- function Preview({ sessionId, execute, useTabInfo }) {
262
+ function PresetPreview({ execute, visible }) {
263
+ const [state, setState] = useState(null);
264
+ useEffect(() => {
265
+ let active = true;
266
+ execute('availability').then(value => { if (active) setState(value); }, error => { if (active) setState({ error: error.message }); });
267
+ return () => { active = false; };
268
+ }, [execute]);
269
+ if (!state) return h('p', { role: 'status' }, 'Checking DSCODE session…');
270
+ if (state.error) return h('p', { role: 'alert' }, state.error);
271
+ if (!state.available) return h('section', { className: 'dscode-ui', style: { padding: 16 } }, h('h3', null, 'DSCODE Browser preview'),
272
+ h('p', null, 'This session uses another preset. Create a new session and select DSCODE to use this preview. Its original browser tools remain available.'));
273
+ return h(SessionPane, { execute, visible });
274
+ }
275
+ function Preview({ sessionId, execute, useTabInfo, presetOnly }) {
263
276
  const { tab } = useTabInfo();
264
277
  // Session changes discard the draft. Hidden panes discard their request
265
278
  // lock and preview, while late responses stay bound to the original session.
266
- return h(SessionPane, { key: sessionId, execute, visible: tab.visible });
279
+ return h(presetOnly ? PresetPreview : SessionPane, { key: sessionId, execute, visible: tab.visible });
267
280
  }
268
281
  return {
269
282
  inject: ['slots', 'sidebarRightTabs', 'connection'],
270
- apply(ctx) {
283
+ apply(ctx, options = {}) {
271
284
  ctx.effect(() => ctx.sidebarRightTabs.register({ id, kind: 'dscode-browser', multiple: false, keepMounted: true, title: () => 'Browser preview', guide: [{ id: 'open', order: 31, title: () => 'Browser preview', description: () => 'Inspect the controlled Chrome tab and send a visual annotation.' }] }), 'dscode-browser-preview.type');
272
285
  ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({ name: 'sidebar.right.pane.tab', key: id,
273
- inject: sessionId => ({ sessionId, execute: async (action, args = {}) => {
286
+ inject: sessionId => ({ sessionId, presetOnly: options.presetOnly === true, execute: async (action, args = {}) => {
274
287
  const response = await ctx.connection.rpc.call('/api', 'dscode-browser', { sessionId, action, ...args });
275
288
  if (!response.ok) throw Error(response.error.message);
276
289
  return response.value;
@@ -13,6 +13,11 @@ export function apply(ctx) {
13
13
  if (!payload || typeof payload !== 'object' || typeof payload.sessionId !== 'string') throw Error('Choose a live session.');
14
14
  const agent = ctx.agents.get(payload.sessionId);
15
15
  if (!agent) throw Error('The session is no longer active. Open it before using Browser preview.');
16
+ const available = !ctx.get('dscodeDesktop') || agent.session.header.agentPreset === 'dscode';
17
+ if (action === 'availability') return { ok: true, value: { available } };
18
+ if (!available) {
19
+ throw Error('Browser preview belongs to DSCODE. Create a new session with the DSCODE preset; this session keeps its original browser tools.');
20
+ }
16
21
  if (action === 'start' || action === 'stop' || action === 'resume') {
17
22
  if (action === 'resume') {
18
23
  const browser = browserForAgent(agent);
@@ -19,9 +19,11 @@ const LOGINS = new Set(PROVIDERS.filter(provider => provider.login).map(provider
19
19
  export default class DscodeCredentials extends LocalCredentialProvider {
20
20
  shared;
21
21
  sharedConfig;
22
+ desktop;
22
23
  constructor(ctx, config = {}) {
23
24
  // Retain the old profile store, including other providers and OAuth records.
24
25
  super(ctx, { ...config, path: undefined });
26
+ this.desktop = config.desktop === true;
25
27
  this.sharedConfig = { ...config, path: config.path ?? join(homedir(), '.dscode', 'credentials.yaml') };
26
28
  }
27
29
  async *[Service.init]() {
@@ -34,6 +36,10 @@ export default class DscodeCredentials extends LocalCredentialProvider {
34
36
  yield* super[Service.init]();
35
37
  }
36
38
  async resolve(ref) {
39
+ if (this.desktop) {
40
+ const native = await super.resolve(ref);
41
+ if (native) return native;
42
+ }
37
43
  // dscode: the Grok subscription rail is the official CLI login, read-only. DSCODE never
38
44
  // writes that file: the CLI owns refresh-token rotation, and two writers log the other out.
39
45
  if (ref === GROK_TOKEN_REF) {
@@ -47,6 +53,10 @@ export default class DscodeCredentials extends LocalCredentialProvider {
47
53
  return super.resolve(ref);
48
54
  }
49
55
  async describe(ref) {
56
+ if (this.desktop) {
57
+ const native = await super.describe(ref);
58
+ if (native.configured) return LOGINS.has(ref) ? { ...native, source: 'account', writable: false } : native;
59
+ }
50
60
  if (ref === GROK_TOKEN_REF) {
51
61
  const state = grokAuthState();
52
62
  return state.kind === 'ready' ? { configured: true, source: 'file', writable: false } : { configured: false, writable: false };
@@ -63,6 +73,7 @@ export default class DscodeCredentials extends LocalCredentialProvider {
63
73
  set(ref, value) {
64
74
  // The CLI file is not a DSCODE store: saving here would go somewhere nothing reads.
65
75
  if (ref === GROK_TOKEN_REF) throw new Error('GROK_CLI_TOKEN comes from ~/.grok/auth.json; run grok login instead');
76
+ if (this.desktop) return super.set(ref, value);
66
77
  return (SHARED.has(ref) || isCustomKey(ref)) ? this.shared.set(ref, value) : super.set(ref, value);
67
78
  }
68
79
  async unset(ref) {
@@ -3,7 +3,7 @@ globalThis.__ModuleLoader__.load({
3
3
  id: '@toddzheng024/dscode-desktop',
4
4
  factory: require => {
5
5
  const { createElement: h, useState, useEffect, useRef } = require('react');
6
- const control = { padding: '8px 10px', border: '1px solid #8886', borderRadius: 6, background: 'transparent', color: 'inherit', font: 'inherit', boxSizing: 'border-box' };
6
+ const control = { boxSizing: 'border-box', minWidth: 0, maxWidth: '100%' };
7
7
  const row = { display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center' };
8
8
  const resetModel = model => {
9
9
  const next = { ...model, thinking: 'default', inputModalities: ['text'] };
@@ -49,10 +49,10 @@ globalThis.__ModuleLoader__.load({
49
49
  h('input', { 'aria-label': label, style: { ...control, width: '100%' }, value: value ?? '', onChange: event => onChange(event.target.value), ...options }));
50
50
  const choice = (label, value, choices, onChange) => h('label', { style: { display: 'grid', gap: 5, flex: 1 } }, label,
51
51
  h('select', { 'aria-label': label, value, onChange: event => onChange(event.target.value), style: control }, ...choices.map(([id, text]) => h('option', { key: id, value: id }, text))));
52
- return h('section', { 'aria-label': 'DSCODE model settings', style: { padding: 20, display: 'grid', gap: 16, maxWidth: 850, color: 'inherit' } },
53
- h('h2', { style: { margin: 0 } }, 'DSCODE models'),
52
+ return h('section', { className: 'dscode-ui', 'aria-label': 'DSCODE model settings', style: { padding: 0, display: 'grid', gap: 16, maxWidth: 850, color: 'inherit' } },
53
+ h('h2', { style: { margin: 0 } }, 'Models'),
54
54
  h('p', { style: { margin: 0 } }, 'Connect a private or self-hosted model service. Saved models appear in the model picker.'),
55
- error && h('div', { role: 'alert', style: { color: '#d85b5b', whiteSpace: 'pre-wrap' } }, error),
55
+ error && h('div', { role: 'alert', style: { color: 'var(--dsw-alias-state-error-primary)', whiteSpace: 'pre-wrap' } }, error),
56
56
  notice && h('div', { role: 'status' }, notice),
57
57
  busy && request.current && h('button', { type: 'button', style: control, disabled: request.current.signal.aborted,
58
58
  onClick: () => { request.current?.abort(); setNotice('Cancelling request…'); },
@@ -85,8 +85,8 @@ globalThis.__ModuleLoader__.load({
85
85
  change({ backend: found.backend, models });
86
86
  setNotice(`Found ${found.models.length} models. Confirm the context size and image support before saving.`);
87
87
  }, false, true), button('Add model', async () => change({ models: [...draft.models, { id: '', thinking: 'default', inputModalities: ['text'] }] }))),
88
- ...draft.models.map((model, index) => h('fieldset', { key: index, style: { border: '1px solid #8886', borderRadius: 8, padding: 14, display: 'grid', gap: 12 } },
89
- h('legend', null, `Model ${index + 1}`),
88
+ ...draft.models.map((model, index) => h('fieldset', { className: 'dscode-card', key: index, style: { border: '.5px solid var(--dsw-alias-settings-card-stroke)', borderRadius: 'var(--dsw-radius-xl)', padding: 14, display: 'grid', gap: 12 } },
89
+ h('h3', null, `Model ${index + 1}`),
90
90
  h('div', { style: row }, input(`Model ${index + 1} ID`, model.id, id => change({ models: draft.models.map((entry, i) => i === index ? { ...resetModel(entry), id } : entry) })),
91
91
  input(`Model ${index + 1} context`, model.contextWindow, value => modelChange(index, { contextWindow: value ? Number(value) : undefined, contextSource: 'user' }), { type: 'number', min: 1, step: 1 }),
92
92
  input(`Model ${index + 1} output limit`, model.maxTokens, value => modelChange(index, { maxTokens: value ? Number(value) : undefined, outputSource: 'user' }), { type: 'number', min: 1, step: 1 })),
@@ -115,8 +115,7 @@ globalThis.__ModuleLoader__.load({
115
115
  }
116
116
  return {
117
117
  inject: ['slots', 'connection'],
118
- apply(ctx) {
119
- ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dscode-models', order: 11, label: () => 'DSCODE models',
118
+ settingsPage(ctx) { return { entry: { name: 'settings.section', id: 'dscode-models', order: 11, label: () => 'Models',
120
119
  inject: () => ({ execute: async (action, args = {}, signal) => {
121
120
  signal?.throwIfAborted();
122
121
  const requestId = signal ? crypto.randomUUID() : undefined;
@@ -143,7 +142,11 @@ globalThis.__ModuleLoader__.load({
143
142
  if (!response.ok) throw Error(response.error.message);
144
143
  return response.value;
145
144
  } }),
146
- }, Settings));
145
+ }, Component: Settings }; },
146
+ apply(ctx, options = {}) {
147
+ if (options.settings === false) return;
148
+ const page = this.settingsPage(ctx);
149
+ ctx.slots.inject('settings.section', () => ctx.slots.register(page.entry, page.Component));
147
150
  },
148
151
  };
149
152
  },