Files
kandev/apps/web/components/task/file-editor-panel.tsx
T
Carlos Florêncio f4d1dfd3a7 feat: better context files (#222)
feat: context files
2026-02-14 20:13:11 +00:00

121 lines
4.0 KiB
TypeScript

'use client';
import { memo, useEffect, useRef } from 'react';
import type { IDockviewPanelProps } from 'dockview-react';
import { PanelRoot, PanelBody } from './panel-primitives';
import { FileEditorContent } from './file-editor-content';
import { FileImageViewer } from './file-image-viewer';
import { FileBinaryViewer } from './file-binary-viewer';
import { useAppStore } from '@/components/state-provider';
import { useDockviewStore, type FileEditorState } from '@/lib/state/dockview-store';
import { useFileEditors } from '@/hooks/use-file-editors';
import { getFileCategory } from '@/lib/utils/file-types';
import { getWebSocketClient } from '@/lib/ws/connection';
import { requestFileContent } from '@/lib/ws/workspace-files';
import { calculateHash } from '@/lib/utils/file-diff';
export const FileEditorPanel = memo(function FileEditorPanel(
props: IDockviewPanelProps<{ path: string }>
) {
const path = props.params.path;
const fileState = useDockviewStore((s) => s.openFiles.get(path));
const setFileState = useDockviewStore((s) => s.setFileState);
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
const activeSession = useAppStore((state) =>
activeSessionId ? state.taskSessions.items[activeSessionId] ?? null : null
);
const { savingFiles, handleFileChange, saveFile, deleteFile } = useFileEditors();
// Self-load file content when state is missing (e.g. after hot reload clears the store)
const loadingRef = useRef(false);
useEffect(() => {
if (fileState || loadingRef.current || !activeSessionId) return;
loadingRef.current = true;
const client = getWebSocketClient();
if (!client) { loadingRef.current = false; return; }
requestFileContent(client, activeSessionId, path)
.then(async (response) => {
const hash = await calculateHash(response.content);
const name = path.split('/').pop() || path;
const state: FileEditorState = {
path,
name,
content: response.content,
originalContent: response.content,
originalHash: hash,
isDirty: false,
isBinary: response.is_binary,
};
setFileState(path, state);
})
.catch(() => { /* will stay on loading state */ })
.finally(() => { loadingRef.current = false; });
}, [fileState, activeSessionId, path, setFileState]);
if (!fileState) {
return (
<PanelRoot>
<PanelBody padding={false} scroll={false} className="flex items-center justify-center text-muted-foreground text-sm">
Loading file...
</PanelBody>
</PanelRoot>
);
}
const extCategory = getFileCategory(path);
const category = fileState.isBinary
? extCategory === 'image'
? 'image'
: 'binary'
: 'text';
if (category === 'image') {
return (
<PanelRoot>
<PanelBody padding={false} scroll={false}>
<FileImageViewer
path={path}
content={fileState.content}
worktreePath={activeSession?.worktree_path ?? undefined}
/>
</PanelBody>
</PanelRoot>
);
}
if (category === 'binary') {
return (
<PanelRoot>
<PanelBody padding={false} scroll={false}>
<FileBinaryViewer
path={path}
worktreePath={activeSession?.worktree_path ?? undefined}
/>
</PanelBody>
</PanelRoot>
);
}
return (
<PanelRoot>
<PanelBody padding={false} scroll={false}>
<FileEditorContent
path={path}
content={fileState.content}
originalContent={fileState.originalContent}
isDirty={fileState.isDirty}
isSaving={savingFiles.has(path)}
sessionId={activeSessionId || undefined}
worktreePath={activeSession?.worktree_path ?? undefined}
enableComments={!!activeSessionId}
onChange={(newContent) => handleFileChange(path, newContent)}
onSave={() => saveFile(path)}
onDelete={() => deleteFile(path)}
/>
</PanelBody>
</PanelRoot>
);
});