feat: code/txt editor sync

This commit is contained in:
hjlarry
2026-01-29 09:16:52 +08:00
parent 26dd6c128c
commit 90bb7bf2f3
2 changed files with 106 additions and 5 deletions

View File

@@ -0,0 +1,91 @@
import { useCallback, useEffect, useRef } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { skillCollaborationManager } from './skill-collaboration-manager'
type UseSkillCodeCollaborationProps = {
appId: string
fileId: string | null
enabled: boolean
initialContent: string
baselineContent: string
onLocalChange: (value: string) => void
onLeaderSync: () => void
}
export const useSkillCodeCollaboration = ({
appId,
fileId,
enabled,
initialContent,
baselineContent,
onLocalChange,
onLeaderSync,
}: UseSkillCodeCollaborationProps) => {
const storeApi = useWorkflowStore()
const suppressNextChangeRef = useRef<string | null>(null)
// Keep the latest server baseline to avoid marking the editor dirty on initial sync.
const baselineContentRef = useRef(baselineContent)
useEffect(() => {
suppressNextChangeRef.current = null
}, [fileId])
useEffect(() => {
baselineContentRef.current = baselineContent
}, [baselineContent])
useEffect(() => {
if (!enabled || !fileId)
return
skillCollaborationManager.openFile(appId, fileId, initialContent)
skillCollaborationManager.setActiveFile(appId, fileId, true)
const unsubscribe = skillCollaborationManager.subscribe(fileId, (nextText) => {
suppressNextChangeRef.current = nextText
const state = storeApi.getState()
if (nextText === baselineContentRef.current) {
state.clearDraftContent(fileId)
}
else {
state.setDraftContent(fileId, nextText)
state.pinTab(fileId)
}
})
const unsubscribeSync = skillCollaborationManager.onSyncRequest(fileId, onLeaderSync)
return () => {
unsubscribe()
unsubscribeSync()
skillCollaborationManager.setActiveFile(appId, fileId, false)
skillCollaborationManager.closeFile(fileId)
}
}, [appId, enabled, fileId, initialContent, onLeaderSync, storeApi])
const handleCollaborativeChange = useCallback((value: string | undefined) => {
const nextValue = value ?? ''
if (!fileId) {
onLocalChange(nextValue)
return
}
if (!enabled) {
onLocalChange(nextValue)
return
}
if (suppressNextChangeRef.current === nextValue) {
suppressNextChangeRef.current = null
return
}
skillCollaborationManager.updateText(fileId, nextValue)
onLocalChange(nextValue)
}, [enabled, fileId, onLocalChange])
return {
handleCollaborativeChange,
isLeader: fileId ? skillCollaborationManager.isLeader(fileId) : false,
}
}

View File

@@ -12,6 +12,7 @@ import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import { basePath } from '@/utils/var'
import { useSkillCodeCollaboration } from '../collaboration/skills/use-skill-code-collaboration'
import { useSkillMarkdownCollaboration } from '../collaboration/skills/use-skill-markdown-collaboration'
import { START_TAB_ID } from './constants'
import CodeFileEditor from './editor/code-file-editor'
@@ -62,7 +63,7 @@ const FileContentPanel = () => {
const originalContent = fileContent?.content ?? ''
const currentContent = draftContent !== undefined ? draftContent : originalContent
const initialContentRegistryRef = useRef<Map<string, string>>(new Map())
const canInitCollaboration = Boolean(appId && fileTabId && isMarkdown && isEditable && !isLoading && !error)
const canInitCollaboration = Boolean(appId && fileTabId && isEditable && !isLoading && !error)
if (canInitCollaboration && fileTabId && !initialContentRegistryRef.current.has(fileTabId))
initialContentRegistryRef.current.set(fileTabId, currentContent)
@@ -155,10 +156,19 @@ const FileContentPanel = () => {
const language = currentFileNode ? getFileLanguage(currentFileNode.name) : 'plaintext'
const theme = appTheme === Theme.light ? 'light' : 'vs-dark'
const { handleCollaborativeChange } = useSkillMarkdownCollaboration({
const { handleCollaborativeChange: handleMarkdownCollaborativeChange } = useSkillMarkdownCollaboration({
appId,
fileId: fileTabId,
enabled: canInitCollaboration,
enabled: canInitCollaboration && isMarkdown,
initialContent: initialCollaborativeContent,
baselineContent: originalContent,
onLocalChange: handleEditorChange,
onLeaderSync: handleLeaderSync,
})
const { handleCollaborativeChange: handleCodeCollaborativeChange } = useSkillCodeCollaboration({
appId,
fileId: fileTabId,
enabled: canInitCollaboration && isCodeOrText,
initialContent: initialCollaborativeContent,
baselineContent: originalContent,
onLocalChange: handleEditorChange,
@@ -210,7 +220,7 @@ const FileContentPanel = () => {
key={fileTabId}
instanceId={fileTabId || undefined}
value={currentContent}
onChange={handleCollaborativeChange}
onChange={handleMarkdownCollaborativeChange}
collaborationEnabled={canInitCollaboration}
/>
)
@@ -222,7 +232,7 @@ const FileContentPanel = () => {
language={language}
theme={isMounted ? theme : 'default-theme'}
value={currentContent}
onChange={handleEditorChange}
onChange={handleCodeCollaborativeChange}
onMount={handleEditorDidMount}
/>
)