diff --git a/packages/frontend/src/hooks/useApi.ts b/packages/frontend/src/hooks/useApi.ts index 68480a9..ca3f457 100644 --- a/packages/frontend/src/hooks/useApi.ts +++ b/packages/frontend/src/hooks/useApi.ts @@ -215,6 +215,22 @@ export type RepositoryGitStatus = { diff: GitDiffEntry[]; }; +export type RepositoryFileGitDetails = { + path: string; + tracked: boolean; + ignored: boolean; + lineStats: { green: number; red: number }; + lastCommit: { + id: string | null; + message: string | null; + date: string | null; + link: string | null; + }; + lastModified: string; + lineCount: number; + diffBlocks: { before: string; after: string }[]; +}; + export type HarnessDefinition = { id: string; name: string; @@ -450,6 +466,10 @@ export const api = { getRepositoryGitStatus: (name: string) => request(`/repositories/${name}/git`), + getRepositoryFileGitDetails: (name: string, filePath: string) => + request( + `/repositories/${name}/git/file?path=${encodeURIComponent(filePath)}`, + ), pullRepository: (name: string) => request<{ output: string }>(`/repositories/${name}/git/pull`, { method: "POST", @@ -505,15 +525,19 @@ export const api = { request<{ workflows: WorkflowDefinition[] }>("/workflows"), getWorkspaceWorkflows: () => request<{ workflows: WorkspaceWorkflowSummary[] }>("/workspace/workflows"), - getWorkflowLogs: () => request<{ logs: WorkflowLogSummary[] }>("/workflow-logs"), + getWorkflowLogs: () => + request<{ logs: WorkflowLogSummary[] }>("/workflow-logs"), getWorkflowLogTail: (location: string, logName: string) => request( `/workflow-logs/${encodeURIComponent(location)}/${encodeURIComponent(logName)}`, ), terminateWorkflow: (workflowId: string) => - request<{ success: boolean; message?: string }>(`/workflows/${encodeURIComponent(workflowId)}/terminate`, { - method: "POST", - }), + request<{ success: boolean; message?: string }>( + `/workflows/${encodeURIComponent(workflowId)}/terminate`, + { + method: "POST", + }, + ), saveWorkflows: (workflows: WorkflowDefinition[]) => request<{ workflows: WorkflowDefinition[] }>("/workflows", { method: "PUT", diff --git a/packages/frontend/src/pages/RepositoryPage.tsx b/packages/frontend/src/pages/RepositoryPage.tsx index ac754d2..ad46626 100644 --- a/packages/frontend/src/pages/RepositoryPage.tsx +++ b/packages/frontend/src/pages/RepositoryPage.tsx @@ -16,7 +16,10 @@ import { ChatWindow } from "../components/ChatWindow"; import { MentionPathTextarea } from "../components/MentionPathTextarea"; import { WorkflowBuilderPanel } from "../components/WorkflowBuilderPanel"; import { SessionPickerModal } from "../components/SessionPickerModal"; -import { AgentSelector, DEFAULT_AGENT_VALUE } from "../components/AgentSelector"; +import { + AgentSelector, + DEFAULT_AGENT_VALUE, +} from "../components/AgentSelector"; import { usePersistentChat } from "../hooks/usePersistentChat"; import { usePersistentString } from "../hooks/usePersistentString"; import { useAgentCli } from "../hooks/useAgentCli"; @@ -26,6 +29,7 @@ import { ChatSession, FileNode, HarnessDefinition, + RepositoryFileGitDetails, RepositorySummary, RepositoryGitStatus, } from "../hooks/useApi"; @@ -66,6 +70,12 @@ const INLINE_CODE_PATTERN = /`[^`]*`/g; const normalizeMentionQuery = (value: string) => value.replace(/^\.\//, "").replace(/\\/g, "/").trim(); +const toRelativeFilePath = (value: string) => + value.replace(/\\/g, "/").replace(/^\.?\//, ""); + +const buildFileCommentPrompt = (relativeFilePath: string, userComment: string) => + `Regarding \`${relativeFilePath}\`: ${userComment}`; + const splitMentionSearch = (query: string) => { const normalized = normalizeMentionQuery(query); const lastSlash = normalized.lastIndexOf("/"); @@ -153,15 +163,15 @@ const MODEL_OPTIONS = [ }, { value: "github-copilot/gpt-5.2", - label: "github-copilot/gpt-5.2" + label: "github-copilot/gpt-5.2", }, { value: "github-copilot/gpt-5.3", - label: "github-copilot/gpt-5.3" + label: "github-copilot/gpt-5.3", }, { value: "github-copilot/gpt-5.4", - label: "github-copilot/gpt-5.4" + label: "github-copilot/gpt-5.4", }, { value: "github-copilot/gpt-5.2-codex", @@ -193,7 +203,7 @@ const MODEL_OPTIONS = [ { value: "openai/gpt-5.4-codex", label: "openai/gpt-5.4-codex", - } + }, ]; type HarnessRun = { @@ -248,6 +258,13 @@ const formatHarnessTimestamp = (value: string) => { return new Date(parsed).toLocaleString(); }; +const formatDateTime = (value: string | null) => { + if (!value) return "Unknown"; + const parsed = Date.parse(value); + if (!Number.isFinite(parsed)) return value; + return new Date(parsed).toLocaleString(); +}; + const formatCommandSourceLabel = (source: string) => { switch (source) { case "user": @@ -424,7 +441,9 @@ export const RepositoryPage: React.FC = () => { ]; }, [availableCommands, fileTree, repository?.path]); const [mentionQuery, setMentionQuery] = useState(""); - const [recursiveMentionResults, setRecursiveMentionResults] = useState([]); + const [recursiveMentionResults, setRecursiveMentionResults] = useState< + string[] + >([]); const [commandsError, setCommandsError] = useState(null); const [commandsLoading, setCommandsLoading] = useState(false); const [availableHarnesses, setAvailableHarnesses] = useState< @@ -488,9 +507,8 @@ export const RepositoryPage: React.FC = () => { placeholders: [], values: [], }); - const [commandPreview, setCommandPreview] = useState( - null, - ); + const [commandPreview, setCommandPreview] = + useState(null); const [harnessModal, setHarnessModal] = useState<{ open: boolean; harness: HarnessDefinition | null; @@ -548,6 +566,17 @@ export const RepositoryPage: React.FC = () => { const [selectedFile, setSelectedFile] = useState(null); const [editorContent, setEditorContent] = useState(""); const [editorStatus, setEditorStatus] = useState(null); + const [fileGitDetails, setFileGitDetails] = + useState(null); + const [fileGitDetailsLoading, setFileGitDetailsLoading] = useState(false); + const [fileGitDetailsError, setFileGitDetailsError] = useState( + null, + ); + const [fileCommentModal, setFileCommentModal] = useState<{ + open: boolean; + source: "preview" | "diff" | null; + }>({ open: false, source: null }); + const [fileCommentText, setFileCommentText] = useState(""); const [createModal, setCreateModal] = useState(false); const [uploadModal, setUploadModal] = useState(false); const [renameModal, setRenameModal] = useState<{ @@ -727,6 +756,29 @@ export const RepositoryPage: React.FC = () => { .finally(() => setGitStatusLoading(false)); }, [name, repository?.hasGit]); + const loadFileGitDetails = useCallback( + async (filePath: string) => { + if (!name || !repository?.hasGit) return; + setFileGitDetailsLoading(true); + try { + const response = await api.getRepositoryFileGitDetails(name, filePath); + setFileGitDetails(response); + setFileGitDetailsError(null); + } catch (error) { + console.error("Failed to load file git details", error); + const message = + error instanceof Error + ? error.message + : "Failed to load file git details"; + setFileGitDetailsError(message); + setFileGitDetails(null); + } finally { + setFileGitDetailsLoading(false); + } + }, + [name, repository?.hasGit], + ); + const refreshHarnessStatuses = useCallback(async () => { if (!harnessHistory.length) return; const updates = await Promise.all( @@ -818,7 +870,10 @@ export const RepositoryPage: React.FC = () => { } }, [name]); - const findNodeByPath = (node: FileNode, targetPath: string): FileNode | null => { + const findNodeByPath = ( + node: FileNode, + targetPath: string, + ): FileNode | null => { if (node.path === targetPath) { return node; } @@ -890,6 +945,12 @@ export const RepositoryPage: React.FC = () => { setSelectedFile(filePath); setEditorContent(response.content); setEditorStatus(null); + if (repository?.hasGit) { + void loadFileGitDetails(filePath); + } else { + setFileGitDetails(null); + setFileGitDetailsError(null); + } setActiveTab("editor"); } catch (error) { console.error("Failed to open file", error); @@ -931,7 +992,10 @@ export const RepositoryPage: React.FC = () => { } }; - const handleCreateWorktree = async (directoryName: string, branchName: string) => { + const handleCreateWorktree = async ( + directoryName: string, + branchName: string, + ) => { if (!name) return; setCreatingWorktree(true); try { @@ -1065,15 +1129,15 @@ export const RepositoryPage: React.FC = () => { model, agent, ); - + // No immediate message processing - polling handles everything if (reply.sessionId) { setSessionId(reply.sessionId); } - + setChatError(null); setActiveTab("agent"); - + // Keep chatLoading=true if processing (triggers existing polling) if (!reply.processing) setChatLoading(false); } catch (error) { @@ -1135,7 +1199,9 @@ export const RepositoryPage: React.FC = () => { } catch (error) { console.error("Failed to load session history", error); const message = - error instanceof Error ? error.message : "Failed to load session history"; + error instanceof Error + ? error.message + : "Failed to load session history"; setChatError(message); } finally { setChatLoading(false); @@ -1290,12 +1356,37 @@ export const RepositoryPage: React.FC = () => { await api.saveRepositoryFile(name, selectedFile, editorContent); setEditorStatus("Saved successfully"); refreshFiles(); + if (repository?.hasGit) { + void loadGitStatus(); + void loadFileGitDetails(selectedFile); + } } catch (error) { setEditorStatus("Failed to save file"); console.error("Failed to save file", error); } }; + const openFileCommentModal = (source: "preview" | "diff") => { + setFileCommentModal({ open: true, source }); + setFileCommentText(""); + }; + + const closeFileCommentModal = () => { + setFileCommentModal({ open: false, source: null }); + setFileCommentText(""); + }; + + const submitFileComment = () => { + if (!selectedFile || !fileCommentText.trim()) return; + const prompt = buildFileCommentPrompt( + toRelativeFilePath(selectedFile), + fileCommentText.trim(), + ); + handleSendMessage(prompt); + setActiveTab("agent"); + closeFileCommentModal(); + }; + const handleCreateFile = async () => { if (!name || !newFilePath.trim()) return; try { @@ -1404,6 +1495,8 @@ export const RepositoryPage: React.FC = () => { if (selectedFile === deleteModal.target) { setSelectedFile(null); setEditorContent(""); + setFileGitDetails(null); + setFileGitDetailsError(null); } setDeleteModal({ open: false, target: null }); refreshFiles(); @@ -1427,9 +1520,7 @@ export const RepositoryPage: React.FC = () => { const renderNode = (node: FileNode, depth = 0): React.ReactNode => { if (node.path === ".") { - return sortNodes(node.children).map((child) => - renderNode(child, depth), - ); + return sortNodes(node.children).map((child) => renderNode(child, depth)); } const indent = { marginLeft: depth * 16 }; const isFolder = node.type === "folder"; @@ -1826,7 +1917,7 @@ export const RepositoryPage: React.FC = () => { { id: "files", label: "File Browser", - content: ( + content: ( <>
+ } + > +
+ {fileGitDetails.diffBlocks.map((block, index) => ( +
+
+

Before

+
+                            {block.before || "(empty)"}
+                          
+
+
+

After

+
+                            {block.after || "(empty)"}
+                          
+
+
+ ))} +
+ + )} + + )} { className="editor-input" /> - + openFileCommentModal("preview")} + > + Comment + + ) + } + > {selectedFile?.endsWith(".md") ? (
{ const argumentPlan = getCommandArgumentPlan(command); const usesArguments = argumentPlan.labels.length > 0; return ( -
+
-
- + {uploadError &&
{uploadError}
}
@@ -2279,6 +2497,35 @@ export const RepositoryPage: React.FC = () => {
+ + +
+ +