@dickpy/dsh-imagegen 1.2.3 → 1.4.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.
Files changed (45) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +203 -181
  3. package/cordis.patch.yml +8 -8
  4. package/docs/images/multi-model-comparison.png +0 -0
  5. package/lib/client.js +2711 -1318
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +830 -155
  8. package/package.json +70 -68
  9. package/src/agent-image-tools.ts +418 -316
  10. package/src/client/ImageGenPanel.tsx +1703 -1476
  11. package/src/client/SettingsCard.tsx +936 -648
  12. package/src/client/TemplateLibrary.tsx +336 -336
  13. package/src/client/api.ts +193 -193
  14. package/src/client/channels-form.ts +263 -0
  15. package/src/client/controller.ts +46 -46
  16. package/src/client/conversation-sync.ts +14 -0
  17. package/src/client/css-modules.d.ts +5 -5
  18. package/src/client/helpers.ts +33 -33
  19. package/src/client/image-toolview.module.css +73 -73
  20. package/src/client/image-toolview.tsx +170 -152
  21. package/src/client/index.ts +32 -22
  22. package/src/client/locales.ts +610 -484
  23. package/src/client/mount.tsx +185 -96
  24. package/src/client/panel.module.css +1713 -1445
  25. package/src/client/settings-card.module.css +1023 -536
  26. package/src/client/settings-form.ts +336 -336
  27. package/src/client/settings-scope.ts +298 -250
  28. package/src/client/sidebar-entry.ts +148 -102
  29. package/src/client/templates.module.css +453 -453
  30. package/src/engine.ts +520 -464
  31. package/src/gallery-store.ts +286 -280
  32. package/src/generation-runtime.ts +79 -48
  33. package/src/history-store.ts +250 -238
  34. package/src/image-format.ts +11 -0
  35. package/src/image-models.ts +19 -19
  36. package/src/index.ts +318 -212
  37. package/src/model-catalog.ts +115 -0
  38. package/src/presets.ts +71 -0
  39. package/src/prompt-enhancer.ts +137 -79
  40. package/src/protocol.ts +338 -253
  41. package/src/routes.ts +916 -738
  42. package/src/task-queue.ts +113 -103
  43. package/src/templates/cases.json +10196 -10196
  44. package/src/templates-store.ts +278 -278
  45. package/src/updater.ts +117 -117
@@ -1,31 +1,26 @@
1
1
  /**
2
- * Sidebar entry injection.
3
- *
4
- * dsh's sidebar shell exposes no slot an external plugin can register into,
5
- * so — following the dsh-ssh / task-board precedent of DOM-level extension
6
- * the entry row is injected after the shell's New Session button (after the
7
- * sibling plugin family block). The injection self-heals: a MutationObserver
8
- * watches the sidebar root and re-inserts the row whenever a React re-render
9
- * displaces it (re-insertion happens in the same frame, before paint).
10
- *
11
- * The row is plain DOM (no React tree) so it can never disturb the shell's
12
- * reconciliation; the panel view it toggles is a separate React root mounted
13
- * in the center column (see mount.tsx).
2
+ * Replace the shell's standalone New Session affordance with a two-tab entry:
3
+ * New Session and Image Generation. When image generation is active, the
4
+ * plugin also uses the shell's region area as a dedicated history surface;
5
+ * the original workspace/session tree remains underneath and is restored when
6
+ * the panel closes.
14
7
  */
15
8
 
16
9
  import type { ImageGenController } from './controller.ts'
17
10
  import css from './panel.module.css'
18
11
 
19
- /** Stable data attribute identifying the injected entry row. */
20
- export const ENTRY_SELECTOR = '[data-dsh-imagegen-entry]'
12
+ /** Stable selector for the injected two-tab host. */
13
+ export const ENTRY_SELECTOR = '[data-dsh-imagegen-session-tabs]'
14
+ /** Stable selector for the history surface in the shell region area. */
15
+ export const HISTORY_HOST_SELECTOR = '[data-dsh-imagegen-history-host]'
21
16
 
22
- /** Inline icon (matches the shell's 16px nav-icon look): a picture glyph. */
23
- const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><circle cx="5.6" cy="5.8" r="1"/><path d="M2.5 12.5l3.6-3.4 2.4 2.2 3-3 2 2.4"/></svg>'
17
+ /** Inline picture glyph kept deliberately small for the sidebar rail. */
18
+ const IMAGE_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><circle cx="5.6" cy="5.8" r="1"/><path d="M2.5 12.5l3.6-3.4 2.4 2.2 3-3 2 2.4"/></svg>'
24
19
 
25
- /** Family entry selectors of sibling plugins (relative placement anchor). */
26
- const FAMILY_ENTRY_SELECTOR = '[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-imagegen-entry]'
20
+ /** Inline plus glyph for the new-session tab. */
21
+ const NEW_SESSION_ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg>'
27
22
 
28
- /** Find the sidebar shell root element, or undefined while not yet mounted. */
23
+ /** Find the sidebar shell root, or undefined while it is not mounted. */
29
24
  function sidebarRoot(): HTMLElement | undefined {
30
25
  const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
31
26
  if (column === null) return undefined
@@ -33,112 +28,163 @@ function sidebarRoot(): HTMLElement | undefined {
33
28
  return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
34
29
  }
35
30
 
36
- /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
31
+ /** The shell-owned New Session button across current and legacy shells. */
37
32
  function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
38
- const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
39
- if (nested !== null) return nested
40
- for (const child of root.children) {
41
- if (child.tagName === 'BUTTON') return child as HTMLButtonElement
42
- }
43
- return undefined
33
+ return root.querySelector<HTMLButtonElement>(
34
+ 'button[data-dsh-part="new-session"], button[class*="newSession"]',
35
+ ) ?? Array.from(root.children).find(
36
+ (child): child is HTMLButtonElement => child instanceof HTMLElement && child.tagName === 'BUTTON',
37
+ )
38
+ }
39
+
40
+ /** Locate the shell region that normally contains workspaces and sessions. */
41
+ function regionArea(root: HTMLElement): HTMLElement | undefined {
42
+ return root.querySelector<HTMLElement>('[class*="regionArea"]') ?? undefined
43
+ }
44
+
45
+ function makeTab(
46
+ label: string,
47
+ tooltip: string,
48
+ icon: string,
49
+ onClick: () => void,
50
+ ): HTMLButtonElement {
51
+ const tab = document.createElement('button')
52
+ tab.type = 'button'
53
+ tab.className = css.sessionTab
54
+ tab.setAttribute('aria-label', label)
55
+ tab.setAttribute('title', tooltip)
56
+ tab.innerHTML = `<span class="${css.sessionTabIcon}">${icon}</span><span class="${css.sessionTabLabel}">${label}</span>`
57
+ tab.addEventListener('click', onClick)
58
+ return tab
59
+ }
60
+
61
+ function hideShellButton(button: HTMLButtonElement): void {
62
+ button.dataset.dshImagegenOriginal = ''
63
+ button.setAttribute('aria-hidden', 'true')
64
+ button.tabIndex = -1
65
+ button.style.display = 'none'
44
66
  }
45
67
 
46
- /** Build the entry row (a detached button; insert once the shell is up). */
47
- function createEntry(controller: ImageGenController, label: string, tooltip: string): HTMLButtonElement {
48
- const entry = document.createElement('button')
49
- entry.type = 'button'
50
- entry.dataset.dshImagegenEntry = ''
51
- entry.className = css.entry
52
- entry.setAttribute('aria-label', label)
53
- entry.setAttribute('title', tooltip)
54
- entry.innerHTML = '<span class="' + css.entryIcon + '">' + ICON + '</span><span class="' + css.entryLabel + '">' + label + '</span>'
55
- entry.addEventListener('click', () => { controller.toggle() })
56
- return entry
68
+ function restoreShellButton(button: HTMLButtonElement): void {
69
+ button.style.removeProperty('display')
70
+ button.removeAttribute('aria-hidden')
71
+ button.removeAttribute('tabindex')
72
+ delete button.dataset.dshImagegenOriginal
57
73
  }
58
74
 
59
- /** Re-insert the entry after the family block (task board ssh imagegen). */
60
- function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
75
+ /** Mount or repair the two-tab host at the shell's New Session position. */
76
+ function placeTabs(
77
+ root: HTMLElement,
78
+ controller: ImageGenController,
79
+ newSessionLabel: string,
80
+ newSessionTooltip: string,
81
+ imageLabel: string,
82
+ imageTooltip: string,
83
+ ): HTMLDivElement | undefined {
61
84
  const button = newSessionButton(root)
62
- if (button === undefined) return false
63
- if (entry.parentElement !== root) {
64
- // Position relative to the family block, never relative to transient
65
- // logoRow geometry: every family plugin that self-heals during a
66
- // re-render then lands in the same relative order.
67
- const row = button.closest('[class*="logoRow"]')
68
- const base = (row !== null && row.parentElement === root) ? row : button
69
- const family = Array.from(root.children).filter(
70
- (el): el is HTMLElement => el instanceof HTMLElement && el.matches(FAMILY_ENTRY_SELECTOR),
71
- )
72
- const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling
73
- root.insertBefore(entry, anchor)
85
+ if (button === undefined) return undefined
86
+
87
+ const existing = root.querySelector<HTMLDivElement>(ENTRY_SELECTOR)
88
+ if (existing !== null && existing.parentElement === button.parentElement) {
89
+ hideShellButton(button)
90
+ return existing
74
91
  }
75
- return true
92
+
93
+ existing?.remove()
94
+ const tabs = document.createElement('div')
95
+ tabs.dataset.dshImagegenSessionTabs = ''
96
+ tabs.className = css.sessionTabs
97
+ tabs.setAttribute('role', 'tablist')
98
+ tabs.setAttribute('aria-label', imageTooltip)
99
+
100
+ const newSessionTab = makeTab(newSessionLabel, newSessionTooltip, NEW_SESSION_ICON, () => {
101
+ controller.close()
102
+ button.click()
103
+ })
104
+ const imageTab = makeTab(imageLabel, imageTooltip, IMAGE_ICON, () => {
105
+ controller.open()
106
+ })
107
+ newSessionTab.dataset.dshImagegenTab = 'new-session'
108
+ imageTab.dataset.dshImagegenTab = 'image'
109
+ tabs.append(newSessionTab, imageTab)
110
+
111
+ button.parentElement?.insertBefore(tabs, button)
112
+ hideShellButton(button)
113
+ return tabs
114
+ }
115
+
116
+ /** Mount an overlay host over the workspace/session tree for image history. */
117
+ function placeHistoryHost(root: HTMLElement): HTMLDivElement | undefined {
118
+ const region = regionArea(root)
119
+ if (region === undefined) return undefined
120
+ const existing = region.querySelector<HTMLDivElement>(HISTORY_HOST_SELECTOR)
121
+ if (existing !== null) return existing
122
+ const host = document.createElement('div')
123
+ host.dataset.dshImagegenHistoryHost = ''
124
+ host.className = css.sidebarHistoryHost
125
+ region.append(host)
126
+ return host
76
127
  }
77
128
 
78
129
  /**
79
- * Mount the sidebar entry, waiting for the shell to render and self-healing
80
- * on later React re-renders.
81
- * @param controller - the panel controller the entry toggles.
82
- * @param label - the entry label (localized).
83
- * @param tooltip - the entry tooltip (localized).
84
- * @returns disposer removing the entry and its observers.
130
+ * Mount the two tabs and self-heal after React rebuilds the sidebar. The
131
+ * shell-owned button is restored by the disposer so unloading the plugin
132
+ * leaves the host unchanged.
85
133
  */
86
- export function mountSidebarEntry(controller: ImageGenController, label: string, tooltip: string): () => void {
87
- const entry = createEntry(controller, label, tooltip)
134
+ export function mountSidebarEntry(
135
+ controller: ImageGenController,
136
+ newSessionLabel: string,
137
+ newSessionTooltip: string,
138
+ imageLabel: string,
139
+ imageTooltip: string,
140
+ ): () => void {
88
141
  let root: HTMLElement | undefined
89
- let placed = false
142
+ let tabs: HTMLDivElement | undefined
143
+ let historyHost: HTMLDivElement | undefined
144
+ let originalButton: HTMLButtonElement | undefined
90
145
 
91
- const tryPlace = (): void => {
92
- if (root !== undefined && !root.isConnected) {
93
- rootObserver.disconnect()
94
- root = undefined
95
- placed = false
146
+ const syncActive = (): void => {
147
+ if (tabs === undefined) return
148
+ const newTab = tabs.querySelector<HTMLElement>('[data-dsh-imagegen-tab="new-session"]')
149
+ const imageTab = tabs.querySelector<HTMLElement>('[data-dsh-imagegen-tab="image"]')
150
+ if (controller.getSnapshot().panelOpen) {
151
+ if (newTab !== null) delete newTab.dataset.active
152
+ if (imageTab !== null) imageTab.dataset.active = ''
153
+ } else {
154
+ if (newTab !== null) newTab.dataset.active = ''
155
+ if (imageTab !== null) delete imageTab.dataset.active
96
156
  }
97
- if (placed) {
98
- if (document.body.contains(entry)) return
99
- rootObserver.disconnect()
157
+ }
158
+
159
+ const ensure = (): void => {
160
+ if (root !== undefined && !root.isConnected) {
100
161
  root = undefined
101
- placed = false
162
+ tabs = undefined
163
+ historyHost = undefined
164
+ originalButton = undefined
102
165
  }
103
166
  root ??= sidebarRoot()
104
167
  if (root === undefined) return
105
- placed = placeEntry(root, entry)
106
- if (placed) {
107
- rootObserver.observe(root, { childList: true, subtree: true })
108
- }
168
+ root.dataset.dshImagegenSidebarRoot = ''
169
+ const button = newSessionButton(root)
170
+ if (button === undefined) return
171
+ originalButton ??= button
172
+ tabs = placeTabs(root, controller, newSessionLabel, newSessionTooltip, imageLabel, imageTooltip)
173
+ historyHost = placeHistoryHost(root)
174
+ syncActive()
109
175
  }
110
176
 
111
- // Body-level watcher retained as the "whole rebuild" fallback.
112
- const waitObserver = new MutationObserver(() => { tryPlace() })
113
- waitObserver.observe(document.body, { childList: true, subtree: true })
114
-
115
- // Self-heal: if a React re-render displaces the row, re-insert it in the
116
- // same frame (microtask before paint -> no visible flicker).
117
- const rootObserver = new MutationObserver(() => {
118
- if (root === undefined || !root.isConnected) {
119
- placed = false
120
- tryPlace()
121
- return
122
- }
123
- if (!root.contains(entry)) {
124
- placed = placeEntry(root, entry)
125
- }
126
- })
127
-
128
- // Reflect the panel's open state on the row (active highlight).
129
- const syncActive = () => {
130
- if (controller.getSnapshot().panelOpen) entry.dataset.active = 'true'
131
- else delete entry.dataset.active
132
- }
177
+ const bodyObserver = new MutationObserver(ensure)
178
+ bodyObserver.observe(document.body, { childList: true, subtree: true })
133
179
  const unsubscribe = controller.subscribe(syncActive)
134
- syncActive()
135
-
136
- tryPlace()
180
+ ensure()
137
181
 
138
182
  return () => {
139
- waitObserver.disconnect()
140
- rootObserver.disconnect()
183
+ bodyObserver.disconnect()
141
184
  unsubscribe()
142
- entry.remove()
185
+ tabs?.remove()
186
+ historyHost?.remove()
187
+ if (originalButton !== undefined && originalButton.isConnected) restoreShellButton(originalButton)
188
+ if (root !== undefined) delete root.dataset.dshImagegenSidebarRoot
143
189
  }
144
190
  }