@wisdoverse/dsh-inline-media-viewer 1.0.10 → 1.0.12

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.12
4
+
5
+ - Verify the current source on DSH v0.2.0-rc.2: Web settings load and persist, authenticated ComfyUI image reads succeed, and unauthenticated reads are rejected; the plugin-facing APIs used here are unchanged from v0.1.7-rc.2.
6
+ - Replace the removed `settingsScope` client service with live `Config` fields and `configForms`, fixing plugins stuck pending on DSH v0.1.7 and v0.2.0.
7
+ - Preserve remote read-only media preferences through the authenticated RPC channel.
8
+ - Register media endpoints on DSH's authenticated `/api` bridge; real v0.1.7 testing caught the old custom channel returning HTTP 405.
9
+ - Render the turn-tail media card from the slot's actual turn props; verified image display and enlargement in the v0.1.7-rc.2 Web UI.
10
+ - Align the profile entry id with the old `inline-media` settings section for migration.
11
+
12
+ ## 1.0.11
13
+
14
+ - Register the authenticated RPC route inside a connection-injected context so
15
+ Cordis can provide the Web server dependency during deferred setup.
16
+ - Verify host startup, the settings section, and HTTP health on DSH v0.1.5-rc.1.
17
+
3
18
  ## 1.0.10
4
19
 
5
20
  - Promote 1.0.10-alpha.1 to the stable release without runtime changes.
package/README.md CHANGED
@@ -59,14 +59,14 @@ audio/sound.mp3 → <audio controls>
59
59
 
60
60
  ## Installation
61
61
 
62
- Targets DSH **v0.1.3-alpha.1** APIs. Host startup was also verified on **v0.1.2-rc.1**. Browser media playback and the final DSH v0.1.3 release have not been verified.
62
+ Version **1.0.12** supports DSH **v0.1.7-rc.2** and **v0.2.0-rc.2**. On v0.2.0-rc.2, the plugin loaded in the real Web UI, settings remained saved after a refresh, and the authenticated ComfyUI image route returned a decodable image; unauthenticated access returned HTTP 401. Local image reads and the inline image card were verified on v0.1.7-rc.2, but have not been retested on v0.2.0-rc.2. Video and audio playback remain unverified. Upgrade from v1.0.11 to remove its obsolete `settingsScope` dependency.
63
63
 
64
64
  ### From npm
65
65
 
66
66
  Install and activate the bundle in your Web profile:
67
67
 
68
68
  ```bash
69
- dsh plugin --profile web add @wisdoverse/dsh-inline-media-viewer@1.0.10
69
+ dsh plugin --profile web add @wisdoverse/dsh-inline-media-viewer@1.0.12
70
70
  ```
71
71
 
72
72
  Restart the Web profile after installation.
@@ -97,7 +97,7 @@ Restart the Web profile after installation.
97
97
 
98
98
  3. Rebuild or restart the Web profile.
99
99
 
100
- The plugin mounts its host RPC channel, client projection, and settings section
100
+ The plugin mounts its host API routes, client projection, and settings section
101
101
  through [`cordis.patch.yml`](cordis.patch.yml).
102
102
 
103
103
  ## Configuration
@@ -114,9 +114,10 @@ switch live between English and Chinese.
114
114
  | Maximum media height | `380 px` | `160`–`1200 px` |
115
115
  | ComfyUI origin (optional) | Empty | `http(s)://host[:port]` |
116
116
 
117
- Settings are stored in the DSH settings document. Writes are available only on
118
- loopback connections; remote browsers can read settings but cannot persist
119
- changes.
117
+ Settings are stored in the active DSH profile configuration. DSH imports the
118
+ previous `inline-media` section from `settings.yaml` when upgrading. Writes are
119
+ available only on loopback connections; remote browsers can read these four
120
+ settings through the plugin's authenticated RPC channel but cannot change them.
120
121
 
121
122
  ### ComfyUI origin
122
123
 
@@ -156,6 +157,7 @@ limitations.
156
157
  | Command | Purpose |
157
158
  | --- | --- |
158
159
  | `node test.mjs` | Run the dependency-free unit tests directly. |
160
+ | `node test.mjs --host` | Run the host registration check after installing peer dependencies. |
159
161
  | `npm test` | Run the same unit test suite through npm. |
160
162
  | `npm run lint` | Syntax-check the host modules and test file. |
161
163
 
@@ -163,7 +165,7 @@ limitations.
163
165
 
164
166
  | Path | Responsibility |
165
167
  | --- | --- |
166
- | `index.js` | Host RPC channel, ComfyUI proxy, and settings registration. |
168
+ | `index.js` | Host API routes, ComfyUI proxy, and settings registration. |
167
169
  | `lib.js` | Pure, dependency-free helpers. |
168
170
  | `client/client.js` | Turn projection, media renderer, and settings UI. |
169
171
  | `cordis.patch.yml` | Bundle mounting and integration points. |
package/README.zh-CN.md CHANGED
@@ -57,14 +57,14 @@ audio/sound.mp3 → <audio controls>
57
57
 
58
58
  ## 安装
59
59
 
60
- 适配 DSH **v0.1.3-alpha.1** API,并已在 **v0.1.2-rc.1** 验证宿主启动。浏览器媒体播放及 DSH v0.1.3 正式版尚未验证。
60
+ **1.0.12** 兼容 DSH **v0.1.7-rc.2** 和 **v0.2.0-rc.2**。在 v0.2.0-rc.2 的真实 Web 页面中,插件正常加载,设置刷新后仍能读回,带认证的 ComfyUI 图片接口返回可解码图片,未认证访问返回 HTTP 401。本地图片读取和内联图片卡片已在 v0.1.7-rc.2 验证,尚未在 v0.2.0-rc.2 重新测试;视频和音频播放也尚未验证。请从 v1.0.11 升级,以移除已废弃的 `settingsScope` 依赖。
61
61
 
62
62
  ### 通过 npm 安装
63
63
 
64
64
  在 Web profile 中安装并启用 Bundle:
65
65
 
66
66
  ```bash
67
- dsh plugin --profile web add @wisdoverse/dsh-inline-media-viewer@1.0.10
67
+ dsh plugin --profile web add @wisdoverse/dsh-inline-media-viewer@1.0.12
68
68
  ```
69
69
 
70
70
  安装后重启 Web profile。
@@ -95,7 +95,7 @@ dsh plugin --profile web add @wisdoverse/dsh-inline-media-viewer@1.0.10
95
95
 
96
96
  3. 重新构建或重启 Web profile。
97
97
 
98
- 插件通过 [`cordis.patch.yml`](cordis.patch.yml) 挂载宿主 RPC 通道、客户端投影和设置区域。
98
+ 插件通过 [`cordis.patch.yml`](cordis.patch.yml) 挂载宿主 API 路由、客户端投影和设置区域。
99
99
 
100
100
  ## 配置
101
101
 
@@ -110,7 +110,7 @@ dsh plugin --profile web add @wisdoverse/dsh-inline-media-viewer@1.0.10
110
110
  | 媒体最大高度 | `380 px` | `160`–`1200 px` |
111
111
  | ComfyUI 源站(可选) | 留空 | `http(s)://host[:port]` |
112
112
 
113
- 设置值保存在 DSH 设置文档中。只有回环连接可以写入;远程浏览器可以读取设置,但不能持久化修改。
113
+ 设置值保存在当前 DSH profile 配置中。升级时,DSH 会导入旧 `settings.yaml` 中的 `inline-media` 设置。只有回环连接可以写入;远程浏览器可通过插件的认证 RPC 通道读取这四项设置,但不能修改。
114
114
 
115
115
  ### ComfyUI 源站
116
116
 
@@ -144,6 +144,7 @@ ComfyUI 来源。非空的无效地址会明确报错,而不会静默回退到
144
144
  | 命令 | 用途 |
145
145
  | --- | --- |
146
146
  | `node test.mjs` | 直接运行无依赖的单元测试。 |
147
+ | `node test.mjs --host` | 安装 peer 依赖后运行宿主注册检查。 |
147
148
  | `npm test` | 通过 npm 运行同一套单元测试。 |
148
149
  | `npm run lint` | 对宿主模块和测试文件执行语法检查。 |
149
150
 
@@ -151,7 +152,7 @@ ComfyUI 来源。非空的无效地址会明确报错,而不会静默回退到
151
152
 
152
153
  | 路径 | 职责 |
153
154
  | --- | --- |
154
- | `index.js` | 宿主 RPC 通道、ComfyUI 代理和设置注册。 |
155
+ | `index.js` | 宿主 API 路由、ComfyUI 代理和设置注册。 |
155
156
  | `lib.js` | 无依赖的纯辅助函数。 |
156
157
  | `client/client.js` | 消息投影、媒体渲染器和设置界面。 |
157
158
  | `cordis.patch.yml` | Bundle 挂载和集成点。 |
package/SECURITY.md CHANGED
@@ -39,9 +39,8 @@ exfiltration, no arbitrary SSRF, no unbounded memory use.
39
39
 
40
40
  ## Transport
41
41
 
42
- - The media channel `/inline-media/read` is registered with the DSH
43
- connection service (`authority: "trusted-host"`), so it runs under the same
44
- browser-trust fence and session gating as DSH's own RPC surface.
42
+ - The media route `/api/inline-media.read` uses DSH's authenticated `/api`
43
+ bridge and browser-trust fence.
45
44
  - The client renderer never receives file system paths back — only `data:`
46
45
  URLs — so a hostile session cannot use the channel as a file oracle for
47
46
  paths the UI already knows.
@@ -53,6 +52,8 @@ exfiltration, no arbitrary SSRF, no unbounded memory use.
53
52
  comfyUrl string ≤ 512 chars, parsed to a bare origin at use time).
54
53
  - The settings service only writes back for **loopback** connections; remote
55
54
  browsers are read-only, so a remote viewer cannot persist state.
55
+ - The plugin's authenticated `settings` RPC returns only its four non-secret
56
+ display and ComfyUI preferences to remote browsers.
56
57
 
57
58
  ## Known limitations
58
59
 
package/client/client.js CHANGED
@@ -7,15 +7,16 @@ window.__ModuleLoader__.load({
7
7
 
8
8
  const React = require("react");
9
9
  const { createElement: h, useEffect, useMemo, useState } = React;
10
- const CHANNEL = "/inline-media";
11
- const ENDPOINT = "read";
10
+ const CHANNEL = "/api";
11
+ const ENDPOINT = "inline-media.read";
12
12
  const DISPLAY_CAP = 12;
13
13
  const SETTINGS_NAMESPACE = "inline-media";
14
14
  // Empty config = the host-side built-in default
15
15
  // (http://127.0.0.1:8188, ComfyUI's standard local address).
16
16
  const COMFY_DEFAULT_URL = "";
17
17
  const DEFAULT_SETTINGS = Object.freeze({ autoRender: true, displayCap: DISPLAY_CAP, imageMaxPx: 380, comfyUrl: COMFY_DEFAULT_URL });
18
- let settingsScope = null;
18
+ let settingsForm = null;
19
+ let remoteSettingsRequest = null;
19
20
 
20
21
  function clampInt(raw, min, max, fallback) {
21
22
  const n = Number.parseInt(String(raw), 10);
@@ -23,10 +24,8 @@ window.__ModuleLoader__.load({
23
24
  return Math.min(max, Math.max(min, n));
24
25
  }
25
26
 
26
- function readSettings() {
27
- if (settingsScope === null) return DEFAULT_SETTINGS;
28
- const snapshot = settingsScope.getSnapshot();
29
- const value = snapshot && snapshot.value;
27
+ function readSettings(remoteSettings) {
28
+ const value = settingsForm && settingsForm.getSnapshot().value || remoteSettings;
30
29
  if (!value || typeof value !== "object") return DEFAULT_SETTINGS;
31
30
  return {
32
31
  autoRender: value.autoRender !== false,
@@ -37,6 +36,20 @@ window.__ModuleLoader__.load({
37
36
  : DEFAULT_SETTINGS.comfyUrl,
38
37
  };
39
38
  }
39
+
40
+ function fetchRemoteSettings(connection) {
41
+ if (remoteSettingsRequest === null) {
42
+ remoteSettingsRequest = connection.rpc.call(CHANNEL, "inline-media.settings", {}).then((result) => {
43
+ if (result.ok) return result.value;
44
+ remoteSettingsRequest = null;
45
+ return null;
46
+ }).catch(() => {
47
+ remoteSettingsRequest = null;
48
+ return null;
49
+ });
50
+ }
51
+ return remoteSettingsRequest;
52
+ }
40
53
  const MEDIA_EXTENSIONS = new Set([
41
54
  "png", "jpg", "jpeg", "webp", "gif", "avif", "bmp",
42
55
  "mp4", "webm", "mov", "m4v", "mkv", "avi", "ogv",
@@ -159,8 +172,6 @@ window.__ModuleLoader__.load({
159
172
  };
160
173
 
161
174
  function selectMedia(owner) {
162
- const settings = readSettings();
163
- if (!settings.autoRender) return null;
164
175
  const text = [];
165
176
  const mentions = owner.turn.data && owner.turn.data.get("inline-media-mentions");
166
177
  if (mentions) {
@@ -173,7 +184,7 @@ window.__ModuleLoader__.load({
173
184
  .filter((block) => block && block.kind === "text" && typeof block.text === "string")
174
185
  .map((block) => block.text));
175
186
  }
176
- const candidates = extractCandidates(text.join("\n")).slice(0, settings.displayCap);
187
+ const candidates = extractCandidates(text.join("\n"));
177
188
  return candidates.length === 0 ? null : candidates;
178
189
  }
179
190
 
@@ -363,9 +374,22 @@ window.__ModuleLoader__.load({
363
374
  })) : null);
364
375
  }
365
376
 
366
- function MediaTail({ matched, connection, sessionId }) {
367
- const settings = readSettings();
377
+ function MediaTailContent({ matched, connection, sessionId }) {
378
+ const [remoteSettings, setRemoteSettings] = useState(null);
379
+ const [settings, setSettings] = useState(() => readSettings());
380
+ useEffect(() => {
381
+ let active = true;
382
+ void fetchRemoteSettings(connection).then((value) => {
383
+ if (active) setRemoteSettings(value);
384
+ });
385
+ return () => { active = false; };
386
+ }, [connection]);
387
+ useEffect(() => {
388
+ setSettings(readSettings(remoteSettings));
389
+ return settingsForm?.subscribe(() => setSettings(readSettings(remoteSettings)));
390
+ }, [remoteSettings]);
368
391
  const candidates = useMemo(() => matched.slice(0, settings.displayCap), [matched, settings.displayCap]);
392
+ if (!settings.autoRender) return null;
369
393
  return h("section", {
370
394
  "aria-label": "媒体预览",
371
395
  style: { marginTop: 14 },
@@ -393,11 +417,16 @@ window.__ModuleLoader__.load({
393
417
  }))));
394
418
  }
395
419
 
420
+ function MediaTail(props) {
421
+ const matched = selectMedia(props);
422
+ return matched === null ? null : h(MediaTailContent, { ...props, matched });
423
+ }
424
+
396
425
  const SETTINGS_NS = "inlineMedia";
397
426
  const SETTINGS_DICT = {
398
427
  zh: {
399
428
  nav: "媒体预览",
400
- intro: "调整聊天中内联媒体预览的行为。设置会持久保存到本机设置文档。",
429
+ intro: "调整聊天中内联媒体预览的行为。设置会保存到当前 profile 配置。",
401
430
  autoRender: "自动渲染检测到的媒体",
402
431
  autoRenderHint: "关闭后仅扫描不展示(仍会扫描消息)。",
403
432
  displayCap: "每回合最多显示",
@@ -412,7 +441,7 @@ window.__ModuleLoader__.load({
412
441
  },
413
442
  en: {
414
443
  nav: "Media preview",
415
- intro: "Tune how inline media is previewed in conversations. Changes persist to the local settings document.",
444
+ intro: "Tune how inline media is previewed in conversations. Changes persist to the active profile configuration.",
416
445
  autoRender: "Auto-render detected media",
417
446
  autoRenderHint: "When off, mentions are still scanned but nothing is shown.",
418
447
  displayCap: "Max items per turn",
@@ -427,10 +456,19 @@ window.__ModuleLoader__.load({
427
456
  },
428
457
  };
429
458
 
430
- function MediaSettingsSection({ scope, t }) {
431
- const [snap, setSnap] = useState(scope ? scope.getSnapshot() : null);
432
- useEffect(() => (scope ? scope.subscribe(() => setSnap(scope.getSnapshot())) : undefined), [scope]);
433
- const stored = snap && snap.value && typeof snap.value === "object" ? snap.value : DEFAULT_SETTINGS;
459
+ function MediaSettingsSection({ form, connection, t }) {
460
+ const [snap, setSnap] = useState(form ? form.getSnapshot() : null);
461
+ const [remoteSettings, setRemoteSettings] = useState(null);
462
+ useEffect(() => (form ? form.subscribe(() => setSnap(form.getSnapshot())) : undefined), [form]);
463
+ useEffect(() => {
464
+ let active = true;
465
+ void fetchRemoteSettings(connection).then((value) => {
466
+ if (active) setRemoteSettings(value);
467
+ });
468
+ return () => { active = false; };
469
+ }, [connection]);
470
+ const stored = snap && snap.value && typeof snap.value === "object"
471
+ ? snap.value : remoteSettings || DEFAULT_SETTINGS;
434
472
  const writable = snap ? snap.writable === true : false;
435
473
  const comfyUrlValue = String(typeof stored.comfyUrl === "string" ? stored.comfyUrl : DEFAULT_SETTINGS.comfyUrl);
436
474
  const comfyUrlInvalid = comfyUrlValue.trim() !== "" && !normalizeComfyOrigin(comfyUrlValue);
@@ -438,15 +476,14 @@ window.__ModuleLoader__.load({
438
476
  setSnap((prev) => (prev
439
477
  ? { ...prev, value: { ...(prev.value || DEFAULT_SETTINGS), [field]: next } }
440
478
  : prev));
441
- if (scope && writable) scope.set(field, next);
479
+ if (form && writable) void form.set(field, next).catch(() => setSnap(form.getSnapshot()));
442
480
  };
443
481
  const reset = () => {
444
- if (!scope || !writable) return;
482
+ if (!form || !writable) return;
445
483
  setSnap((prev) => (prev ? { ...prev, value: DEFAULT_SETTINGS } : prev));
446
- scope.unset("autoRender");
447
- scope.unset("displayCap");
448
- scope.unset("imageMaxPx");
449
- scope.unset("comfyUrl");
484
+ void form.mutate(["autoRender", "displayCap", "imageMaxPx", "comfyUrl"]
485
+ .map((field) => ({ op: "unset", path: [field] })))
486
+ .catch(() => setSnap(form.getSnapshot()));
450
487
  };
451
488
  const field = (label, hint, node) => h("label", {
452
489
  style: { display: "grid", gap: 4, margin: "14px 0 0" },
@@ -542,18 +579,22 @@ window.__ModuleLoader__.load({
542
579
  }
543
580
 
544
581
  const name = "dsh-inline-media-viewer";
545
- const inject = ["slots", "connection", "settingsScope", "locale", "uiConversation"];
582
+ const inject = ["slots", "connection", "configForms", "locale", "uiConversation"];
546
583
 
547
584
  function apply(ctx) {
548
585
  const connection = ctx.get("connection");
549
586
  ctx.uiConversation.events.register(mediaMentionsDefinition);
550
587
  ctx.effect(() => ctx.locale.register(SETTINGS_NS, SETTINGS_DICT), "inline-media: dictionaries");
551
588
  const t = ctx.locale.bind(SETTINGS_NS);
552
- const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
553
- settingsScope = scope;
589
+ const form = ctx.configForms.get(SETTINGS_NAMESPACE);
590
+ settingsForm = form;
591
+ ctx.effect(() => () => {
592
+ settingsForm = null;
593
+ remoteSettingsRequest = null;
594
+ }, "inline-media: client settings");
554
595
  ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
555
596
  name: "conversation.chat.turnTail",
556
- select: selectMedia,
597
+ id: "inline-media",
557
598
  inject: () => ({ connection }),
558
599
  }, MediaTail));
559
600
  ctx.slots.inject("settings.section", () => ctx.slots.register({
@@ -562,14 +603,14 @@ window.__ModuleLoader__.load({
562
603
  order: 25,
563
604
  label: () => t("nav"),
564
605
  locale: SETTINGS_NS,
565
- inject: () => ({ scope }),
606
+ inject: () => ({ form, connection }),
566
607
  }, MediaSettingsSection));
567
608
  }
568
609
 
569
610
  exports.apply = apply;
570
611
  exports.inject = inject;
571
612
  exports.name = name;
572
- exports.testing = Object.freeze({ extractCandidates, mediaMentionsDefinition, mediaTransport, selectMedia, toolArgumentText });
613
+ exports.testing = Object.freeze({ extractCandidates, fetchRemoteSettings, mediaMentionsDefinition, mediaTransport, readSettings, selectMedia, toolArgumentText });
573
614
  return module.exports;
574
615
  },
575
616
  });
package/cordis.patch.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  - insert:
2
- - id: inline-media-viewer
2
+ - id: inline-media
3
3
  name: "@wisdoverse/dsh-inline-media-viewer"
package/index.js CHANGED
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * dsh-inline-media-viewer — host half.
3
3
  *
4
- * Serves media bytes for the web client over a dedicated RPC channel
5
- * (`/inline-media/read`), reading workspace-local files under the calling
4
+ * Serves media bytes for the web client over authenticated DSH API routes
5
+ * (`/api/inline-media.read`), reading workspace-local files under the calling
6
6
  * session's workspace root and proxying ComfyUI media URLs onto the
7
7
  * user-configured ComfyUI origin (default: `http://127.0.0.1:8188`,
8
- * ComfyUI's standard local address). Registers a persistent user settings
9
- * namespace (`inline-media`) so the client can tune display preferences
10
- * and the ComfyUI server address.
8
+ * ComfyUI's standard local address). Its live Config fields are edited
9
+ * through DSH's configuration forms.
11
10
  *
12
11
  * Security model (see SECURITY.md): local reads are confined to the session
13
12
  * workspace (realpath + containment), remote reads always fetch from the
@@ -28,20 +27,11 @@ export { testing } from "./lib.js";
28
27
  export const name = "dsh-inline-media-viewer";
29
28
  export const inject = ["connection", "sessions", "sessionQuery", "settings"];
30
29
 
31
- const CHANNEL = "/inline-media";
32
30
  const ENDPOINT = "read";
33
31
 
34
32
  /** Persistent user settings namespace for this plugin. */
35
33
  export const MEDIA_SETTINGS_NAMESPACE = "inline-media";
36
34
 
37
- /** Schema of the user settings section. */
38
- export const MEDIA_SETTINGS_SCHEMA = z.object({
39
- autoRender: z.boolean().required(),
40
- displayCap: z.number().step(1).min(1).max(30).required(),
41
- imageMaxPx: z.number().step(1).min(160).max(1200).required(),
42
- comfyUrl: z.string().max(512).required(),
43
- });
44
-
45
35
  /** Composition defaults; the user layer overrides these. */
46
36
  export const MEDIA_SETTINGS_DEFAULTS = Object.freeze({
47
37
  autoRender: true,
@@ -52,6 +42,14 @@ export const MEDIA_SETTINGS_DEFAULTS = Object.freeze({
52
42
  comfyUrl: "",
53
43
  });
54
44
 
45
+ /** Live configuration projected into DSH's settings forms. */
46
+ export const Config = z.object({
47
+ autoRender: z.boolean().default(MEDIA_SETTINGS_DEFAULTS.autoRender).volatile(),
48
+ displayCap: z.number().step(1).min(1).max(30).default(MEDIA_SETTINGS_DEFAULTS.displayCap).volatile(),
49
+ imageMaxPx: z.number().step(1).min(160).max(1200).default(MEDIA_SETTINGS_DEFAULTS.imageMaxPx).volatile(),
50
+ comfyUrl: z.string().max(512).default(MEDIA_SETTINGS_DEFAULTS.comfyUrl).volatile(),
51
+ });
52
+
55
53
  function success(value) {
56
54
  return { ok: true, value };
57
55
  }
@@ -136,6 +134,7 @@ async function readLocal(ctx, source, sessionId) {
136
134
  }
137
135
 
138
136
  async function handleRead(ctx, endpoint, payload, signal, resolveSettings) {
137
+ if (endpoint === "settings") return success(resolveSettings());
139
138
  if (endpoint !== ENDPOINT) return failure("unknown inline-media endpoint");
140
139
  if (!payload || typeof payload !== "object") return failure("invalid request");
141
140
  const { source, sessionId } = payload;
@@ -155,20 +154,31 @@ async function handleRead(ctx, endpoint, payload, signal, resolveSettings) {
155
154
  }
156
155
  }
157
156
 
158
- export function apply(ctx) {
159
- const settings = ctx.settings.register(MEDIA_SETTINGS_NAMESPACE, MEDIA_SETTINGS_SCHEMA, {
160
- base: MEDIA_SETTINGS_DEFAULTS,
157
+ export function apply(ctx, config) {
158
+ ctx.effect(() => ctx.settings.configure({ auto: false }, ctx.fiber), "inline-media: settings page");
159
+ const resolveSettings = () => ({
160
+ autoRender: config.autoRender.get(),
161
+ displayCap: config.displayCap.get(),
162
+ imageMaxPx: config.imageMaxPx.get(),
163
+ comfyUrl: config.comfyUrl.get(),
164
+ });
165
+ ctx.inject(["connection"], (connectionCtx) => {
166
+ for (const endpoint of ["read", "settings"]) {
167
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
168
+ path: `/api/inline-media.${endpoint}`,
169
+ methods: ["POST"],
170
+ requestBody: "buffered",
171
+ async fetch(request) {
172
+ let message;
173
+ try { message = await request.json(); } catch { return new Response("invalid request", { status: 400 }); }
174
+ if (message?.type !== "client-request" || typeof message.rpcId !== "string" ||
175
+ message.method !== `inline-media.${endpoint}`) return new Response("invalid request", { status: 400 });
176
+ const result = await handleRead(ctx, endpoint, message.payload, request.signal, resolveSettings);
177
+ return Response.json({ type: "server-response", rpcId: message.rpcId, result });
178
+ },
179
+ }), `inline-media: ${endpoint} route`);
180
+ }
161
181
  });
162
- const resolveSettings = () => settings.get();
163
- ctx.effect(() => {
164
- const dispose = ctx.connection.rpc.handle(
165
- CHANNEL,
166
- (endpoint, payload, signal) => handleRead(ctx, endpoint, payload, signal, resolveSettings),
167
- );
168
- return () => {
169
- void dispose();
170
- };
171
- }, "inline-media: rpc");
172
182
  }
173
183
 
174
184
  export { MIME } from "./lib.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wisdoverse/dsh-inline-media-viewer",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Persistent inline image, video, and audio previews for DeepSeek Harness Web conversations, with workspace-confined local reads and an optional ComfyUI proxy.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -53,8 +53,8 @@
53
53
  "lint": "node --check index.js && node --check lib.js && node --check client/client.js && node --check test.mjs"
54
54
  },
55
55
  "peerDependencies": {
56
- "@deepseek-ai/cordis": "^4.0.1",
57
- "@deepseek-ai/schemastery": "^3.18.1"
56
+ "@deepseek-ai/cordis": "^4.0.4",
57
+ "@deepseek-ai/schemastery": "^3.18.4"
58
58
  },
59
59
  "dsh": {
60
60
  "bundle": {