diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx index 9107fd07..ec965e77 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx @@ -1,17 +1,5 @@ import { useRef } from 'react'; -import { - Loader2, - Folder, - ClipboardPaste, - FolderPlus, - FolderUp, - LayoutGrid, - Upload, - ClipboardCopy, - MessageSquare, - Download, - Mic, -} from 'lucide-react'; +import { Loader2, Folder, ClipboardPaste, FolderPlus, FolderUp, LayoutGrid, Upload, ClipboardCopy, MessageSquare, Download, Mic } from 'lucide-react'; import { getIcon } from 'material-file-icons'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import type { UseFileBrowserAppType } from '../useFileBrowserApp'; @@ -104,7 +92,9 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps })} ) : searchResults ? ( -
No results found
+
+ No results found +
) : null} ) : ( diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Toolbar/SelectionActions.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Toolbar/SelectionActions.tsx index b6308148..ac6415c9 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Toolbar/SelectionActions.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Toolbar/SelectionActions.tsx @@ -6,16 +6,8 @@ type SelectionActionsProps = { }; export const SelectionActions = ({ fileBrowserManager }: SelectionActionsProps) => { - const { - selected, - clipboard, - handleCut, - handleCopy, - handlePaste, - handleDownloadSelected, - handleDeleteSelected, - setSelected, - } = fileBrowserManager; + const { selected, clipboard, handleCut, handleCopy, handlePaste, handleDownloadSelected, handleDeleteSelected, setSelected } = + fileBrowserManager; return ( <> diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/usePipelineRunner.ts b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/usePipelineRunner.ts index f2552d17..63e4244d 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/usePipelineRunner.ts +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/usePipelineRunner.ts @@ -33,61 +33,19 @@ type ParallelStep = { type ServerMessage = | { jobId: string; type: 'pipeline:init'; steps: StepDef[] } - | { - jobId: string; - type: 'step:start'; - stepIndex: number; - taskName: string; - iteration?: { current: number; total: number; label: string }; - } - | { - jobId: string; - type: 'step:complete'; - stepIndex: number; - cost?: { inputTokens: number; outputTokens: number; totalUSD: number }; - } + | { jobId: string; type: 'step:start'; stepIndex: number; taskName: string; iteration?: { current: number; total: number; label: string } } + | { jobId: string; type: 'step:complete'; stepIndex: number; cost?: { inputTokens: number; outputTokens: number; totalUSD: number } } | { jobId: string; type: 'step:skip'; stepIndex: number; label: string; reason: string } - | { - jobId: string; - type: 'step:parallel'; - stepIndex: number; - taskName: string; - iterations: string[]; - concurrency: number; - } + | { jobId: string; type: 'step:parallel'; stepIndex: number; taskName: string; iterations: string[]; concurrency: number } | { jobId: string; type: 'step:waiting'; stepIndex: number; iterationLabel?: string; elapsed: number } | { jobId: string; type: 'iteration:start'; stepIndex: number; label: string } - | { - jobId: string; - type: 'iteration:complete'; - stepIndex: number; - label: string; - cost?: { inputTokens: number; outputTokens: number; totalUSD: number }; - } + | { jobId: string; type: 'iteration:complete'; stepIndex: number; label: string; cost?: { inputTokens: number; outputTokens: number; totalUSD: number } } | { jobId: string; type: 'iteration:error'; stepIndex: number; label: string; error: string } | { jobId: string; type: 'assistant:delta'; text: string; iterationLabel?: string } | { jobId: string; type: 'assistant:text'; text: string; iterationLabel?: string } - | { - jobId: string; - type: 'tool:start'; - toolCallId: string; - toolName: string; - toolInput: Record; - iterationLabel?: string; - } - | { - jobId: string; - type: 'tool:result'; - toolCallId: string; - output: string; - isError: boolean; - iterationLabel?: string; - } - | { - jobId: string; - type: 'pipeline:complete'; - totalCost: { inputTokens: number; outputTokens: number; totalUSD: number }; - } + | { jobId: string; type: 'tool:start'; toolCallId: string; toolName: string; toolInput: Record; iterationLabel?: string } + | { jobId: string; type: 'tool:result'; toolCallId: string; output: string; isError: boolean; iterationLabel?: string } + | { jobId: string; type: 'pipeline:complete'; totalCost: { inputTokens: number; outputTokens: number; totalUSD: number } } | { jobId: string; type: 'error'; message: string } | { jobId: string; type: 'stopped' } | { type: 'job:created'; jobId: string } @@ -103,18 +61,12 @@ export function usePipelineRunner() { const [parallelStep, setParallelStep] = useState(null); const [messages, setMessages] = useState([]); const [streamingText, setStreamingText] = useState(''); - const [totalCost, setTotalCost] = useState<{ inputTokens: number; outputTokens: number; totalUSD: number } | null>( - null, - ); + const [totalCost, setTotalCost] = useState<{ inputTokens: number; outputTokens: number; totalUSD: number } | null>(null); const [runningCost, setRunningCost] = useState({ inputTokens: 0, outputTokens: 0, totalUSD: 0 }); const [hasError, setHasError] = useState(false); const [skippedItems, setSkippedItems] = useState>([]); const [elapsed, setElapsed] = useState(0); - const [waitingStatus, setWaitingStatus] = useState<{ - stepIndex: number; - elapsed: number; - iterationLabel?: string; - } | null>(null); + const [waitingStatus, setWaitingStatus] = useState<{ stepIndex: number; elapsed: number; iterationLabel?: string } | null>(null); const wsRef = useRef(null); const streamBufferRef = useRef(''); const startTimeRef = useRef(0); @@ -132,10 +84,7 @@ export function usePipelineRunner() { }, []); const stopTimer = useCallback(() => { - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } + if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } }, []); const addCost = useCallback((cost: { inputTokens: number; outputTokens: number; totalUSD: number }) => { @@ -146,184 +95,178 @@ export function usePipelineRunner() { })); }, []); - const handleEvent = useCallback( - (msg: ServerMessage) => { - // Filter events by jobId (ignore events from other jobs) - if ('jobId' in msg && msg.jobId && jobIdRef.current && msg.jobId !== jobIdRef.current) return; + const handleEvent = useCallback((msg: ServerMessage) => { + // Filter events by jobId (ignore events from other jobs) + if ('jobId' in msg && msg.jobId && jobIdRef.current && msg.jobId !== jobIdRef.current) return; - switch (msg.type) { - case 'job:created': - jobIdRef.current = msg.jobId; - setJobId(msg.jobId); - break; + switch (msg.type) { + case 'job:created': + jobIdRef.current = msg.jobId; + setJobId(msg.jobId); + break; - case 'job:state': - // Reconnection to a completed/failed job - if ( - msg.status === 'completed' || - msg.status === 'failed' || - msg.status === 'stopped' || - msg.status === 'interrupted' - ) { - setPhase('done'); - if (msg.cost) setTotalCost(msg.cost as { inputTokens: number; outputTokens: number; totalUSD: number }); - if (msg.status === 'failed' || msg.status === 'interrupted') setHasError(true); - stopTimer(); - } - break; - - case 'pipeline:init': - setSteps(msg.steps); - break; - - case 'step:start': - flushStream(); - setMessages([]); - setParallelStep(null); - setWaitingStatus(null); - inParallelRef.current = false; - setCurrentStep({ - taskName: msg.taskName, - iteration: msg.iteration, - status: 'running', - }); - break; - - case 'step:complete': - flushStream(); - setWaitingStatus(null); - setCurrentStep((prev) => (prev ? { ...prev, status: 'complete', cost: msg.cost } : null)); - if (msg.cost) addCost(msg.cost); - break; - - case 'step:skip': - setSkippedItems((prev) => [...prev, { label: msg.label, reason: msg.reason }]); - break; - - case 'step:waiting': - setWaitingStatus({ stepIndex: msg.stepIndex, elapsed: msg.elapsed, iterationLabel: msg.iterationLabel }); - break; - - case 'step:parallel': - flushStream(); - setMessages([]); - setCurrentStep(null); - inParallelRef.current = true; - setParallelStep({ - stepIndex: msg.stepIndex, - taskName: msg.taskName, - concurrency: msg.concurrency, - iterations: msg.iterations.map((label) => ({ label, status: 'pending' })), - }); - break; - - case 'iteration:start': - setParallelStep((prev) => { - if (!prev) return prev; - return { - ...prev, - iterations: prev.iterations.map((it) => (it.label === msg.label ? { ...it, status: 'running' } : it)), - }; - }); - break; - - case 'iteration:complete': - setParallelStep((prev) => { - if (!prev) return prev; - return { - ...prev, - iterations: prev.iterations.map((it) => - it.label === msg.label ? { ...it, status: 'complete', cost: msg.cost } : it, - ), - }; - }); - if (msg.cost) addCost(msg.cost); - break; - - case 'iteration:error': - setParallelStep((prev) => { - if (!prev) return prev; - return { - ...prev, - iterations: prev.iterations.map((it) => - it.label === msg.label ? { ...it, status: 'error', error: msg.error } : it, - ), - }; - }); - break; - - case 'assistant:delta': - // Skip messages from parallel sub-agents (shown in iteration grid instead) - if (inParallelRef.current && msg.iterationLabel) break; - setWaitingStatus(null); - streamBufferRef.current += msg.text; - setStreamingText(streamBufferRef.current); - break; - - case 'assistant:text': { - if (inParallelRef.current && msg.iterationLabel) break; - const text = msg.text || streamBufferRef.current; - if (text) { - setMessages((prev) => [...prev, { role: 'assistant', id: randomId(), text }]); - } - streamBufferRef.current = ''; - setStreamingText(''); - break; + case 'job:state': + // Reconnection to a completed/failed job + if (msg.status === 'completed' || msg.status === 'failed' || msg.status === 'stopped' || msg.status === 'interrupted') { + setPhase('done'); + if (msg.cost) setTotalCost(msg.cost as { inputTokens: number; outputTokens: number; totalUSD: number }); + if (msg.status === 'failed' || msg.status === 'interrupted') setHasError(true); + stopTimer(); } + break; - case 'tool:start': - if (inParallelRef.current && msg.iterationLabel) break; - setWaitingStatus(null); - flushStream(); - setMessages((prev) => [ + case 'pipeline:init': + setSteps(msg.steps); + break; + + case 'step:start': + flushStream(); + setMessages([]); + setParallelStep(null); + setWaitingStatus(null); + inParallelRef.current = false; + setCurrentStep({ + taskName: msg.taskName, + iteration: msg.iteration, + status: 'running', + }); + break; + + case 'step:complete': + flushStream(); + setWaitingStatus(null); + setCurrentStep((prev) => prev ? { ...prev, status: 'complete', cost: msg.cost } : null); + if (msg.cost) addCost(msg.cost); + break; + + case 'step:skip': + setSkippedItems((prev) => [...prev, { label: msg.label, reason: msg.reason }]); + break; + + case 'step:waiting': + setWaitingStatus({ stepIndex: msg.stepIndex, elapsed: msg.elapsed, iterationLabel: msg.iterationLabel }); + break; + + case 'step:parallel': + flushStream(); + setMessages([]); + setCurrentStep(null); + inParallelRef.current = true; + setParallelStep({ + stepIndex: msg.stepIndex, + taskName: msg.taskName, + concurrency: msg.concurrency, + iterations: msg.iterations.map((label) => ({ label, status: 'pending' })), + }); + break; + + case 'iteration:start': + setParallelStep((prev) => { + if (!prev) return prev; + return { ...prev, - { - role: 'tool' as const, - id: randomId(), - toolCallId: msg.toolCallId, - toolName: msg.toolName, - toolInput: msg.toolInput, - output: undefined, - isError: false, - }, - ]); - break; - - case 'tool:result': - if (inParallelRef.current && msg.iterationLabel) break; - setMessages((prev) => - prev.map((m) => - m.role === 'tool' && 'toolCallId' in m && m.toolCallId === msg.toolCallId - ? { ...m, output: msg.output, isError: msg.isError } - : m, + iterations: prev.iterations.map((it) => + it.label === msg.label ? { ...it, status: 'running' } : it, ), - ); - break; + }; + }); + break; - case 'pipeline:complete': - flushStream(); - setTotalCost(msg.totalCost); - setPhase('done'); - stopTimer(); - break; + case 'iteration:complete': + setParallelStep((prev) => { + if (!prev) return prev; + return { + ...prev, + iterations: prev.iterations.map((it) => + it.label === msg.label ? { ...it, status: 'complete', cost: msg.cost } : it, + ), + }; + }); + if (msg.cost) addCost(msg.cost); + break; - case 'error': - flushStream(); - setMessages((prev) => [...prev, { role: 'error' as const, id: randomId(), text: msg.message }]); - setHasError(true); - setPhase('done'); - stopTimer(); - break; + case 'iteration:error': + setParallelStep((prev) => { + if (!prev) return prev; + return { + ...prev, + iterations: prev.iterations.map((it) => + it.label === msg.label ? { ...it, status: 'error', error: msg.error } : it, + ), + }; + }); + break; - case 'stopped': - flushStream(); - setPhase('done'); - stopTimer(); - break; + case 'assistant:delta': + // Skip messages from parallel sub-agents (shown in iteration grid instead) + if (inParallelRef.current && msg.iterationLabel) break; + setWaitingStatus(null); + streamBufferRef.current += msg.text; + setStreamingText(streamBufferRef.current); + break; + + case 'assistant:text': { + if (inParallelRef.current && msg.iterationLabel) break; + const text = msg.text || streamBufferRef.current; + if (text) { + setMessages((prev) => [...prev, { role: 'assistant', id: randomId(), text }]); + } + streamBufferRef.current = ''; + setStreamingText(''); + break; } - }, - [flushStream, stopTimer, addCost], - ); + + case 'tool:start': + if (inParallelRef.current && msg.iterationLabel) break; + setWaitingStatus(null); + flushStream(); + setMessages((prev) => [ + ...prev, + { + role: 'tool' as const, + id: randomId(), + toolCallId: msg.toolCallId, + toolName: msg.toolName, + toolInput: msg.toolInput, + output: undefined, + isError: false, + }, + ]); + break; + + case 'tool:result': + if (inParallelRef.current && msg.iterationLabel) break; + setMessages((prev) => + prev.map((m) => + m.role === 'tool' && 'toolCallId' in m && m.toolCallId === msg.toolCallId + ? { ...m, output: msg.output, isError: msg.isError } + : m, + ), + ); + break; + + case 'pipeline:complete': + flushStream(); + setTotalCost(msg.totalCost); + setPhase('done'); + stopTimer(); + break; + + case 'error': + flushStream(); + setMessages((prev) => [...prev, { role: 'error' as const, id: randomId(), text: msg.message }]); + setHasError(true); + setPhase('done'); + stopTimer(); + break; + + case 'stopped': + flushStream(); + setPhase('done'); + stopTimer(); + break; + } + }, [flushStream, stopTimer, addCost]); // The socket below is opened once and must stay open, so its listener is registered once too — and would // hold the first render's `handleEvent` forever. That closure carries `flushStream`'s captured @@ -365,35 +308,32 @@ export function usePipelineRunner() { }; }, []); - const run = useCallback( - (taskDirName: string, inputs: Record, cwd?: string, model?: string, startAt?: number) => { - if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; + const run = useCallback((taskDirName: string, inputs: Record, cwd?: string, model?: string, startAt?: number) => { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; - setPhase('running'); - setMessages([]); - setStreamingText(''); - setTotalCost(null); - setRunningCost({ inputTokens: 0, outputTokens: 0, totalUSD: 0 }); - setHasError(false); - setSkippedItems([]); - setCurrentStep(null); - setParallelStep(null); - setWaitingStatus(null); - setElapsed(0); - setJobId(null); - jobIdRef.current = null; - streamBufferRef.current = ''; + setPhase('running'); + setMessages([]); + setStreamingText(''); + setTotalCost(null); + setRunningCost({ inputTokens: 0, outputTokens: 0, totalUSD: 0 }); + setHasError(false); + setSkippedItems([]); + setCurrentStep(null); + setParallelStep(null); + setWaitingStatus(null); + setElapsed(0); + setJobId(null); + jobIdRef.current = null; + streamBufferRef.current = ''; - startTimeRef.current = Date.now(); - if (timerRef.current) clearInterval(timerRef.current); - timerRef.current = setInterval(() => { - setElapsed(Math.floor((Date.now() - startTimeRef.current) / 1000)); - }, 1000); + startTimeRef.current = Date.now(); + if (timerRef.current) clearInterval(timerRef.current); + timerRef.current = setInterval(() => { + setElapsed(Math.floor((Date.now() - startTimeRef.current) / 1000)); + }, 1000); - wsRef.current.send(JSON.stringify({ type: 'run', taskDirName, inputs, cwd, model, startAt })); - }, - [], - ); + wsRef.current.send(JSON.stringify({ type: 'run', taskDirName, inputs, cwd, model, startAt })); + }, []); const stop = useCallback(() => { if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !jobIdRef.current) return; @@ -401,21 +341,7 @@ export function usePipelineRunner() { }, []); return { - phase, - isConnected, - jobId, - steps, - currentStep, - parallelStep, - messages, - streamingText, - totalCost, - runningCost, - hasError, - skippedItems, - elapsed, - waitingStatus, - run, - stop, + phase, isConnected, jobId, steps, currentStep, parallelStep, messages, streamingText, + totalCost, runningCost, hasError, skippedItems, elapsed, waitingStatus, run, stop, }; } diff --git a/src/workspaces/officerdev/src/hooks/useFileViewerPanels/Providers.tsx b/src/workspaces/officerdev/src/hooks/useFileViewerPanels/Providers.tsx index dc097f1d..3b652093 100644 --- a/src/workspaces/officerdev/src/hooks/useFileViewerPanels/Providers.tsx +++ b/src/workspaces/officerdev/src/hooks/useFileViewerPanels/Providers.tsx @@ -7,8 +7,7 @@ import { EmbeddableChat } from '../../apps/Chat/EmbeddableChat'; type SetSearchParams = ReturnType[1]; -const onReplaceView = - (setSearchParams: SetSearchParams) => +const onReplaceView = (setSearchParams: SetSearchParams) => (viewPath: string, viewRoot: string, ephemeralPath: string, ephemeralRoot: string) => { setSearchParams((prev) => { const next = new URLSearchParams(prev); @@ -37,12 +36,7 @@ export function ViewerProvider({ children }: { children: ReactNode }) { }; return ( - + {children} ); @@ -65,13 +59,7 @@ export function EphemeralProvider({ children }: { children: ReactNode }) { }; return ( - + {children} ); @@ -85,13 +73,7 @@ export function Ephemeral2Provider({ children }: { children: ReactNode }) { const fileName = ephemeral2Path.split('/').pop() ?? ''; return ( - + {children} ); @@ -103,14 +85,15 @@ export const ChatEphemeralBody = () => { const chatContext = searchParams.get('chatContext') ?? ''; const chatType = searchParams.get('chatType') as 'file' | 'folder' | null; - const cwdPath = chatType === 'file' ? chatContext.substring(0, chatContext.lastIndexOf('/')) || '/' : chatContext; + const cwdPath = chatType === 'file' + ? chatContext.substring(0, chatContext.lastIndexOf('/')) || '/' + : chatContext; const tag = chatType === 'file' ? 'file' : 'folder'; const path = chatContext.replace(/^\//, ''); - const message = - chatType === 'file' - ? `[${tag}: ${path}] Let's talk about this file` - : `[${tag}: ${path || '/'}] consider, for this session, this directory as your current working directory`; + const message = chatType === 'file' + ? `[${tag}: ${path}] Let's talk about this file` + : `[${tag}: ${path || '/'}] consider, for this session, this directory as your current working directory`; const handleMessageComplete = useCallback(() => { bumpFilesRefresh();