dsh-vision-router 1.2.2 → 1.2.3

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/README.md CHANGED
@@ -28,9 +28,11 @@
28
28
  <p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
29
29
 
30
30
  > [!WARNING]
31
- > 📌 **Announcement (v1.2.2)**
31
+ > 📌 **Announcement (v1.2.3)**
32
32
  >
33
- > v1.2.2 closes the last attachment-id gap — ids announced for images the host persisted itself (e.g. `read_image` re-uploads, `sha256:…`) now resolve in `vision_describe` and every pixel tool (issue #72) — stops `vision_present` and other tool-result image blocks from ever locking a text-model session with `UNSUPPORTED_CONTENT` (issue #74; already-locked sessions heal after upgrading), and warns loudly when a stale sharp left over from a pre-v1.2 upgrade would break the pixel tools with `colourspace: parameter space not set` (issue #75).
33
+ > v1.2.3 fixes DSH Desktop's re-appearing first-run dialog: the onboarding "seen" flag and the model-guide step now persist in the profile settings file instead of origin-scoped `localStorage`, which a random per-launch port (`--port 0`) wiped on every boot (issue #78).
34
+ >
35
+ > v1.2.2 closed the last attachment-id gap — ids announced for images the host persisted itself (e.g. `read_image` re-uploads, `sha256:…`) now resolve in `vision_describe` and every pixel tool (issue #72) — stopped `vision_present` and other tool-result image blocks from ever locking a text-model session with `UNSUPPORTED_CONTENT` (issue #74; already-locked sessions heal after upgrading), and warned loudly when a stale sharp left over from a pre-v1.2 upgrade would break the pixel tools with `colourspace: parameter space not set` (issue #75).
34
36
  >
35
37
  > v1.2.1 hardened the pixel loop: all eleven pixel tools now accept uploaded-image attachment ids directly (no more `cannot read …/sha256:…` round trips), artifact filenames carry collision-free fingerprints, `vision_ground` retries degenerate boxes, the model guide replays fully from step 1 (leaving the settings first), and the settings card scrolls smoothly even with hundreds of models per provider.
36
38
 
package/README.zh.md CHANGED
@@ -28,7 +28,9 @@
28
28
  <p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
29
29
 
30
30
  > [!WARNING]
31
- > 📌 **公告(v1.2.2)**
31
+ > 📌 **公告(v1.2.3)**
32
+ >
33
+ > v1.2.3 现已支持:修复 DSH Desktop 每次启动都重复弹出首次引导的问题——引导「已读」标记与模型引导步骤改存 profile 设置文件(不再依赖按 origin 隔离、每次随机端口重启即清零的 `localStorage`)(issue #78)。
32
34
  >
33
35
  > v1.2.2 现已支持:补上最后一处附件 ID 缺口——宿主 `read_image` 回挂图片公布的 `sha256:…` ID 现在可被 `vision_describe` 与全部像素工具解析(issue #72);`vision_present` 等工具结果里的图像块不再把文本模型会话锁死在 `UNSUPPORTED_CONTENT`(issue #74,已锁死的历史会话升级后自动修复);检测到 v1.1.x 升级残留的旧版 sharp 时明确告警,把玄学的 `colourspace` 报错变成一眼可见的修复指引(issue #75)。
34
36
  >
package/index.js CHANGED
@@ -233,6 +233,15 @@ export const Config = z.object({
233
233
  tool: z.boolean().default(true),
234
234
  progressiveTools: z.boolean().default(true),
235
235
  autoActivateOnImage: z.boolean().default(true),
236
+ // Client-persisted UI state (issue #78): DSH Desktop serves the Web UI from
237
+ // a random port on every launch, so origin-scoped localStorage forgets the
238
+ // first-run onboarding dialog and it re-appeared on every boot. These keys
239
+ // live in the settings section (the profile settings file) instead; the
240
+ // client still mirrors best-effort copies into localStorage for downgrades.
241
+ // Both are internal to the client bundle — the settings card renders them
242
+ // nowhere, and the server half never reads them.
243
+ onboardingSeen: z.boolean().default(false),
244
+ visionGuideStep: z.string().default(''),
236
245
  artifactsDir: z.string().default('.dsh-vision-router/artifacts'),
237
246
  rewriteImages: z.boolean().default(true),
238
247
  downscale: z.boolean().default(true),
package/lib/client.js CHANGED
@@ -554,20 +554,116 @@ window.__ModuleLoader__.load({
554
554
  }
555
555
 
556
556
  const ONBOARDING_STORAGE_KEY = 'dsh-vision-router:onboarding:model-guide-v2'
557
+ // issue #78: DSH Desktop serves the Web UI from a random port on every
558
+ // launch (--port 0), so origin-scoped localStorage forgets everything
559
+ // between restarts and the first-run dialog re-appeared on every boot.
560
+ // The durable source of truth is now the `vision-router` settings section
561
+ // (the profile settings file, synced through ctx.settingsScope);
562
+ // localStorage survives only as a legacy/migration fallback for
563
+ // pre-v1.2.3 browsers and as a best-effort downgrade channel.
564
+ const ONBOARDING_SETTINGS_KEY = 'onboardingSeen'
557
565
  // The walkthrough has three steps: step1 (the session/text model selector
558
566
  // on the chat page), step2 (open Settings → Plugins → Vision Router), and
559
567
  // step3 (the highlighted vision chain, rendered by the settings card as a
560
568
  // callout). Only step1/step2 need persistence; step3 re-derives from the
561
569
  // card being on screen.
562
570
  const VISION_GUIDE_STORAGE_KEY = 'dsh-vision-router:guide:vision-backend-v2'
571
+ const VISION_GUIDE_SETTINGS_KEY = 'visionGuideStep'
563
572
  const VISION_GUIDE_EVENT = 'dsh-vision-router:vision-settings-guide'
564
573
  let visionGuidePrompt
565
574
  let visionGuideStepMemory
575
+ // Installed by apply(): a narrow, defensive view of the settings scope.
576
+ // { get, set, unset } never throw, and subscribe returns a disposer (or
577
+ // undefined when the scope is unavailable — the legacy paths take over).
578
+ let settingsPersistence
579
+ function installSettingsPersistence(scope) {
580
+ const readSection = () => {
581
+ try {
582
+ const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
583
+ return snapshot && snapshot.value ? snapshot.value : undefined
584
+ } catch {
585
+ return undefined
586
+ }
587
+ }
588
+ settingsPersistence = {
589
+ get(field) {
590
+ const section = readSection()
591
+ return section ? section[field] : undefined
592
+ },
593
+ set(field, value) {
594
+ try {
595
+ const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
596
+ if (!snapshot || !snapshot.writable || typeof scope.set !== 'function') return
597
+ void scope.set(field, value)
598
+ } catch {
599
+ // Read-only provider or unavailable scope: the localStorage
600
+ // fallback keeps this load functional.
601
+ }
602
+ },
603
+ unset(field) {
604
+ try {
605
+ const snapshot = scope && scope.getSnapshot && scope.getSnapshot()
606
+ if (!snapshot || !snapshot.writable || typeof scope.unset !== 'function') return
607
+ void scope.unset(field)
608
+ } catch {
609
+ // Same fallback reasoning as set().
610
+ }
611
+ },
612
+ subscribe(listener) {
613
+ try {
614
+ return scope && typeof scope.subscribe === 'function' ? scope.subscribe(listener) : undefined
615
+ } catch {
616
+ return undefined
617
+ }
618
+ },
619
+ }
620
+ }
621
+
622
+ function readOnboardingSeen() {
623
+ try {
624
+ if (settingsPersistence && settingsPersistence.get(ONBOARDING_SETTINGS_KEY) === true) return true
625
+ } catch {
626
+ // Fall through to the legacy origin-scoped marker.
627
+ }
628
+ try {
629
+ if (window.localStorage && window.localStorage.getItem(ONBOARDING_STORAGE_KEY) === 'seen') {
630
+ // Legacy value from before the settings-backed flag: migrate it so
631
+ // it survives desktop restarts, then honor it for this load.
632
+ if (settingsPersistence) settingsPersistence.set(ONBOARDING_SETTINGS_KEY, true)
633
+ return true
634
+ }
635
+ } catch {
636
+ // Best effort only.
637
+ }
638
+ return false
639
+ }
640
+ function rememberOnboardingSeen() {
641
+ try {
642
+ if (settingsPersistence) settingsPersistence.set(ONBOARDING_SETTINGS_KEY, true)
643
+ } catch {
644
+ // The dialog can still be dismissed for this page load.
645
+ }
646
+ try {
647
+ if (window.localStorage) window.localStorage.setItem(ONBOARDING_STORAGE_KEY, 'seen')
648
+ } catch {
649
+ // Best effort: the dialog can still be dismissed for this page load.
650
+ }
651
+ }
566
652
 
567
653
  function readVisionGuideStep() {
568
654
  try {
569
- const value = window.localStorage && window.localStorage.getItem(VISION_GUIDE_STORAGE_KEY)
655
+ const value = settingsPersistence && settingsPersistence.get(VISION_GUIDE_SETTINGS_KEY)
570
656
  if (value === 'step1' || value === 'step2') return value
657
+ } catch {
658
+ // Fall through to the legacy origin-scoped marker.
659
+ }
660
+ try {
661
+ const value = window.localStorage && window.localStorage.getItem(VISION_GUIDE_STORAGE_KEY)
662
+ if (value === 'step1' || value === 'step2') {
663
+ // Legacy value: migrate into settings for desktop durability.
664
+ if (settingsPersistence) settingsPersistence.set(VISION_GUIDE_SETTINGS_KEY, value)
665
+ return value
666
+ }
571
667
  } catch {
572
668
  // Fall through to page-memory state when storage access is blocked.
573
669
  }
@@ -575,6 +671,14 @@ window.__ModuleLoader__.load({
575
671
  }
576
672
  function writeVisionGuideStep(step) {
577
673
  visionGuideStepMemory = step === 'step1' || step === 'step2' ? step : undefined
674
+ try {
675
+ if (settingsPersistence) {
676
+ if (visionGuideStepMemory) settingsPersistence.set(VISION_GUIDE_SETTINGS_KEY, visionGuideStepMemory)
677
+ else settingsPersistence.unset(VISION_GUIDE_SETTINGS_KEY)
678
+ }
679
+ } catch {
680
+ // Page-memory state still keeps the guide functional for this load.
681
+ }
578
682
  try {
579
683
  if (!window.localStorage) return
580
684
  if (visionGuideStepMemory) window.localStorage.setItem(VISION_GUIDE_STORAGE_KEY, visionGuideStepMemory)
@@ -677,9 +781,13 @@ window.__ModuleLoader__.load({
677
781
  syncVisionGuidePrompt(t)
678
782
  window.addEventListener(VISION_GUIDE_EVENT, sync)
679
783
  window.addEventListener('popstate', sync)
784
+ // The durable step now lives in the settings section; re-sync when its
785
+ // snapshot changes (e.g. the first read resolves after page load).
786
+ const unsubscribe = settingsPersistence && settingsPersistence.subscribe(sync)
680
787
  return () => {
681
788
  window.removeEventListener(VISION_GUIDE_EVENT, sync)
682
789
  window.removeEventListener('popstate', sync)
790
+ if (typeof unsubscribe === 'function') unsubscribe()
683
791
  removeVisionGuidePrompt()
684
792
  }
685
793
  }
@@ -687,13 +795,6 @@ window.__ModuleLoader__.load({
687
795
  let onboardingOverlay
688
796
  let onboardingKeyDown
689
797
 
690
- function rememberOnboardingSeen() {
691
- try {
692
- if (window.localStorage) window.localStorage.setItem(ONBOARDING_STORAGE_KEY, 'seen')
693
- } catch {
694
- // Best effort: the dialog can still be dismissed for this page load.
695
- }
696
- }
697
798
  function dismissOnboarding(remember = true) {
698
799
  if (remember) rememberOnboardingSeen()
699
800
  if (typeof document !== 'undefined' && onboardingKeyDown) {
@@ -779,16 +880,33 @@ window.__ModuleLoader__.load({
779
880
  }
780
881
  function installOnboarding(t) {
781
882
  if (typeof document === 'undefined' || typeof window === 'undefined') return
782
- try {
783
- if (window.localStorage && window.localStorage.getItem(ONBOARDING_STORAGE_KEY) === 'seen') return
784
- } catch {
785
- // Privacy/storage restrictions should not prevent the guidance itself.
883
+ let timer
884
+ const clearTimer = () => {
885
+ if (timer !== undefined) window.clearTimeout(timer)
886
+ timer = undefined
786
887
  }
787
-
788
- const timer = window.setTimeout(() => showOnboarding(t), 650)
789
-
888
+ // Reactive against the settings scope: the settings snapshot arrives
889
+ // asynchronously on a cold page load, so a one-shot localStorage check
890
+ // could show the dialog before the durable "seen" flag lands. Re-sync
891
+ // on every snapshot change and dismiss the overlay if the flag appears.
892
+ const sync = () => {
893
+ if (readOnboardingSeen()) {
894
+ clearTimer()
895
+ if (onboardingOverlay) dismissOnboarding(false)
896
+ return
897
+ }
898
+ if (timer === undefined && !onboardingOverlay) {
899
+ timer = window.setTimeout(() => {
900
+ timer = undefined
901
+ if (!readOnboardingSeen()) showOnboarding(t)
902
+ }, 650)
903
+ }
904
+ }
905
+ sync()
906
+ const unsubscribe = settingsPersistence && settingsPersistence.subscribe(sync)
790
907
  return () => {
791
- if (timer !== undefined) window.clearTimeout(timer)
908
+ clearTimer()
909
+ if (typeof unsubscribe === 'function') unsubscribe()
792
910
  // Unmounting is not a user choice: drop the dialog without marking
793
911
  // the guide seen, so the first-run overview can still auto-open.
794
912
  if (onboardingOverlay) dismissOnboarding(false)
@@ -1894,6 +2012,10 @@ window.__ModuleLoader__.load({
1894
2012
 
1895
2013
  function apply(ctx) {
1896
2014
  const scope = ctx.settingsScope.bind({ namespace: 'vision-router' })
2015
+ // issue #78: route the onboarding/guide durability through the settings
2016
+ // section (profile settings file) so it survives DSH Desktop's random
2017
+ // per-launch port. Installed before the effects below run.
2018
+ installSettingsPersistence(scope)
1897
2019
  const presentedImageUrls = new Map()
1898
2020
  const createdImageUrls = new Set()
1899
2021
  const loadPresentedImage = (sessionId, attachment) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vision-router",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
5
5
  "license": "MIT",
6
6
  "repository": {