diff --git a/.gitignore b/.gitignore index d9643715..95112315 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,10 @@ AGENTS.md .envrc # Session store used for example-client.ts -.session-store.json \ No newline at end of file +.session-store.json + +# PostHog task artifacts (generated per-task) +.posthog/ + +# Test media files +sounds/ \ No newline at end of file diff --git a/apps/array/src/main/services/session-manager.ts b/apps/array/src/main/services/session-manager.ts index 9756c46f..3ade69fe 100644 --- a/apps/array/src/main/services/session-manager.ts +++ b/apps/array/src/main/services/session-manager.ts @@ -276,7 +276,7 @@ export class SessionManager { await connection.newSession({ cwd: repoPath, mcpServers, - _meta: { sessionId: taskRunId }, + _meta: { sessionId: taskRunId, taskId }, }); } diff --git a/apps/array/src/renderer/features/editor/components/PlanEditor.tsx b/apps/array/src/renderer/features/editor/components/PlanEditor.tsx index 55bb21bf..03c1e3a2 100644 --- a/apps/array/src/renderer/features/editor/components/PlanEditor.tsx +++ b/apps/array/src/renderer/features/editor/components/PlanEditor.tsx @@ -1,14 +1,48 @@ -import { RichTextEditor } from "@features/editor/components/RichTextEditor"; import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore"; import { FloppyDiskIcon } from "@phosphor-icons/react"; import { Box, Button, TextArea } from "@radix-ui/themes"; import { logger } from "@renderer/lib/logger"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; const log = logger.scope("plan-editor"); +// Hook to watch for external file changes +function usePlanFileWatcher( + repoPath: string | undefined, + taskId: string, + fileName: string, + onFileChanged: () => void, +) { + const onFileChangedRef = useRef(onFileChanged); + onFileChangedRef.current = onFileChanged; + + useEffect(() => { + if (!repoPath || !window.electronAPI?.onFileChanged) return; + + // Build the expected path for the plan file + const expectedPath = `${repoPath}/.posthog/${taskId}/${fileName}`; + + log.debug("Watching for changes to:", expectedPath); + + const unsubscribe = window.electronAPI.onFileChanged( + ({ repoPath: eventRepoPath, filePath }) => { + // Only process events for our repo + if (eventRepoPath !== repoPath) return; + + // Check if the changed file is our plan file + if (filePath === expectedPath) { + log.debug("Plan file changed externally:", filePath); + onFileChangedRef.current(); + } + }, + ); + + return unsubscribe; + }, [repoPath, taskId, fileName]); +} + interface PlanEditorProps { taskId: string; repoPath: string; @@ -29,12 +63,12 @@ export function PlanEditor({ const [content, setContent] = useState(initialContent || ""); const [isSaving, setIsSaving] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const [hasInitialized, setHasInitialized] = useState(!!initialContent); + const savedContentRef = useRef(initialContent || ""); const updateTabMetadata = usePanelLayoutStore( (state) => state.updateTabMetadata, ); - const isMarkdownFile = fileName.endsWith(".md"); - const queryClient = useQueryClient(); const { data: fetchedContent } = useQuery({ queryKey: ["task-file", repoPath, taskId, fileName], @@ -56,11 +90,49 @@ export function PlanEditor({ }, }); + // Initialize content from fetched data only once useEffect(() => { - if (!initialContent && fetchedContent && content === "") { + if (!hasInitialized && fetchedContent !== undefined) { setContent(fetchedContent); + savedContentRef.current = fetchedContent; + setHasInitialized(true); } - }, [fetchedContent, initialContent, content]); + }, [fetchedContent, hasInitialized]); + + // Handle external file changes + const handleExternalFileChange = useCallback(async () => { + // Refetch the file content + try { + let newContent: string | null = null; + if (fileName === "plan.md") { + newContent = await window.electronAPI?.readPlanFile(repoPath, taskId); + } else { + newContent = await window.electronAPI?.readTaskArtifact( + repoPath, + taskId, + fileName, + ); + } + + if (newContent !== null && newContent !== savedContentRef.current) { + // Only update if the file content actually changed from what we last saved/loaded + setContent(newContent); + savedContentRef.current = newContent; + // Also reset unsaved changes since we just loaded fresh content + setHasUnsavedChanges(false); + queryClient.setQueryData( + ["task-file", repoPath, taskId, fileName], + newContent, + ); + log.debug("Reloaded plan content from external change"); + } + } catch (error) { + log.error("Failed to reload file after external change:", error); + } + }, [repoPath, taskId, fileName, queryClient]); + + // Watch for external file changes + usePlanFileWatcher(repoPath, taskId, fileName, handleExternalFileChange); const handleSave = useCallback( async (contentToSave: string) => { @@ -80,6 +152,7 @@ export function PlanEditor({ ["task-file", repoPath, taskId, fileName], contentToSave, ); + savedContentRef.current = contentToSave; setHasUnsavedChanges(false); } catch (error) { log.error("Failed to save file:", error); @@ -94,10 +167,10 @@ export function PlanEditor({ handleSave(content); }, [content, handleSave]); - // Track unsaved changes + // Track unsaved changes by comparing to last saved content useEffect(() => { - setHasUnsavedChanges(content !== fetchedContent); - }, [content, fetchedContent]); + setHasUnsavedChanges(content !== savedContentRef.current); + }, [content]); // Update tab metadata when unsaved changes state changes useEffect(() => { @@ -115,7 +188,7 @@ export function PlanEditor({ handleManualSave(); } }, - { enableOnFormTags: ["INPUT", "TEXTAREA"] }, + { enableOnFormTags: ["INPUT", "TEXTAREA"], enableOnContentEditable: true }, [hasUnsavedChanges, isSaving, handleManualSave], ); @@ -134,23 +207,22 @@ export function PlanEditor({ overflow: "hidden", }} > - {isMarkdownFile ? ( - - ) : ( -