@goodandready/dsh-image-gen 0.11.26 → 0.11.28

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.
@@ -112,3 +112,28 @@ export function renderToolOutput(value) {
112
112
  }
113
113
  return blocks
114
114
  }
115
+
116
+ export function buildPresentationMeta(value) {
117
+ if (!value || typeof value !== 'object') return null
118
+ const meta = {
119
+ url: value.url,
120
+ path: value.path,
121
+ width: value.width,
122
+ height: value.height,
123
+ seed: value.seed,
124
+ prompt: value.prompt,
125
+ provider: value.provider,
126
+ model: value.model,
127
+ attachment: value.attachment,
128
+ attachmentId: value.attachment?.attachmentId || value.attachmentId,
129
+ }
130
+ if (value.images) meta.images = value.images
131
+ if (value.cells) meta.cells = value.cells
132
+ if (value.base_prompt) meta.base_prompt = value.base_prompt
133
+ if (value.blind_mode !== undefined) meta.blind_mode = value.blind_mode
134
+ if (value.light) meta.light = value.light
135
+ if (value.dark) meta.dark = value.dark
136
+ if (value.html_snippet) meta.html_snippet = value.html_snippet
137
+ if (value.css_snippet) meta.css_snippet = value.css_snippet
138
+ return meta
139
+ }
package/lib/client.js CHANGED
@@ -145,11 +145,15 @@ window.__ModuleLoader__.load({
145
145
  .ig-progress-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary);font-weight:500}
146
146
  .ig-progress-track{height:6px;width:100%;background:var(--dsw-alias-bg-layer-1);border-radius:3px;overflow:hidden;position:relative}
147
147
  .ig-progress-fill{height:100%;background:var(--dsw-alias-state-brand);border-radius:3px;transition:width 0.25s ease-in-out}
148
+ .ig-progress-indeterminate{width:40%;animation:ig-indeterminate-slide 1.5s infinite ease-in-out}
149
+ @keyframes ig-indeterminate-slide{0%{transform:translateX(-100%)}50%{transform:translateX(150%)}100%{transform:translateX(300%)}}
148
150
 
149
151
  .ig-draft-overlay{position:absolute;bottom:8px;left:8px;padding:3px 8px;border-radius:4px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 80%, transparent);font-size:11px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px;backdrop-filter:blur(4px)}
150
152
 
151
153
  @media (prefers-reduced-motion: reduce){
152
154
  .ig-draft-img,.ig-final-img{transition:none !important}
155
+ .ig-progress-fill{transition:none !important;animation:none !important}
156
+ .ig-progress-indeterminate{animation:none !important;width:100%;opacity:0.6}
153
157
  }
154
158
 
155
159
  /* Asset Vault & Studio (#158, #159) */
@@ -1369,17 +1373,53 @@ window.__ModuleLoader__.load({
1369
1373
  }
1370
1374
 
1371
1375
  // -------------------------------------------------------------- Toolview Component
1372
- const IMAGE_URL_RE = /https?:\/\/[^\s)]+\.(png|jpg|jpeg|webp|gif)(\?[^\s)]*)?/i
1376
+ const HTTP_IMAGE_URL_RE = /https?:\/\/[^\s)]+\.(png|jpg|jpeg|webp|gif)(\?[^\s)]*)?/i
1377
+ const RELATIVE_IMAGE_URL_RE = /(\/dsh-image-gen\/image\?[^\s)"'<>]+)/i
1378
+ const MARKDOWN_IMAGE_RE = /!\[.*?\]\(((\/|https?:\/\/)[^\s)]+)\)/i
1379
+ const MARKDOWN_LINK_RE = /\[.*?\]\(((\/|https?:\/\/)[^\s)]+)\)/i
1373
1380
 
1374
1381
  function readResult(block) {
1375
1382
  if (!block) return { text: '', url: '', attachment: null }
1376
1383
  const text = block.output || block.text || ''
1377
- const attachment = block.attachment || (block.attachments && block.attachments[0]) || null
1378
- const linked = text.match(IMAGE_URL_RE)
1384
+ const meta = block.presentationMeta || block.meta || block.data || block.result || null
1385
+
1386
+ let attachment = block.attachment
1387
+ || (Array.isArray(block.attachments) && block.attachments[0])
1388
+ || meta?.attachment
1389
+ || null
1390
+
1391
+ let url = meta?.url || block.url || ''
1392
+
1393
+ if (!url && attachment) {
1394
+ url = attachmentImageUrl(attachment)
1395
+ }
1396
+
1397
+ let cleanText = text
1398
+ if (!url && text) {
1399
+ const mdImg = text.match(MARKDOWN_IMAGE_RE)
1400
+ const relUrl = text.match(RELATIVE_IMAGE_URL_RE)
1401
+ const httpUrl = text.match(HTTP_IMAGE_URL_RE)
1402
+ const mdLink = text.match(MARKDOWN_LINK_RE)
1403
+
1404
+ if (mdImg) {
1405
+ url = mdImg[1]
1406
+ cleanText = text.replace(mdImg[0], '').trim()
1407
+ } else if (relUrl) {
1408
+ url = relUrl[1]
1409
+ cleanText = text.replace(relUrl[0], '').trim()
1410
+ } else if (httpUrl) {
1411
+ url = httpUrl[0]
1412
+ cleanText = text.replace(httpUrl[0], '').trim()
1413
+ } else if (mdLink) {
1414
+ url = mdLink[1]
1415
+ cleanText = text.replace(mdLink[0], '').trim()
1416
+ }
1417
+ }
1418
+
1379
1419
  return {
1380
1420
  attachment,
1381
- url: linked ? linked[0] : '',
1382
- text: linked ? text.replace(linked[0], '').trim() : text,
1421
+ url,
1422
+ text: cleanText,
1383
1423
  }
1384
1424
  }
1385
1425
 
@@ -1586,6 +1626,7 @@ window.__ModuleLoader__.load({
1586
1626
  const draftSrc = block && (block.draftUrl || block.previewUrl || '')
1587
1627
  body.push(react.createElement(ProgressiveImagePreview, {
1588
1628
  key: 'prog-img',
1629
+ callId: block && (block.id || block.callId || block.toolCallId || ''),
1589
1630
  src: displaySrc,
1590
1631
  draftSrc,
1591
1632
  isRunning: running,
@@ -1694,8 +1735,9 @@ window.__ModuleLoader__.load({
1694
1735
  react,
1695
1736
  t,
1696
1737
  parsed,
1697
- imgUrl: parsed.imageUrl || parsed.url || parsed.attachment?.url,
1698
- targetRef: parsed.attachmentId || parsed.attachment?.attachmentId || parsed.imageUrl || parsed.url,
1738
+ imgUrl: parsed.url || (parsed.attachment ? attachmentImageUrl(parsed.attachment) : ''),
1739
+ targetRef: (parsed.attachment && (parsed.attachment.attachmentId || parsed.attachment.id)) || parsed.url || 'latest',
1740
+ sendActionPrompt,
1699
1741
  onClose: () => setInpaintOpen(false),
1700
1742
  }) : null
1701
1743
 
@@ -1761,23 +1803,76 @@ window.__ModuleLoader__.load({
1761
1803
  current.seed !== undefined ? `seed: ${current.seed}` : (current.timestamp ? new Date(current.timestamp).toLocaleTimeString() : '')
1762
1804
  )
1763
1805
  )
1764
- } // -------------------------------------------------------------- Progressive Draft & Live Progress Preview (#147, #329)
1806
+ } // -------------------------------------------------------------- Progressive Draft & Live Progress Preview (#147, #329, #379)
1765
1807
  function ProgressiveImagePreview(props) {
1766
1808
  const src = props.src
1767
- const draftSrc = props.draftSrc
1768
1809
  const isRunning = props.isRunning
1769
- const progress = typeof props.progress === 'number' ? Math.min(100, Math.max(0, props.progress)) : undefined
1770
- const step = props.step
1771
- const stage = props.stage || (progress !== undefined && progress < 100 ? 'Rendering...' : 'Complete')
1810
+ const callId = props.callId
1772
1811
  const alt = props.alt || 'Generated image'
1773
1812
  const t = props.t || ((k) => k)
1774
1813
 
1775
1814
  const [loaded, setLoaded] = react.useState(false)
1815
+ const [liveState, setLiveState] = react.useState({
1816
+ progress: undefined,
1817
+ step: undefined,
1818
+ stage: undefined,
1819
+ draftSrc: '',
1820
+ })
1776
1821
 
1777
1822
  react.useEffect(() => {
1778
1823
  setLoaded(false)
1779
1824
  }, [src])
1780
1825
 
1826
+ // Subscribe to live events transport when running and callId is present (#379)
1827
+ react.useEffect(() => {
1828
+ const EventSourceImpl = typeof window !== 'undefined' ? window.EventSource : undefined
1829
+ if (!isRunning || !callId || !EventSourceImpl) {
1830
+ return
1831
+ }
1832
+ let es
1833
+ try {
1834
+ es = new EventSourceImpl('/dsh-image-gen/live-events?callId=' + encodeURIComponent(callId))
1835
+ es.onmessage = (e) => {
1836
+ try {
1837
+ const data = JSON.parse(e.data)
1838
+ if (!data) return
1839
+ setLiveState((prev) => ({
1840
+ progress: typeof data.progress === 'number' ? Math.min(100, Math.max(0, data.progress)) : prev.progress,
1841
+ step: data.step !== undefined ? data.step : prev.step,
1842
+ stage: data.stage !== undefined ? data.stage : prev.stage,
1843
+ draftSrc: data.draftUrl || prev.draftSrc,
1844
+ }))
1845
+ } catch (_err) {
1846
+ // Ignore event parsing errors
1847
+ }
1848
+ }
1849
+ es.onerror = () => {
1850
+ // EventSource handles retries or terminates on close
1851
+ }
1852
+ } catch (_err) {
1853
+ // Ignore EventSource creation errors
1854
+ }
1855
+
1856
+ return () => {
1857
+ if (es) {
1858
+ try {
1859
+ es.close()
1860
+ } catch (_err) {
1861
+ // Ignore EventSource close errors
1862
+ }
1863
+ }
1864
+ }
1865
+ }, [isRunning, callId])
1866
+
1867
+ const draftSrc = liveState.draftSrc || props.draftSrc
1868
+ const progress = liveState.progress !== undefined
1869
+ ? liveState.progress
1870
+ : (typeof props.progress === 'number' ? Math.min(100, Math.max(0, props.progress)) : undefined)
1871
+ const step = liveState.step !== undefined ? liveState.step : props.step
1872
+ const stage = liveState.stage
1873
+ || props.stage
1874
+ || (progress !== undefined && progress < 100 ? (t('card.rendering') || 'Rendering...') : (isRunning ? (t('card.generating') || 'Generating...') : 'Complete'))
1875
+
1781
1876
  const showProgress = isRunning || (progress !== undefined && progress < 100)
1782
1877
 
1783
1878
  const progressElement = showProgress ? react.createElement(
@@ -1793,8 +1888,8 @@ window.__ModuleLoader__.load({
1793
1888
  'div',
1794
1889
  { className: 'ig-progress-track' },
1795
1890
  react.createElement('div', {
1796
- className: 'ig-progress-fill',
1797
- style: { width: (progress !== undefined ? progress : 45) + '%' },
1891
+ className: 'ig-progress-fill' + (progress === undefined ? ' ig-progress-indeterminate' : ''),
1892
+ style: progress !== undefined ? { width: progress + '%' } : undefined,
1798
1893
  })
1799
1894
  )
1800
1895
  ) : null
@@ -1842,10 +1937,17 @@ window.__ModuleLoader__.load({
1842
1937
  progressElement
1843
1938
  )
1844
1939
  }
1845
- // 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285, #301, #328).
1940
+ // 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285, #301, #328, #380).
1941
+
1942
+ function buildEditInstruction(opts) {
1943
+ const p = (opts && opts.prompt ? opts.prompt.trim() : '') || 'inpaint masked region'
1944
+ const img = (opts && opts.image) || 'latest'
1945
+ const m = (opts && opts.mask) || ''
1946
+ return 'Use edit_image with prompt: "' + p + '", image: "' + img + '", mask: "' + m + '"'
1947
+ }
1846
1948
 
1847
1949
  function InpaintCanvas(props) {
1848
- const { imgUrl, targetRef, parsed, onClose, t } = props
1950
+ const { imgUrl, targetRef, parsed, onClose, sendActionPrompt, t } = props
1849
1951
  const canvasRef = react.useRef(null)
1850
1952
  const isDrawingRef = react.useRef(false)
1851
1953
  const undoStackRef = react.useRef([])
@@ -1856,8 +1958,12 @@ window.__ModuleLoader__.load({
1856
1958
  const [inpaintPrompt, setInpaintPrompt] = react.useState('')
1857
1959
  const [canvasDims, setCanvasDims] = react.useState({ width: 512, height: 512, ratio: '1/1' })
1858
1960
 
1859
- const effectiveImgUrl = imgUrl || parsed?.imageUrl || parsed?.url || parsed?.attachment?.url || ''
1860
- const effectiveTargetRef = targetRef || parsed?.attachmentId || parsed?.attachment?.attachmentId || parsed?.imageUrl || parsed?.url || 'current image'
1961
+ const effectiveImgUrl = imgUrl || (parsed?.attachment ? attachmentImageUrl(parsed.attachment) : (parsed?.url || ''))
1962
+ const effectiveTargetRef = targetRef
1963
+ || (parsed?.attachment && (parsed.attachment.attachmentId || parsed.attachment.id))
1964
+ || parsed?.attachmentId
1965
+ || parsed?.url
1966
+ || 'latest'
1861
1967
 
1862
1968
  const initCanvas = react.useCallback(() => {
1863
1969
  const canvas = canvasRef.current
@@ -1877,11 +1983,8 @@ window.__ModuleLoader__.load({
1877
1983
  if (e.target && e.target.naturalWidth && e.target.naturalHeight) {
1878
1984
  const nw = e.target.naturalWidth
1879
1985
  const nh = e.target.naturalHeight
1880
- // Scale dimensions down to manageable working canvas size (max 768)
1881
- const scale = Math.min(1, 768 / Math.max(nw, nh))
1882
- const cw = Math.round(nw * scale)
1883
- const ch = Math.round(nh * scale)
1884
- setCanvasDims({ width: cw, height: ch, ratio: `${cw}/${ch}` })
1986
+ // Preserve 1:1 intrinsic source dimensions for accurate mask generation (#380)
1987
+ setCanvasDims({ width: nw, height: nh, ratio: `${nw}/${nh}` })
1885
1988
  }
1886
1989
  }
1887
1990
 
@@ -1889,11 +1992,12 @@ window.__ModuleLoader__.load({
1889
1992
  const rect = canvas.getBoundingClientRect()
1890
1993
  const clientX = e.touches ? e.touches[0].clientX : e.clientX
1891
1994
  const clientY = e.touches ? e.touches[0].clientY : e.clientY
1892
- const scaleX = canvas.width / rect.width
1893
- const scaleY = canvas.height / rect.height
1995
+ const scaleX = canvas.width / (rect.width || canvas.width)
1996
+ const scaleY = canvas.height / (rect.height || canvas.height)
1894
1997
  return {
1895
1998
  x: (clientX - rect.left) * scaleX,
1896
1999
  y: (clientY - rect.top) * scaleY,
2000
+ scale: (scaleX + scaleY) / 2,
1897
2001
  }
1898
2002
  }
1899
2003
 
@@ -1905,16 +2009,17 @@ window.__ModuleLoader__.load({
1905
2009
  if (!ctx) return
1906
2010
  isDrawingRef.current = true
1907
2011
  const pos = getPos(e, canvas)
2012
+ const actualBrush = brushSize * (pos.scale || 1)
1908
2013
  ctx.beginPath()
1909
2014
  ctx.moveTo(pos.x, pos.y)
1910
- ctx.lineWidth = brushSize
2015
+ ctx.lineWidth = actualBrush
1911
2016
  ctx.lineCap = 'round'
1912
2017
  ctx.lineJoin = 'round'
1913
2018
  const color = toolMode === 'eraser' ? 'black' : 'white'
1914
2019
  ctx.strokeStyle = color
1915
2020
  ctx.fillStyle = color
1916
2021
 
1917
- ctx.arc(pos.x, pos.y, brushSize / 2, 0, Math.PI * 2)
2022
+ ctx.arc(pos.x, pos.y, actualBrush / 2, 0, Math.PI * 2)
1918
2023
  ctx.fill()
1919
2024
  ctx.beginPath()
1920
2025
  ctx.moveTo(pos.x, pos.y)
@@ -1929,25 +2034,26 @@ window.__ModuleLoader__.load({
1929
2034
  const ctx = canvas.getContext('2d')
1930
2035
  if (!ctx) return
1931
2036
  const pos = getPos(e, canvas)
1932
- ctx.lineWidth = brushSize
2037
+ const actualBrush = brushSize * (pos.scale || 1)
2038
+ ctx.lineWidth = actualBrush
1933
2039
  ctx.lineCap = 'round'
1934
2040
  ctx.lineJoin = 'round'
1935
2041
  const color = toolMode === 'eraser' ? 'black' : 'white'
1936
2042
  ctx.strokeStyle = color
1937
2043
  ctx.lineTo(pos.x, pos.y)
1938
2044
  ctx.stroke()
1939
- setHasStrokes(true)
2045
+ ctx.beginPath()
2046
+ ctx.moveTo(pos.x, pos.y)
1940
2047
  }
1941
2048
 
1942
- function stopDraw(e) {
2049
+ function stopDraw() {
1943
2050
  if (!isDrawingRef.current) return
1944
2051
  isDrawingRef.current = false
1945
2052
  const canvas = canvasRef.current
1946
2053
  if (!canvas) return
1947
2054
  const ctx = canvas.getContext('2d')
1948
2055
  if (!ctx) return
1949
- ctx.closePath()
1950
-
2056
+ ctx.beginPath()
1951
2057
  if (undoStackRef.current.length >= 15) {
1952
2058
  undoStackRef.current.shift()
1953
2059
  }
@@ -2000,7 +2106,15 @@ window.__ModuleLoader__.load({
2000
2106
  if (!canvas) return
2001
2107
  const maskDataUrl = canvas.toDataURL('image/png')
2002
2108
  const promptText = inpaintPrompt.trim() || 'inpaint masked region'
2003
- const instruction = 'Use edit_image with prompt: "' + promptText + '", source_image: "' + effectiveTargetRef + '", mask: "' + maskDataUrl + '"'
2109
+ const instruction = buildEditInstruction({
2110
+ prompt: promptText,
2111
+ image: effectiveTargetRef,
2112
+ mask: maskDataUrl,
2113
+ })
2114
+
2115
+ if (typeof sendActionPrompt === 'function') {
2116
+ sendActionPrompt(instruction)
2117
+ }
2004
2118
 
2005
2119
  if (typeof navigator !== 'undefined' && navigator.clipboard) {
2006
2120
  navigator.clipboard.writeText(instruction)
@@ -2188,7 +2302,18 @@ window.__ModuleLoader__.load({
2188
2302
  const t = props.t || ((k) => k)
2189
2303
  const block = props.block || {}
2190
2304
  const parsed = react.useMemo(() => {
2305
+ const meta = block.presentationMeta || block.meta || block.data || block.result
2306
+ if (meta && Array.isArray(meta.cells) && meta.cells.length) {
2307
+ return meta
2308
+ }
2191
2309
  const raw = block.output || block.text || ''
2310
+ const tagMatch = raw.match(/<dsh-image-gen-matrix>([\s\S]*?)<\/dsh-image-gen-matrix>/)
2311
+ if (tagMatch) {
2312
+ const fromTag = tryParseJsonObject(tagMatch[1])
2313
+ if (fromTag && Array.isArray(fromTag.cells) && fromTag.cells.length) {
2314
+ return fromTag
2315
+ }
2316
+ }
2192
2317
  const obj = tryParseJsonObject(raw)
2193
2318
  return obj || { base_prompt: '', blind_mode: false, cells: [] }
2194
2319
  }, [block])
@@ -2307,12 +2432,27 @@ window.__ModuleLoader__.load({
2307
2432
  let args = {}
2308
2433
  try { if (raw) args = JSON.parse(raw) } catch (_) { /* invalid json */ }
2309
2434
 
2310
- let data = {}
2311
- try {
2312
- const out = block && (block.output || block.result || block.text || '')
2313
- if (typeof out === 'object') data = out
2314
- else if (typeof out === 'string') data = JSON.parse(out)
2315
- } catch (_) { /* invalid json */ }
2435
+ let data = block && (block.presentationMeta || block.meta || block.data || block.result)
2436
+ if (!data || (!data.light && !data.dark)) {
2437
+ const out = block && (block.output || block.text || '')
2438
+ const tagMatch = typeof out === 'string' ? out.match(/<dsh-image-gen-theme-pair>([\s\S]*?)<\/dsh-image-gen-theme-pair>/) : null
2439
+ if (tagMatch) {
2440
+ try {
2441
+ data = JSON.parse(tagMatch[1])
2442
+ } catch (_err) {
2443
+ // Malformed XML payload
2444
+ }
2445
+ }
2446
+ if (!data || (!data.light && !data.dark)) {
2447
+ try {
2448
+ if (typeof out === 'object') data = out
2449
+ else if (typeof out === 'string') data = JSON.parse(out)
2450
+ } catch (_err) {
2451
+ // Malformed fallback output
2452
+ }
2453
+ }
2454
+ }
2455
+ data = data || {}
2316
2456
 
2317
2457
  const light = data.light || {}
2318
2458
  const dark = data.dark || {}
@@ -73,6 +73,10 @@ export function interpolateComfyWorkflow(workflowGraph, ctx) {
73
73
  const cfgVal = Number.isFinite(ctx.cfg) ? ctx.cfg : 7
74
74
  const modelVal = String(ctx.model ?? 'v1-5-pruned-emaonly.safetensors')
75
75
  const imageVal = String(ctx.image ?? '')
76
+ const faceImageVal = String(ctx.faceImage ?? ctx.face_image ?? '')
77
+ const faceWeightVal = Number.isFinite(ctx.faceWeight ?? ctx.face_weight) ? (ctx.faceWeight ?? ctx.face_weight) : 0.75
78
+ const refImageVal = String(ctx.referenceImage ?? ctx.reference_image ?? '')
79
+ const refWeightVal = Number.isFinite(ctx.referenceWeight ?? ctx.reference_weight) ? (ctx.referenceWeight ?? ctx.reference_weight) : 0.65
76
80
 
77
81
  function transformValue(val) {
78
82
  if (typeof val === 'string') {
@@ -83,6 +87,8 @@ export function interpolateComfyWorkflow(workflowGraph, ctx) {
83
87
  if (trimmed === '{{height}}') return heightVal
84
88
  if (trimmed === '{{steps}}') return stepsVal
85
89
  if (trimmed === '{{cfg}}') return cfgVal
90
+ if (trimmed === '{{face_weight}}' || trimmed === '{{faceWeight}}') return faceWeightVal
91
+ if (trimmed === '{{reference_weight}}' || trimmed === '{{referenceWeight}}') return refWeightVal
86
92
 
87
93
  // General string replacements
88
94
  return val
@@ -96,6 +102,14 @@ export function interpolateComfyWorkflow(workflowGraph, ctx) {
96
102
  .replace(/\{\{cfg\}\}/g, String(cfgVal))
97
103
  .replace(/\{\{model\}\}/g, modelVal)
98
104
  .replace(/\{\{image\}\}/g, imageVal)
105
+ .replace(/\{\{face_image\}\}/g, faceImageVal)
106
+ .replace(/\{\{faceImage\}\}/g, faceImageVal)
107
+ .replace(/\{\{face_weight\}\}/g, String(faceWeightVal))
108
+ .replace(/\{\{faceWeight\}\}/g, String(faceWeightVal))
109
+ .replace(/\{\{reference_image\}\}/g, refImageVal)
110
+ .replace(/\{\{referenceImage\}\}/g, refImageVal)
111
+ .replace(/\{\{reference_weight\}\}/g, String(refWeightVal))
112
+ .replace(/\{\{referenceWeight\}\}/g, String(refWeightVal))
99
113
  }
100
114
  if (Array.isArray(val)) {
101
115
  return val.map(transformValue)
@@ -72,8 +72,11 @@ export async function prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId,
72
72
  let effectiveNegative = styleInfo.negativePrompt
73
73
  const activeAnchor = getSessionAnchor(sessionId)
74
74
  let resolvedFaceRef
75
+ let resolvedRefImage
75
76
 
76
77
  const faceRefInput = args.face_reference || (activeAnchor && (activeAnchor.mode === 'face' || activeAnchor.mode === 'character') ? activeAnchor.image : undefined)
78
+ const refImageInput = args.reference_image || (activeAnchor && activeAnchor.mode === 'style' ? activeAnchor.image : undefined)
79
+
77
80
  if (faceRefInput) {
78
81
  try {
79
82
  resolvedFaceRef = await resolveSource(ctx, exec, faceRefInput)
@@ -90,9 +93,22 @@ export async function prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId,
90
93
  if (facePlan.negativePrompt) {
91
94
  effectiveNegative = effectiveNegative ? `${effectiveNegative}, ${facePlan.negativePrompt}` : facePlan.negativePrompt
92
95
  }
93
- } else if (args.reference_image) {
94
- effectivePrompt = `${effectivePrompt}, visual reference anchor`
95
- } else if (activeAnchor) {
96
+ }
97
+
98
+ if (refImageInput) {
99
+ try {
100
+ resolvedRefImage = await resolveSource(ctx, exec, refImageInput)
101
+ } catch (_err) {
102
+ resolvedRefImage = { url: refImageInput }
103
+ }
104
+ if (!faceRefInput) {
105
+ if (activeAnchor) {
106
+ effectivePrompt = applyAnchorPromptHints(activeAnchor, effectivePrompt)
107
+ } else {
108
+ effectivePrompt = `${effectivePrompt}, visual reference anchor`
109
+ }
110
+ }
111
+ } else if (!faceRefInput && activeAnchor) {
96
112
  effectivePrompt = applyAnchorPromptHints(activeAnchor, effectivePrompt)
97
113
  }
98
114
 
@@ -113,6 +129,7 @@ export async function prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId,
113
129
  styleInfo,
114
130
  activeAnchor,
115
131
  resolvedFaceRef,
132
+ resolvedRefImage,
116
133
  }
117
134
  }
118
135
 
package/lib/index.js CHANGED
@@ -44,6 +44,7 @@ import { registerImageCommand } from './commands.js'
44
44
  import { registerAllTools } from './register-tools.js'
45
45
  import { registerPluginUpdater } from './updater.js'
46
46
  import { registerVaultRoutes } from './vault.js'
47
+ import { registerLiveProgressRoutes } from './live-progress.js'
47
48
  import { registerSettingsRoutes } from './settings-route.js'
48
49
  import { clearAllAnchors } from './anchor-helpers.js'
49
50
  import { resetLoopGuard, getLoopGuardState } from './loop-guard.js'
@@ -418,6 +419,9 @@ export function apply(ctx, rawConfig) {
418
419
  // Vault route (#159)
419
420
  registerVaultRoutes(ctx)
420
421
 
422
+ // Live events route (#379)
423
+ registerLiveProgressRoutes(ctx)
424
+
421
425
  // Reset loop guard when user sends a new message (#370)
422
426
  if (typeof ctx.on === 'function') {
423
427
  ctx.on('chat/message', (session) => {