From 327bbfa96a14cb5df9e173309657dde12502905c Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 27 Mar 2026 15:29:28 +0800 Subject: [PATCH 1/4] refactor: use Base UI popover anchor for skill file picker --- .../plugins/file-picker-block.tsx | 66 +++++++------------ 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx index 354fcba01e..dc07bef169 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-block.tsx @@ -1,21 +1,17 @@ import type { LexicalNode } from 'lexical' -import type { Dispatch, SetStateAction } from 'react' -import { - flip, - offset, - shift, - useFloating, -} from '@floating-ui/react' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { LexicalTypeaheadMenuPlugin, MenuOption } from '@lexical/react/LexicalTypeaheadMenuPlugin' import { $insertNodes, } from 'lexical' import * as React from 'react' -import { useCallback, useLayoutEffect, useMemo } from 'react' -import ReactDOM from 'react-dom' +import { useCallback, useMemo } from 'react' import { useBasicTypeaheadTriggerMatch } from '@/app/components/base/prompt-editor/hooks' import { $splitNodeContainingQuery } from '@/app/components/base/prompt-editor/utils' +import { + Popover, + PopoverContent, +} from '@/app/components/base/ui/popover' import { FilePickerPanel } from './file-picker-panel' import { $createFileReferenceNode } from './file-reference-block/node' @@ -25,29 +21,8 @@ class FilePickerMenuOption extends MenuOption { } } -type ReferenceSyncProps = { - anchor: HTMLElement | null - setReference: Dispatch> | ((node: HTMLElement | null) => void) -} - -const ReferenceSync = ({ anchor, setReference }: ReferenceSyncProps) => { - useLayoutEffect(() => { - setReference(anchor) - }, [anchor, setReference]) - - return null -} - const FilePickerBlock = () => { const [editor] = useLexicalComposerContext() - const { refs, floatingStyles, isPositioned } = useFloating({ - placement: 'bottom-start', - middleware: [ - offset(0), - shift({ padding: 8 }), - flip(), - ], - }) const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', { minLength: 0, maxLength: 0, @@ -76,16 +51,20 @@ const FilePickerBlock = () => { const closeMenu = () => selectOptionAndCleanUp(options[0]) - return ReactDOM.createPortal( - <> - -
{ + if (!open) + closeMenu() + }} + > + { @@ -93,11 +72,10 @@ const FilePickerBlock = () => { closeMenu() }} /> -
- , - anchorElementRef.current, + + ) - }, [floatingStyles, insertFileReference, isPositioned, options, refs]) + }, [insertFileReference, options]) return ( Date: Fri, 27 Mar 2026 15:51:30 +0800 Subject: [PATCH 2/4] fix(web): restore skill tab unsaved close dialog styling --- .../workflow/skill/skill-body/tabs/file-tab-item.spec.tsx | 2 +- .../workflow/skill/skill-body/tabs/file-tab-item.tsx | 2 +- .../workflow/skill/skill-body/tabs/file-tabs.spec.tsx | 4 +++- .../components/workflow/skill/skill-body/tabs/file-tabs.tsx | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/web/app/components/workflow/skill/skill-body/tabs/file-tab-item.spec.tsx b/web/app/components/workflow/skill/skill-body/tabs/file-tab-item.spec.tsx index 078a99f5b1..4603103343 100644 --- a/web/app/components/workflow/skill/skill-body/tabs/file-tab-item.spec.tsx +++ b/web/app/components/workflow/skill/skill-body/tabs/file-tab-item.spec.tsx @@ -46,7 +46,7 @@ describe('FileTabItem', () => { render() - expect(screen.getByText('readme.md')).toHaveClass('italic') + expect(screen.getByText('readme.md')).toHaveClass('italic', 'pr-[0.5px]') }) }) diff --git a/web/app/components/workflow/skill/skill-body/tabs/file-tab-item.tsx b/web/app/components/workflow/skill/skill-body/tabs/file-tab-item.tsx index 4d61747f0e..0abadfabf2 100644 --- a/web/app/components/workflow/skill/skill-body/tabs/file-tab-item.tsx +++ b/web/app/components/workflow/skill/skill-body/tabs/file-tab-item.tsx @@ -74,7 +74,7 @@ const FileTabItem = ({ { render() fireEvent.click(screen.getByRole('button', { name: /common\.operation\.close/i })) - fireEvent.click(screen.getByRole('button', { name: /workflow\.skillSidebar\.unsavedChanges\.confirmClose/i })) + const confirmButton = screen.getByRole('button', { name: /workflow\.skillSidebar\.unsavedChanges\.confirmClose/i }) + expect(confirmButton.className).toContain('btn-destructive') + fireEvent.click(confirmButton) expect(mocks.closeTab).toHaveBeenCalledTimes(1) expect(mocks.closeTab).toHaveBeenCalledWith('file-1') diff --git a/web/app/components/workflow/skill/skill-body/tabs/file-tabs.tsx b/web/app/components/workflow/skill/skill-body/tabs/file-tabs.tsx index 956fabf612..a39de627b3 100644 --- a/web/app/components/workflow/skill/skill-body/tabs/file-tabs.tsx +++ b/web/app/components/workflow/skill/skill-body/tabs/file-tabs.tsx @@ -134,7 +134,7 @@ const FileTabs = () => { {t('operation.cancel', { ns: 'common' })} - + {t('skillSidebar.unsavedChanges.confirmClose')} From 976fa30664996e6c9fff6a7740043d9b5e12e607 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Fri, 27 Mar 2026 15:55:48 +0800 Subject: [PATCH 3/4] chore: try to track why node missing --- .../core/collaboration-manager.ts | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/web/app/components/workflow/collaboration/core/collaboration-manager.ts b/web/app/components/workflow/collaboration/core/collaboration-manager.ts index 47ee511a2d..539bde7413 100644 --- a/web/app/components/workflow/collaboration/core/collaboration-manager.ts +++ b/web/app/components/workflow/collaboration/core/collaboration-manager.ts @@ -76,7 +76,32 @@ type GraphImportLogEntry = { } } +type SetNodesAnomalyReason = 'node_count_decrease' | 'start_removed' + +type SetNodesAnomalyLogEntry = { + timestamp: number + appId: string | null + reasons: SetNodesAnomalyReason[] + oldCount: number + newCount: number + removedNodeIds: string[] + oldStartNodeIds: string[] + newStartNodeIds: string[] + oldNodeIds: string[] + newNodeIds: string[] + visibilityState: DocumentVisibilityState | 'unknown' + meta: { + leaderId: string | null + isLeader: boolean + graphViewActive: boolean | null + pendingInitialSync: boolean + isConnected: boolean + } + stack: string +} + const GRAPH_IMPORT_LOG_LIMIT = 20 +const SET_NODES_ANOMALY_LOG_LIMIT = 100 const toLoroValue = (value: unknown): Value => cloneDeep(value) as Value const toLoroRecord = (value: unknown): Record => cloneDeep(value) as Record @@ -101,6 +126,7 @@ export class CollaborationManager { private pendingGraphImportEmit = false private graphViewActive: boolean | null = null private graphImportLogs: GraphImportLogEntry[] = [] + private setNodesAnomalyLogs: SetNodesAnomalyLogEntry[] = [] private pendingImportLog: { timestamp: number sources: Set<'nodes' | 'edges'> @@ -402,6 +428,7 @@ export class CollaborationManager { if (this.isUndoRedoInProgress) return + this.captureSetNodesAnomaly(oldNodes, newNodes) this.syncNodes(oldNodes, newNodes) this.doc.commit() } @@ -1075,6 +1102,7 @@ export class CollaborationManager { clearGraphImportLog(): void { this.graphImportLogs = [] + this.setNodesAnomalyLogs = [] this.pendingImportLog = null } @@ -1084,8 +1112,10 @@ export class CollaborationManager { appId: this.currentAppId, generatedAt: new Date().toISOString(), entries: this.graphImportLogs, + setNodesAnomalies: this.setNodesAnomalyLogs, summary: { logCount: this.graphImportLogs.length, + setNodesAnomalyCount: this.setNodesAnomalyLogs.length, leaderId: this.leaderId, isLeader: this.isLeader, graphViewActive: this.graphViewActive, @@ -1116,6 +1146,55 @@ export class CollaborationManager { URL.revokeObjectURL(url) } + private captureSetNodesAnomaly(oldNodes: Node[], newNodes: Node[]): void { + const oldNodeIds = oldNodes.map(node => node.id) + const newNodeIds = newNodes.map(node => node.id) + const newNodeIdSet = new Set(newNodeIds) + const removedNodeIds = oldNodeIds.filter(nodeId => !newNodeIdSet.has(nodeId)) + + const oldStartNodeIds = oldNodes + .filter(node => (node.data as CommonNodeType | undefined)?.type === 'start') + .map(node => node.id) + const newStartNodeIds = newNodes + .filter(node => (node.data as CommonNodeType | undefined)?.type === 'start') + .map(node => node.id) + + const reasons: SetNodesAnomalyReason[] = [] + if (newNodes.length < oldNodes.length) + reasons.push('node_count_decrease') + if (oldStartNodeIds.length > 0 && newStartNodeIds.length === 0) + reasons.push('start_removed') + + if (!reasons.length) + return + + const stack = new Error('setNodes anomaly').stack || '' + const entry: SetNodesAnomalyLogEntry = { + timestamp: Date.now(), + appId: this.currentAppId, + reasons, + oldCount: oldNodes.length, + newCount: newNodes.length, + removedNodeIds, + oldStartNodeIds, + newStartNodeIds, + oldNodeIds, + newNodeIds, + visibilityState: typeof document === 'undefined' ? 'unknown' : document.visibilityState, + meta: { + leaderId: this.leaderId, + isLeader: this.isLeader, + graphViewActive: this.graphViewActive, + pendingInitialSync: this.pendingInitialSync, + isConnected: this.isConnected(), + }, + stack, + } + this.setNodesAnomalyLogs.push(entry) + if (this.setNodesAnomalyLogs.length > SET_NODES_ANOMALY_LOG_LIMIT) + this.setNodesAnomalyLogs.splice(0, this.setNodesAnomalyLogs.length - SET_NODES_ANOMALY_LOG_LIMIT) + } + private snapshotReactFlowGraph(): { nodes: Node[], edges: Edge[] } { if (!this.reactFlowStore) { return { From ee093a21c83770a47193d13869ca2dfd0c0263d8 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 27 Mar 2026 16:01:40 +0800 Subject: [PATCH 4/4] fix: click file missing --- .../__tests__/component.spec.tsx | 4 +++- .../plugins/file-reference-block/component.tsx | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/__tests__/component.spec.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/__tests__/component.spec.tsx index b363789d4d..ed4063f26b 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/__tests__/component.spec.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/__tests__/component.spec.tsx @@ -77,7 +77,9 @@ describe('FileReferenceBlock', () => { ) await act(async () => { - fireEvent.mouseDown(screen.getByText('contract.pdf')) + const target = screen.getByText('contract.pdf') + fireEvent.mouseDown(target) + fireEvent.click(target) }) expect(await screen.findByText('workflow.skillEditor.referenceFiles')).toBeInTheDocument() diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/component.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/component.tsx index 973dfe9134..20e26f66d6 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/component.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/component.tsx @@ -163,7 +163,21 @@ const FileReferenceBlock = ({ nodeKey, resourceId }: FileReferenceBlockProps) => const fileBlock = ( { + if (!nextOpen && eventDetails.reason === 'focus-out') + return + + if ( + !nextOpen + && eventDetails.reason === 'outside-press' + && eventDetails.event.target instanceof Node + && ref.current?.contains(eventDetails.event.target) + ) { + return + } + + setOpen(nextOpen) + }} >