Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/main/host/attach.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as ipc from '../lib/ipc'
import { attachCustomerIoMessaging } from '../lib/customerIoMessaging'
import { getAppVersion } from '../lib/ipc'
import { attachSessionDownloadHandler } from '../lib/comfyDownloadManager'
import { getModelDownloadContentScript } from '../lib/comfyContentScript'
Expand Down Expand Up @@ -173,7 +172,7 @@ export function attachInstall(entry: ComfyWindowEntry, opts: AttachInstallOpts):
// state field keeps a later detach from clobbering identity twice.
entry.previewInstallationId = null
indexInstallationId(installationId, entry.windowKey)
const stopCustomerIo = isLocal ? attachCustomerIoMessaging(entry) : () => {}
entry.refreshCustomerIo?.()

// Seed the MRU tracker if this in-place attach happens on the
// already-focused host: no fresh OS `'focus'` event would fire to
Expand Down Expand Up @@ -665,7 +664,6 @@ export function attachInstall(entry: ComfyWindowEntry, opts: AttachInstallOpts):
// Retire async work still pending from this attach so a late resolution
// can't touch a detached or re-attached view.
attachActive = false
stopCustomerIo()
deactivateFirebaseAuthReporter(comfyContents)
installationEvents.off('updated', onInstallationUpdated)
cancelFailRetry()
Expand Down
4 changes: 4 additions & 0 deletions src/main/host/createHostWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import * as mainTelemetry from '../lib/telemetry'
import { getUserTier } from '../lib/userTier'
import { trackFirebaseAuthReporter } from '../lib/firebaseAuthIdentity'
import { attachCustomerIoMessaging } from '../lib/customerIoMessaging'
import { forwardDatadogError } from '../lib/processErrorHandlers'
import { recordDashboardSurface, recordInstanceSurface } from '../lib/lastSession'
import * as settings from '../settings'
Expand Down Expand Up @@ -1025,6 +1026,8 @@ export function createHostWindow(opts: CreateHostWindowOpts): CreateHostWindowRe
// reference, not by a copy at literal-build time.
entry.detachInstall = () => fx.detachInstallImpl(entry)
registerHostEntry(entry)
const stopMessaging = attachCustomerIoMessaging(entry)
comfyWindow.once('closed', stopMessaging)

return { windowKey, comfyWindow, titleBarView, comfyView, entry, layoutViews }
}
Expand Down Expand Up @@ -1219,6 +1222,7 @@ export function rebuildComfyViewIfNeeded(
entry.window.contentView.removeChildView(oldView)
if (!oldView.webContents.isDestroyed()) oldView.webContents.close()
entry.comfyView = newView
entry.refreshCustomerIo?.()
entry.constructedPartition = expectedPartition
}

Expand Down
4 changes: 3 additions & 1 deletion src/main/host/panelView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export function ensurePanelView(
// Insert at zero size, behind the comfy view; layoutViews handles positioning.
panelView.setBounds({ x: 0, y: TITLEBAR_HEIGHT + 1, width: 0, height: 0 })
panelView.setVisible(false)
entry.panelView = panelView
entry.refreshCustomerIo?.()

// Push the latest body mode (may differ from initialPanel) and steal focus if focused.
panelView.webContents.once('did-finish-load', () => {
Expand Down Expand Up @@ -109,7 +111,6 @@ export function ensurePanelView(
void loadPromise.catch(() => {})

_registerExtraBroadcastTarget(panelView.webContents)
entry.panelView = panelView
return panelView
}

Expand All @@ -123,6 +124,7 @@ export function destroyPanelView(entry: ComfyWindowEntry): void {
if (!entry.panelView) return
const oldPanel = entry.panelView
entry.panelView = null
entry.refreshCustomerIo?.()
if (!oldPanel.webContents.isDestroyed()) {
_unregisterExtraBroadcastTarget(oldPanel.webContents)
oldPanel.webContents.close()
Expand Down
2 changes: 1 addition & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,12 +835,12 @@ ipcMain.on('comfy-window:set-first-use-mode', (event, payload: { mode: unknown }
for (const entry of comfyWindows.values()) {
if (entry.panelView?.webContents === event.sender) {
entry.firstUseMode = mode
entry.refreshCustomerIo?.()
if (!entry.titleBarView.webContents.isDestroyed()) {
entry.titleBarView.webContents.send('comfy-titlebar:first-use-mode-changed', mode)
}
return
}
return
}
})

Expand Down
100 changes: 100 additions & 0 deletions src/main/lib/customerIoDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ipcMain, shell, type IpcMainEvent, type WebContents } from 'electron'
import { CUSTOMER_IO_ACTION, CUSTOMER_IO_READY, CUSTOMER_IO_STATE } from '../../shared/customerIo'
import type { CustomerIoSession } from '../../shared/customerIo'
import { isLoopbackOrigin } from './verifiedLocalFirebaseAuth'

const linkHandlers = new WeakMap<WebContents, (event: IpcMainEvent, action: unknown) => void>()
let linkHandlerInstalled = false

export function attachCustomerIoDocument(
contents: WebContents,
getSession: () => CustomerIoSession | null,
documentChanged: () => void
): { refresh: () => void; dispose: () => void; isReady: () => boolean } {
if (!linkHandlerInstalled) {
ipcMain.on(CUSTOMER_IO_ACTION, (event, action: unknown) => {
// Always answer, including when a retained page has already detached.
event.returnValue = false
linkHandlers.get(event.sender)?.(event, action)
})
linkHandlerInstalled = true
}
let ready = false
let lastState = ''
let pendingNavigation: string | null = null
const send = (state: CustomerIoSession | null): void => {
if (!ready || contents.isDestroyed()) return
const serialized = JSON.stringify(state)
if (serialized === lastState) return
try {
// Pin the recipient to the current document, rather than a replacement navigation.
contents.mainFrame.send(CUSTOMER_IO_STATE, state)
lastState = serialized
} catch {
ready = false
lastState = ''
}
}
const refresh = (): void => send(getSession())
const onReady = (event: IpcMainEvent, channel: string, action?: unknown): void => {
if (channel !== CUSTOMER_IO_READY && channel !== CUSTOMER_IO_ACTION) return
if (channel === CUSTOMER_IO_ACTION) event.returnValue = false
const frame = event.senderFrame
if (!frame || frame !== contents.mainFrame) return
if (channel === CUSTOMER_IO_ACTION) {
if (!ready || !getSession() || typeof action !== 'string') return
try {
const navigation = action.startsWith('gist://loadPage?url=')
const url = new URL(
navigation ? action.slice('gist://loadPage?url='.length) : action,
contents.getURL()
)
// Block workflow replacement even for unsupported relative/local links.
if (navigation) pendingNavigation = url.href
if (!['https:', 'http:', 'mailto:'].includes(url.protocol)) return
if (isLoopbackOrigin(url.origin)) return
// A synchronous acknowledgement installs this guard before the SDK tries
// to navigate ComfyUI away from the user's workflow.
void shell.openExternal(url.href).catch(() => {})
event.returnValue = true
} catch {
/* Unsupported actions are left to the in-app SDK. */
}
return
}
ready = true
lastState = ''
documentChanged()
refresh()
}
const onNavigation = (
details: Electron.Event<Electron.WebContentsDidStartNavigationEventParams>
): void => {
if (!details.isMainFrame || details.isSameDocument) return
send(null)
ready = false
lastState = ''
documentChanged()
}
const onLinkNavigation = (event: Electron.Event, url: string): void => {
if (url !== pendingNavigation) return
pendingNavigation = null
event.preventDefault()
}
const onMessage = (event: IpcMainEvent, channel: string): void => {
if (channel === CUSTOMER_IO_READY) onReady(event, channel)
}
contents.on('ipc-message', onMessage)
linkHandlers.set(contents, (event, action) => onReady(event, CUSTOMER_IO_ACTION, action))
contents.on('will-navigate', onLinkNavigation)
contents.on('did-start-navigation', onNavigation)
const dispose = (): void => {
send(null)
ready = false
contents.off('ipc-message', onMessage)
linkHandlers.delete(contents)
contents.off('will-navigate', onLinkNavigation)
contents.off('did-start-navigation', onNavigation)
}
return { refresh, dispose, isReady: () => ready }
}
Loading
Loading