Compare commits

...

6 Commits

Author SHA1 Message Date
Shibo Zhou
68aa291e5b Merge branch 'main' into feat/app-mode-panel-resize-telemetry 2026-06-01 14:47:39 -07:00
Shibo Zhou
4dc831e5da Merge branch 'main' into feat/app-mode-panel-resize-telemetry 2026-06-01 14:41:19 -07:00
Shibo Zhou
112f4b254a Merge branch 'main' into feat/app-mode-panel-resize-telemetry 2026-06-01 13:14:54 -07:00
Shibo Zhou
f44f6f8525 Merge branch 'main' into feat/app-mode-panel-resize-telemetry 2026-06-01 11:50:03 -07:00
shibozhou
48f007f4db fix: read the input panel opposite the sidebar in App Mode resize telemetry
The input/prompt panel renders opposite the sidebar, so the resize lookup
must use the panel on the opposite side of Comfy.Sidebar.Location. The
previous code used the same side, mislabeling sidebar drags as input
resizes (or dropping the event) for left-sidebar layouts.

Extract the panel selection + metadata into a unit-tested helper
(buildInputPanelResizeMetadata) and emit only on genuine input-panel
resizes, skipping no-op and first-measurement reads. Tighten the metadata
type accordingly and cover the provider/registry dispatch.
2026-06-01 11:00:52 -07:00
shibozhou
2727587e35 feat: track App Mode panel resizes (app:app_mode_panel_resized)
Emit app:app_mode_panel_resized when a user drags the App Mode splitter (LinearView), carrying panel, direction (wider/narrower/same), previous and new pixel width, and sidebar_location. App Mode has no text-size control, so widening the input panel is the only way to enlarge the prompt — this measures how often users do it and whether they widen, testing whether the default panel is too narrow. Extends useStablePrimeVueSplitterSizer with an optional onResize callback (old->new widths captured before persist) so the generic composable stays telemetry-free; LinearView fires the event for the input panel in consume mode only.
2026-06-01 09:46:56 -07:00
10 changed files with 254 additions and 6 deletions

View File

@@ -130,4 +130,44 @@ describe('useStablePrimeVueSplitterSizer', () => {
expect(validRef.value!.style.flexBasis).toBe('200px')
})
it('passes old→new widths to onResize (null old on first resize)', async () => {
const panelRef = createPanel(320)
const trigger = ref(0)
const onResize = vi.fn()
const { onResizeEnd } = useStablePrimeVueSplitterSizer(
[{ ref: panelRef, storageKey: 'test-onresize-first' }],
[trigger],
onResize
)
await flushWatcher()
onResizeEnd(resizeEndEvent())
expect(onResize).toHaveBeenCalledWith([
{ storageKey: 'test-onresize-first', oldWidth: null, newWidth: 320 }
])
})
it('reports the previous width as old on the next resize', async () => {
const panelRef = createPanel(320)
const trigger = ref(0)
const onResize = vi.fn()
const { onResizeEnd } = useStablePrimeVueSplitterSizer(
[{ ref: panelRef, storageKey: 'test-onresize-next' }],
[trigger],
onResize
)
await flushWatcher()
onResizeEnd(resizeEndEvent())
panelRef.value = createPanel(480).value
onResizeEnd(resizeEndEvent())
expect(onResize).toHaveBeenLastCalledWith([
{ storageKey: 'test-onresize-next', oldWidth: 320, newWidth: 480 }
])
})
})

View File

@@ -10,6 +10,12 @@ interface PanelConfig {
storageKey: string
}
export interface PanelResizeChange {
storageKey: string
oldWidth: number | null
newWidth: number
}
/**
* Works around PrimeVue Splitter not properly initializing flexBasis
* when panels are conditionally rendered. Captures pixel widths on
@@ -21,9 +27,11 @@ interface PanelConfig {
*/
export function useStablePrimeVueSplitterSizer(
panels: PanelConfig[],
watchSources: WatchSource[]
watchSources: WatchSource[],
onResize?: (changes: PanelResizeChange[]) => void
) {
const storedWidths = panels.map((panel) => ({
storageKey: panel.storageKey,
ref: panel.ref,
width: useStorage<number | null>(panel.storageKey, null)
}))
@@ -45,10 +53,16 @@ export function useStablePrimeVueSplitterSizer(
}
function onResizeEnd(_event: SplitterResizeEndEvent) {
for (const { ref, width } of storedWidths) {
const changes: PanelResizeChange[] = []
for (const { storageKey, ref, width } of storedWidths) {
const el = resolveElement(ref)
if (el) width.value = el.offsetWidth
if (!el) continue
const oldWidth = width.value
const newWidth = el.offsetWidth
width.value = newWidth
changes.push({ storageKey, oldWidth, newWidth })
}
onResize?.(changes)
}
watch(

View File

@@ -0,0 +1,24 @@
import { describe, expect, it, vi } from 'vitest'
import { TelemetryRegistry } from './TelemetryRegistry'
import type { AppModePanelResizedMetadata, TelemetryProvider } from './types'
describe('TelemetryRegistry', () => {
it('forwards trackAppModePanelResized to registered providers', () => {
const registry = new TelemetryRegistry()
const trackAppModePanelResized = vi.fn()
const provider: TelemetryProvider = { trackAppModePanelResized }
registry.registerProvider(provider)
const metadata: AppModePanelResizedMetadata = {
panel: 'input',
direction: 'wider',
previous_width_px: 320,
new_width_px: 480,
sidebar_location: 'left'
}
registry.trackAppModePanelResized(metadata)
expect(trackAppModePanelResized).toHaveBeenCalledWith(metadata)
})
})

View File

@@ -1,6 +1,7 @@
import type { AuditLog } from '@/services/customerEventsService'
import type {
AppModePanelResizedMetadata,
AuthMetadata,
BeginCheckoutMetadata,
DefaultViewSetMetadata,
@@ -241,4 +242,8 @@ export class TelemetryRegistry implements TelemetryDispatcher {
trackPageView(pageName: string, properties?: PageViewMetadata): void {
this.dispatch((provider) => provider.trackPageView?.(pageName, properties))
}
trackAppModePanelResized(metadata: AppModePanelResizedMetadata): void {
this.dispatch((provider) => provider.trackAppModePanelResized?.(metadata))
}
}

View File

@@ -190,6 +190,25 @@ describe('PostHogTelemetryProvider', () => {
)
})
it('captures App Mode panel resize events with metadata', async () => {
const provider = createProvider()
await vi.dynamicImportSettled()
const metadata = {
panel: 'input',
direction: 'wider',
previous_width_px: 320,
new_width_px: 480,
sidebar_location: 'left'
} as const
provider.trackAppModePanelResized(metadata)
expect(hoisted.mockCapture).toHaveBeenCalledWith(
TelemetryEvents.APP_MODE_PANEL_RESIZED,
metadata
)
})
it('queues events before initialization and flushes after', async () => {
const provider = createProvider()

View File

@@ -10,6 +10,7 @@ import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
import type { RemoteConfig } from '@/platform/remoteConfig/types'
import type {
AppModePanelResizedMetadata,
AuthMetadata,
DefaultViewSetMetadata,
EnterLinearMetadata,
@@ -466,4 +467,8 @@ export class PostHogTelemetryProvider implements TelemetryProvider {
...properties
})
}
trackAppModePanelResized(metadata: AppModePanelResizedMetadata): void {
this.trackEvent(TelemetryEvents.APP_MODE_PANEL_RESIZED, metadata)
}
}

View File

@@ -389,6 +389,19 @@ export interface SubscriptionSuccessMetadata extends Record<string, unknown> {
ecommerce: EcommerceMetadata
}
/**
* App Mode panel resize (LinearView splitter) — UX signal for whether the
* default prompt panel is too small (users widening it to read prompts). Only
* emitted for genuine resizes, so the direction is always wider or narrower.
*/
export interface AppModePanelResizedMetadata {
panel: 'input' | 'preview'
direction: 'wider' | 'narrower'
previous_width_px: number
new_width_px: number
sidebar_location: 'left' | 'right'
}
/**
* Telemetry provider interface for individual providers.
* All methods are optional - providers only implement what they need.
@@ -473,6 +486,9 @@ export interface TelemetryProvider {
// Generic UI button click events
trackUiButtonClicked?(metadata: UiButtonClickMetadata): void
// App Mode UI events
trackAppModePanelResized?(metadata: AppModePanelResizedMetadata): void
// Page view tracking
trackPageView?(pageName: string, properties?: PageViewMetadata): void
}
@@ -561,6 +577,9 @@ export const TelemetryEvents = {
// Generic UI Button Click
UI_BUTTON_CLICKED: 'app:ui_button_clicked',
// App Mode UI
APP_MODE_PANEL_RESIZED: 'app:app_mode_panel_resized',
// Page View
PAGE_VIEW: 'app:page_view'
} as const
@@ -597,6 +616,7 @@ export type TelemetryEventProperties =
| TemplateFilterMetadata
| SettingChangedMetadata
| UiButtonClickMetadata
| AppModePanelResizedMetadata
| HelpCenterOpenedMetadata
| HelpResourceClickedMetadata
| HelpCenterClosedMetadata

View File

@@ -14,6 +14,7 @@ import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import TopbarSubscribeButton from '@/components/topbar/TopbarSubscribeButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { cn } from '@comfyorg/tailwind-utils'
import LinearControls from '@/renderer/extensions/linearMode/LinearControls.vue'
import LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue'
@@ -29,6 +30,10 @@ import {
SIDE_PANEL_SIZE
} from '@/constants/splitterConstants'
import { useAppModeStore } from '@/stores/appModeStore'
import {
LINEAR_VIEW_PANEL_STORAGE_KEY,
buildInputPanelResizeMetadata
} from './linearViewPanelTelemetry'
const settingStore = useSettingStore()
const workspaceStore = useWorkspaceStore()
@@ -78,12 +83,19 @@ const splitterKey = computed(() => {
const leftPanelRef = useTemplateRef<MaybeElement>('leftPanel')
const rightPanelRef = useTemplateRef<MaybeElement>('rightPanel')
const telemetry = useTelemetry()
const { onResizeEnd } = useStablePrimeVueSplitterSizer(
[
{ ref: leftPanelRef, storageKey: 'Comfy.LinearView.LeftPanelWidth' },
{ ref: rightPanelRef, storageKey: 'Comfy.LinearView.RightPanelWidth' }
{ ref: leftPanelRef, storageKey: LINEAR_VIEW_PANEL_STORAGE_KEY.left },
{ ref: rightPanelRef, storageKey: LINEAR_VIEW_PANEL_STORAGE_KEY.right }
],
[activeTab, splitterKey]
[activeTab, splitterKey],
(changes) => {
if (isBuilderMode.value) return
const metadata = buildInputPanelResizeMetadata(changes, sidebarOnLeft.value)
if (metadata) telemetry?.trackAppModePanelResized(metadata)
}
)
const TYPEFORM_WIDGET_ID = 'jmmzmlKw'

View File

@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest'
import type { PanelResizeChange } from '@/composables/useStablePrimeVueSplitterSizer'
import {
LINEAR_VIEW_PANEL_STORAGE_KEY,
buildInputPanelResizeMetadata
} from './linearViewPanelTelemetry'
function change(
storageKey: string,
oldWidth: number | null,
newWidth: number
): PanelResizeChange {
return { storageKey, oldWidth, newWidth }
}
describe('buildInputPanelResizeMetadata', () => {
it('reads the right panel when the sidebar is on the left', () => {
const changes = [
change(LINEAR_VIEW_PANEL_STORAGE_KEY.left, 200, 260),
change(LINEAR_VIEW_PANEL_STORAGE_KEY.right, 320, 480)
]
expect(buildInputPanelResizeMetadata(changes, true)).toEqual({
panel: 'input',
direction: 'wider',
previous_width_px: 320,
new_width_px: 480,
sidebar_location: 'left'
})
})
it('reads the left panel when the sidebar is on the right', () => {
const changes = [
change(LINEAR_VIEW_PANEL_STORAGE_KEY.left, 480, 320),
change(LINEAR_VIEW_PANEL_STORAGE_KEY.right, 200, 260)
]
expect(buildInputPanelResizeMetadata(changes, false)).toEqual({
panel: 'input',
direction: 'narrower',
previous_width_px: 480,
new_width_px: 320,
sidebar_location: 'right'
})
})
it('returns null when only the sidebar panel changed', () => {
const changes = [change(LINEAR_VIEW_PANEL_STORAGE_KEY.left, 200, 260)]
expect(buildInputPanelResizeMetadata(changes, true)).toBeNull()
})
it('returns null when the input panel width did not change', () => {
const changes = [change(LINEAR_VIEW_PANEL_STORAGE_KEY.right, 400, 400)]
expect(buildInputPanelResizeMetadata(changes, true)).toBeNull()
})
it('returns null on the first measurement with no stored width', () => {
const changes = [change(LINEAR_VIEW_PANEL_STORAGE_KEY.right, null, 400)]
expect(buildInputPanelResizeMetadata(changes, true)).toBeNull()
})
})

View File

@@ -0,0 +1,43 @@
import type { PanelResizeChange } from '@/composables/useStablePrimeVueSplitterSizer'
import type { AppModePanelResizedMetadata } from '@/platform/telemetry/types'
export const LINEAR_VIEW_PANEL_STORAGE_KEY = {
left: 'Comfy.LinearView.LeftPanelWidth',
right: 'Comfy.LinearView.RightPanelWidth'
} as const
/**
* Builds the telemetry payload for an App Mode input-panel resize, or null when
* the resize should not be reported.
*
* The input/prompt panel renders opposite the sidebar, so a left sidebar puts
* the input on the right and vice-versa. We return null unless this resize
* actually changed the input panel's width: the splitter re-measures every
* panel on resize-end, so dragging an unrelated gutter (or the very first
* measurement, with no stored width) reports the input panel unchanged and must
* not emit a spurious event.
*/
export function buildInputPanelResizeMetadata(
changes: PanelResizeChange[],
sidebarOnLeft: boolean
): AppModePanelResizedMetadata | null {
const inputPanelKey = sidebarOnLeft
? LINEAR_VIEW_PANEL_STORAGE_KEY.right
: LINEAR_VIEW_PANEL_STORAGE_KEY.left
const inputChange = changes.find((c) => c.storageKey === inputPanelKey)
if (
!inputChange ||
inputChange.oldWidth === null ||
inputChange.oldWidth === inputChange.newWidth
) {
return null
}
return {
panel: 'input',
direction:
inputChange.newWidth > inputChange.oldWidth ? 'wider' : 'narrower',
previous_width_px: inputChange.oldWidth,
new_width_px: inputChange.newWidth,
sidebar_location: sidebarOnLeft ? 'left' : 'right'
}
}