@dickpy/dsh-imagegen 1.2.2 → 1.3.0

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.
@@ -534,3 +534,490 @@
534
534
  transition: none;
535
535
  }
536
536
  }
537
+
538
+ /* --- channel list ------------------------------------------------------------------ */
539
+
540
+ .channelSection {
541
+ padding: 6px 0 2px;
542
+ }
543
+
544
+ .channelEmpty {
545
+ margin: 10px 0 0;
546
+ font-size: 12px;
547
+ line-height: 1.5;
548
+ color: var(--dsw-alias-label-tertiary);
549
+ }
550
+
551
+ .channelList {
552
+ list-style: none;
553
+ margin: 10px 0 0;
554
+ padding: 0;
555
+ display: flex;
556
+ flex-direction: column;
557
+ gap: 6px;
558
+ }
559
+
560
+ .channelRow {
561
+ display: flex;
562
+ align-items: center;
563
+ gap: 10px;
564
+ border: 1px solid var(--dsw-alias-border-l2);
565
+ border-radius: 10px;
566
+ padding: 8px 10px;
567
+ background: var(--dsw-alias-bg-layer-3);
568
+ }
569
+
570
+ .channelRow[data-action] {
571
+ flex-wrap: wrap;
572
+ }
573
+
574
+ .channelDotReady,
575
+ .channelDotWarn {
576
+ flex: none;
577
+ width: 9px;
578
+ height: 9px;
579
+ border-radius: 50%;
580
+ }
581
+
582
+ .channelDotReady {
583
+ background: var(--dsw-color-success, #2fbf71);
584
+ }
585
+
586
+ .channelDotWarn {
587
+ background: var(--dsw-color-danger, #e5484d);
588
+ }
589
+
590
+ .channelMain {
591
+ appearance: none;
592
+ flex: 1;
593
+ min-width: 0;
594
+ display: flex;
595
+ flex-direction: column;
596
+ gap: 3px;
597
+ padding: 0;
598
+ border: 0;
599
+ background: none;
600
+ font: inherit;
601
+ text-align: left;
602
+ cursor: pointer;
603
+ }
604
+
605
+ .channelName {
606
+ font-size: 13px;
607
+ font-weight: 600;
608
+ line-height: 1.4;
609
+ color: var(--dsw-alias-label-primary);
610
+ overflow: hidden;
611
+ text-overflow: ellipsis;
612
+ white-space: nowrap;
613
+ }
614
+
615
+ .channelMeta {
616
+ display: flex;
617
+ align-items: center;
618
+ flex-wrap: wrap;
619
+ gap: 4px 8px;
620
+ }
621
+
622
+ .channelHost {
623
+ font-size: 11px;
624
+ line-height: 1.5;
625
+ color: var(--dsw-alias-label-tertiary);
626
+ font-family: var(--dsw-font-family-mono, monospace);
627
+ overflow: hidden;
628
+ text-overflow: ellipsis;
629
+ white-space: nowrap;
630
+ max-width: 180px;
631
+ }
632
+
633
+ .channelBadge {
634
+ border-radius: 999px;
635
+ padding: 0 7px;
636
+ font-size: 11px;
637
+ line-height: 17px;
638
+ white-space: nowrap;
639
+ font-weight: 500;
640
+ background: var(--dsw-alias-bg-module-platform);
641
+ color: var(--dsw-alias-label-secondary);
642
+ }
643
+
644
+ .channelBadge[data-warn] {
645
+ color: var(--dsw-alias-label-error);
646
+ }
647
+
648
+ .channelBadge[data-default] {
649
+ background: var(--dsw-alias-bg-module-poped, var(--dsw-alias-bg-module-platform));
650
+ color: var(--dsw-alias-brand-primary);
651
+ }
652
+
653
+ .channelAction {
654
+ appearance: none;
655
+ flex: none;
656
+ border: 0;
657
+ background: none;
658
+ padding: 4px 8px;
659
+ border-radius: 6px;
660
+ font: inherit;
661
+ font-size: 12px;
662
+ line-height: 1.5;
663
+ color: var(--dsw-alias-label-secondary);
664
+ cursor: pointer;
665
+ }
666
+
667
+ .channelAction:hover:not(:disabled) {
668
+ color: var(--dsw-alias-label-primary);
669
+ background: var(--dsw-alias-bg-module-platform);
670
+ }
671
+
672
+ .channelAction[data-danger] {
673
+ color: var(--dsw-alias-label-error);
674
+ }
675
+
676
+ .channelAction:disabled {
677
+ opacity: 0.45;
678
+ cursor: default;
679
+ }
680
+
681
+ .channelDanger {
682
+ appearance: none;
683
+ flex: none;
684
+ border: 1px solid var(--dsw-alias-label-error);
685
+ border-radius: 6px;
686
+ padding: 3px 10px;
687
+ background: none;
688
+ color: var(--dsw-alias-label-error);
689
+ font: inherit;
690
+ font-size: 12px;
691
+ cursor: pointer;
692
+ }
693
+
694
+ .channelDanger:hover:not(:disabled) {
695
+ background: color-mix(in srgb, var(--dsw-alias-label-error) 12%, transparent);
696
+ }
697
+
698
+ .channelDanger:disabled {
699
+ opacity: 0.45;
700
+ cursor: default;
701
+ }
702
+
703
+ .deleteConfirmText {
704
+ flex: 1;
705
+ min-width: 0;
706
+ font-size: 12px;
707
+ line-height: 1.5;
708
+ color: var(--dsw-alias-label-error);
709
+ }
710
+
711
+ .channelAddRow {
712
+ display: flex;
713
+ gap: 8px;
714
+ margin-top: 10px;
715
+ }
716
+
717
+ .channelAdd {
718
+ appearance: none;
719
+ min-height: 30px;
720
+ border: 1px dashed var(--dsw-alias-border-l2);
721
+ border-radius: 8px;
722
+ padding: 0 12px;
723
+ background: transparent;
724
+ color: var(--dsw-alias-label-secondary);
725
+ font: inherit;
726
+ font-size: 12px;
727
+ cursor: pointer;
728
+ }
729
+
730
+ .channelAdd:hover:not(:disabled) {
731
+ color: var(--dsw-alias-brand-primary);
732
+ border-color: var(--dsw-alias-brand-primary);
733
+ }
734
+
735
+ .channelAdd:disabled {
736
+ opacity: 0.45;
737
+ cursor: default;
738
+ }
739
+
740
+ .spacer {
741
+ flex: 1;
742
+ }
743
+
744
+ /* --- editor dialog ------------------------------------------------------------------- */
745
+
746
+ .editorBackdrop {
747
+ position: fixed;
748
+ inset: 0;
749
+ z-index: 120;
750
+ display: flex;
751
+ align-items: flex-start;
752
+ justify-content: center;
753
+ padding: 9vh 16px 16px;
754
+ background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 45%, transparent);
755
+ }
756
+
757
+ .editorPanel {
758
+ width: min(560px, 100%);
759
+ max-height: 82vh;
760
+ overflow-y: auto;
761
+ border: 1px solid var(--dsw-alias-border-l2);
762
+ border-radius: 14px;
763
+ background: var(--dsw-alias-bg-layer-2);
764
+ box-shadow: 0 18px 50px rgb(0 0 0 / 0.35);
765
+ padding: 16px;
766
+ }
767
+
768
+ .editorHeader {
769
+ display: flex;
770
+ align-items: flex-start;
771
+ justify-content: space-between;
772
+ gap: 12px;
773
+ padding-bottom: 10px;
774
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
775
+ }
776
+
777
+ .editorClose {
778
+ appearance: none;
779
+ flex: none;
780
+ width: 26px;
781
+ height: 26px;
782
+ border: 0;
783
+ border-radius: 6px;
784
+ background: none;
785
+ color: var(--dsw-alias-label-tertiary);
786
+ font: inherit;
787
+ font-size: 16px;
788
+ line-height: 26px;
789
+ cursor: pointer;
790
+ }
791
+
792
+ .editorClose:hover {
793
+ color: var(--dsw-alias-label-primary);
794
+ background: var(--dsw-alias-bg-module-platform);
795
+ }
796
+
797
+ .editorField {
798
+ display: flex;
799
+ flex-direction: column;
800
+ gap: 6px;
801
+ padding: 12px 0 0;
802
+ }
803
+
804
+ .editorDivider {
805
+ height: 1px;
806
+ margin: 14px 0 4px;
807
+ background: var(--dsw-alias-border-l2);
808
+ }
809
+
810
+ .editorSectionHeader {
811
+ display: flex;
812
+ align-items: center;
813
+ justify-content: space-between;
814
+ gap: 12px;
815
+ padding: 10px 0 4px;
816
+ }
817
+
818
+ .editorSectionHeader .label {
819
+ flex: 1;
820
+ }
821
+
822
+ .editorTools {
823
+ display: flex;
824
+ flex-direction: column;
825
+ gap: 8px;
826
+ margin-top: 10px;
827
+ }
828
+
829
+ .editorFooter {
830
+ display: flex;
831
+ align-items: center;
832
+ gap: 8px;
833
+ padding-top: 12px;
834
+ border-top: 1px solid var(--dsw-alias-border-l2);
835
+ }
836
+
837
+ .detectOk {
838
+ margin: 0;
839
+ font-size: 12px;
840
+ line-height: 1.5;
841
+ color: var(--dsw-color-success, var(--dsw-alias-label-secondary));
842
+ }
843
+
844
+ /* --- model catalog rows ---------------------------------------------------------------- */
845
+
846
+ .modelRows {
847
+ list-style: none;
848
+ margin: 8px 0 0;
849
+ padding: 0;
850
+ display: flex;
851
+ flex-direction: column;
852
+ gap: 8px;
853
+ }
854
+
855
+ .modelRow {
856
+ display: flex;
857
+ align-items: center;
858
+ justify-content: space-between;
859
+ gap: 10px;
860
+ border: 1px solid var(--dsw-alias-border-l2);
861
+ border-radius: 10px;
862
+ padding: 8px 10px;
863
+ background: var(--dsw-alias-bg-layer-3);
864
+ }
865
+
866
+ .modelRowInputs {
867
+ flex: 1;
868
+ min-width: 0;
869
+ display: flex;
870
+ align-items: center;
871
+ gap: 6px;
872
+ }
873
+
874
+ .modelRowInputs .input {
875
+ flex: 1;
876
+ min-width: 0;
877
+ }
878
+
879
+ .modelArrow {
880
+ flex: none;
881
+ color: var(--dsw-alias-label-tertiary);
882
+ font-size: 12px;
883
+ }
884
+
885
+ .modelRowBadges {
886
+ flex: none;
887
+ display: flex;
888
+ align-items: center;
889
+ gap: 6px;
890
+ }
891
+
892
+ .modelBadge {
893
+ border-radius: 999px;
894
+ padding: 0 7px;
895
+ font-size: 11px;
896
+ line-height: 17px;
897
+ white-space: nowrap;
898
+ background: var(--dsw-alias-bg-module-platform);
899
+ color: var(--dsw-alias-label-tertiary);
900
+ }
901
+
902
+ .modelBadge[data-verified] {
903
+ color: var(--dsw-color-success, var(--dsw-alias-label-secondary));
904
+ }
905
+
906
+ .modelBadge[data-warn] {
907
+ color: var(--dsw-alias-label-error);
908
+ }
909
+
910
+ .modelRowRemove {
911
+ appearance: none;
912
+ flex: none;
913
+ width: 20px;
914
+ height: 20px;
915
+ border: 0;
916
+ border-radius: 4px;
917
+ padding: 0;
918
+ background: transparent;
919
+ color: var(--dsw-alias-label-tertiary);
920
+ font: inherit;
921
+ line-height: 20px;
922
+ cursor: pointer;
923
+ }
924
+
925
+ .modelRowRemove:hover:not(:disabled) {
926
+ color: var(--dsw-alias-label-error);
927
+ background: var(--dsw-alias-bg-module-platform);
928
+ }
929
+
930
+ .modelRowRemove:disabled {
931
+ opacity: 0.45;
932
+ cursor: default;
933
+ }
934
+
935
+ .modelCandidate > .modelBadge {
936
+ flex: none;
937
+ }
938
+
939
+ /* --- preset picker ---------------------------------------------------------------------- */
940
+
941
+ .channelControls {
942
+ position: relative;
943
+ }
944
+
945
+ .presetInline {
946
+ position: absolute;
947
+ left: 0;
948
+ bottom: calc(100% + 8px);
949
+ z-index: 10;
950
+ width: min(420px, calc(100vw - 48px));
951
+ max-height: min(440px, 60vh);
952
+ overflow-y: auto;
953
+ margin: 0;
954
+ padding: 12px;
955
+ border: 1px solid var(--dsw-alias-border-l2);
956
+ border-radius: 10px;
957
+ background: var(--dsw-alias-bg-layer-2);
958
+ box-shadow: 0 12px 30px rgb(0 0 0 / 0.3);
959
+ }
960
+
961
+ .presetInlineHeader {
962
+ display: flex;
963
+ align-items: flex-start;
964
+ justify-content: space-between;
965
+ gap: 12px;
966
+ }
967
+
968
+ .presetList {
969
+ display: flex;
970
+ flex-direction: column;
971
+ gap: 8px;
972
+ margin-top: 12px;
973
+ }
974
+
975
+ .presetRow {
976
+ appearance: none;
977
+ display: flex;
978
+ flex-direction: column;
979
+ align-items: flex-start;
980
+ gap: 3px;
981
+ width: 100%;
982
+ border: 1px solid var(--dsw-alias-border-l2);
983
+ border-radius: 10px;
984
+ padding: 10px 12px;
985
+ background: var(--dsw-alias-bg-layer-3);
986
+ color: inherit;
987
+ font: inherit;
988
+ text-align: left;
989
+ cursor: pointer;
990
+ }
991
+
992
+ .presetRow:hover:not(:disabled) {
993
+ border-color: var(--dsw-alias-brand-primary);
994
+ }
995
+
996
+ .presetRow[data-custom] {
997
+ border-style: dashed;
998
+ }
999
+
1000
+ .presetRow:disabled {
1001
+ opacity: 0.5;
1002
+ cursor: default;
1003
+ }
1004
+
1005
+ .presetName {
1006
+ font-size: 13px;
1007
+ font-weight: 600;
1008
+ line-height: 1.4;
1009
+ color: var(--dsw-alias-label-primary);
1010
+ }
1011
+
1012
+ .presetMeta {
1013
+ font-size: 11px;
1014
+ line-height: 1.5;
1015
+ color: var(--dsw-alias-label-tertiary);
1016
+ font-family: var(--dsw-font-family-mono, monospace);
1017
+ }
1018
+
1019
+ .presetHint {
1020
+ font-size: 12px;
1021
+ line-height: 1.5;
1022
+ color: var(--dsw-alias-label-secondary);
1023
+ }
@@ -14,21 +14,32 @@ import {
14
14
  type SettingsScopeSnapshot,
15
15
  type SnapshotStore,
16
16
  } from '@deepseek-ai/dsh-client-runtime/client'
17
- import { SETTINGS_API } from '../protocol.ts'
17
+ import { SETTINGS_API, type ChannelConfig } from '../protocol.ts'
18
18
 
19
19
  /** The fields this plugin's settings card edits. */
20
20
  export interface ImageGenConfig {
21
21
  enabled?: boolean
22
22
  announceToAgent?: boolean
23
23
  allowAgentImageGeneration?: boolean
24
- apiUrl?: string
25
- apiKey?: string
26
- imageModels?: string[]
24
+ /** Configured channels (each: name, endpoint, model catalog). */
25
+ channels?: ChannelConfig[]
26
+ /** Per-channel API keys, keyed by channel id. The redacted wire view returns
27
+ * this as an empty object; key presence comes from the secrets sidecar. */
28
+ channelSecrets?: Record<string, string>
29
+ /** Channel used when a request does not name one. */
30
+ defaultChannelId?: string
27
31
  promptApiUrl?: string
28
32
  promptApiKey?: string
29
33
  promptModel?: string
34
+ /* ----- deprecated legacy single-endpoint fields (migrated to channels) ----- */
35
+ apiUrl?: string
36
+ apiKey?: string
37
+ imageModels?: string[]
30
38
  }
31
39
 
40
+ /** One settings path-op as the bridge consumes it. */
41
+ export type SettingsOp = { op: 'set'; path: string[]; value: unknown } | { op: 'unset'; path: string[] }
42
+
32
43
  /** Wire shape of one namespace view from the bridge. */
33
44
  interface BridgeView {
34
45
  ns: string
@@ -140,11 +151,18 @@ class BridgeScopeController<T> implements SettingsScope<T> {
140
151
  }
141
152
 
142
153
  set(field: string, value: unknown): Promise<void> {
143
- return this.enqueue(() => this.write({ op: 'set', path: [field], value }))
154
+ return this.enqueue(() => this.writeOps([{ op: 'set', path: [field], value }]))
144
155
  }
145
156
 
146
157
  unset(field: string): Promise<void> {
147
- return this.enqueue(() => this.write({ op: 'unset', path: [field] }))
158
+ return this.enqueue(() => this.writeOps([{ op: 'unset', path: [field] }]))
159
+ }
160
+
161
+ /** Apply several path ops in one revision-fenced mutate call (atomic save).
162
+ * Path ops may address plain-object fields (e.g. `channelSecrets.<id>`),
163
+ * but never navigate *inside* arrays — write array fields wholesale. */
164
+ mutateOps(ops: SettingsOp[]): Promise<void> {
165
+ return this.enqueue(() => this.writeOps(ops))
148
166
  }
149
167
 
150
168
  async dispose(): Promise<void> {
@@ -188,13 +206,13 @@ class BridgeScopeController<T> implements SettingsScope<T> {
188
206
  this.accept(view, writable)
189
207
  }
190
208
 
191
- private async write(op: { op: 'set' | 'unset'; path: string[]; value?: unknown }): Promise<void> {
209
+ private async writeOps(ops: SettingsOp[]): Promise<void> {
192
210
  const revision = this.getSnapshot().revision
193
211
  let response
194
212
  try {
195
213
  response = await this.api.mutate({
196
214
  ns: this.spec.namespace,
197
- ops: [op],
215
+ ops,
198
216
  ...revision === undefined ? {} : { expectedRevision: revision },
199
217
  })
200
218
  } catch {
@@ -229,6 +247,8 @@ class BridgeScopeController<T> implements SettingsScope<T> {
229
247
  export interface ImageGenScope extends SettingsScope<ImageGenConfig> {
230
248
  /** Queue a bridge refresh (the invalidation path re-reads the namespace). */
231
249
  load(): Promise<void>
250
+ /** Apply several path ops in one revision-fenced mutate call. */
251
+ mutateOps(ops: SettingsOp[]): Promise<void>
232
252
  getKeySetSnapshot(): boolean
233
253
  subscribeKeySet(listener: () => void): () => void
234
254
  getSecretSetSnapshot(field: string): boolean
@@ -248,3 +268,31 @@ export function bindImageGenScope(fetchFn: typeof fetch = fetch): ImageGenScope
248
268
  void controller.load()
249
269
  return controller
250
270
  }
271
+
272
+ /**
273
+ * Flatten the configured channels into the model options the panel lists
274
+ * (aliases; the default channel's models first) plus the default channel id.
275
+ * Falls back to the legacy flat allow-list while no channels exist (upgrade
276
+ * path). Pure projection — no host calls.
277
+ */
278
+ export function imageModelOptions(config: ImageGenConfig | undefined): { models: string[]; defaultChannelId?: string } {
279
+ const channels = config?.channels ?? []
280
+ if (channels.length === 0) {
281
+ const legacy = Array.isArray(config?.imageModels)
282
+ ? config.imageModels.filter((model): model is string => typeof model === 'string' && model.trim() !== '')
283
+ : []
284
+ return { models: legacy }
285
+ }
286
+ const defaultId = config?.defaultChannelId !== undefined && channels.some(channel => channel.id === config.defaultChannelId)
287
+ ? config.defaultChannelId
288
+ : channels[0]!.id
289
+ const ordered = [defaultId, ...channels.filter(channel => channel.id !== defaultId).map(channel => channel.id)]
290
+ const models: string[] = []
291
+ for (const id of ordered) {
292
+ const channel = channels.find(candidate => candidate.id === id)!
293
+ for (const model of channel.models) {
294
+ if (model.alias !== '' && !models.includes(model.alias)) models.push(model.alias)
295
+ }
296
+ }
297
+ return models.length > 0 ? { models, defaultChannelId: defaultId } : { models: [], defaultChannelId: defaultId }
298
+ }