diff options
| author | soryu <soryu@soryu.co> | 2026-03-09 17:20:52 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-09 17:20:52 +0000 |
| commit | f49aaa39a32661b54c109ba002d24cbdf73f4ea3 (patch) | |
| tree | 457f763ebf0eb5f8d0c66b147f8de5acf8e378fe /makima/frontend/src/components/mesh | |
| parent | afaae8aba719bf74404a64b57426ecc6a7e70775 (diff) | |
| download | soryu-f49aaa39a32661b54c109ba002d24cbdf73f4ea3.tar.gz soryu-f49aaa39a32661b54c109ba002d24cbdf73f4ea3.zip | |
feat: worktree diff/commit endpoints and frontend diff viewing (#88)
* feat: soryu-co/soryu - makima: Fix worktree info failing when origin ref is missing
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Add worktree commit endpoint and diff endpoint for regular users
* feat: soryu-co/soryu - makima: Add frontend diff viewing with clickable worktree files
Diffstat (limited to 'makima/frontend/src/components/mesh')
3 files changed, 83 insertions, 5 deletions
diff --git a/makima/frontend/src/components/mesh/GitActionsPanel.tsx b/makima/frontend/src/components/mesh/GitActionsPanel.tsx index be2e06d..ff1dc7d 100644 --- a/makima/frontend/src/components/mesh/GitActionsPanel.tsx +++ b/makima/frontend/src/components/mesh/GitActionsPanel.tsx @@ -1,5 +1,5 @@ import { useState, useCallback } from "react"; -import { exportTaskPatch, pushTaskBranch, createTaskPR, type ExportPatchResponse } from "../../lib/api"; +import { exportTaskPatch, pushTaskBranch, createTaskPR, commitWorktree, type ExportPatchResponse } from "../../lib/api"; interface GitActionsPanelProps { taskId: string; @@ -20,6 +20,8 @@ export function GitActionsPanel({ const [isExporting, setIsExporting] = useState(false); const [isPushing, setIsPushing] = useState(false); const [isCreatingPR, setIsCreatingPR] = useState(false); + const [isCommitting, setIsCommitting] = useState(false); + const [commitMessage, setCommitMessage] = useState(""); const [toast, setToast] = useState<ToastMessage | null>(null); const [exportedPatch, setExportedPatch] = useState<ExportPatchResponse | null>(null); @@ -82,6 +84,23 @@ export function GitActionsPanel({ } }, [taskId, isLocalOnly]); + const handleCommit = useCallback(async () => { + setIsCommitting(true); + try { + const result = await commitWorktree(taskId, commitMessage || undefined); + if (result.success) { + showToast("success", `Committed: ${result.commitSha?.substring(0, 8) || "done"}`); + setCommitMessage(""); + } else { + showToast("error", result.error || "Failed to commit"); + } + } catch (e) { + showToast("error", e instanceof Error ? e.message : "Failed to commit"); + } finally { + setIsCommitting(false); + } + }, [taskId, commitMessage]); + return ( <div className="space-y-3"> {/* Section Header */} @@ -112,6 +131,25 @@ export function GitActionsPanel({ </div> )} + {/* Commit section */} + <div className="flex gap-2"> + <input + type="text" + value={commitMessage} + onChange={(e) => setCommitMessage(e.target.value)} + placeholder="Commit message (optional)" + className="flex-1 px-2 py-1.5 font-mono text-xs bg-transparent border border-[rgba(117,170,252,0.2)] text-[#dbe7ff] placeholder:text-[#555] focus:border-[#75aafc] focus:outline-none" + onKeyDown={(e) => { if (e.key === "Enter") handleCommit(); }} + /> + <button + onClick={handleCommit} + disabled={isCommitting} + className="px-3 py-1.5 font-mono text-xs text-emerald-400 border border-emerald-400/30 hover:border-emerald-400/50 hover:bg-emerald-400/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" + > + {isCommitting ? "Committing..." : "Commit"} + </button> + </div> + {/* Action buttons */} <div className="flex flex-wrap gap-2"> <button diff --git a/makima/frontend/src/components/mesh/TaskDetail.tsx b/makima/frontend/src/components/mesh/TaskDetail.tsx index fdcc58b..00719e2 100644 --- a/makima/frontend/src/components/mesh/TaskDetail.tsx +++ b/makima/frontend/src/components/mesh/TaskDetail.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useMemo, useEffect } from "react"; import type { TaskWithSubtasks, TaskStatus, TaskSummary, CompletionAction, DaemonDirectory } from "../../lib/api"; -import { retryCompletionAction, getDaemonDirectories, cloneWorktree } from "../../lib/api"; +import { retryCompletionAction, getDaemonDirectories, cloneWorktree, getWorktreeDiff } from "../../lib/api"; import { SubtaskTree, SubtaskProgressBar, calculateTreeStats } from "./SubtaskTree"; import { OverlayDiffViewer } from "./OverlayDiffViewer"; import { PRPreview } from "./PRPreview"; @@ -136,6 +136,9 @@ export function TaskDetail({ ); const [showDiff, setShowDiff] = useState(false); const [showPRPreview, setShowPRPreview] = useState(false); + const [worktreeFileDiff, setWorktreeFileDiff] = useState<string | null>(null); + const [worktreeFileDiffPath, setWorktreeFileDiffPath] = useState<string | null>(null); + const [worktreeFileDiffLoading, setWorktreeFileDiffLoading] = useState(false); const [useTreeView, setUseTreeView] = useState(false); // Track which subtask is expanded for inline editing const [expandedSubtaskId, setExpandedSubtaskId] = useState<string | null>(null); @@ -153,6 +156,22 @@ export function TaskDetail({ // Track branch modal state const [showBranchModal, setShowBranchModal] = useState(false); + // Handle clicking a file in the worktree panel to show its diff + const handleWorktreeFileClick = useCallback(async (filePath: string) => { + setWorktreeFileDiffLoading(true); + setWorktreeFileDiffPath(filePath); + setWorktreeFileDiff(null); + try { + const result = await getWorktreeDiff(task.id, filePath); + setWorktreeFileDiff(result.diff); + } catch (e) { + console.error("Failed to fetch worktree diff:", e); + setWorktreeFileDiff(""); + } finally { + setWorktreeFileDiffLoading(false); + } + }, [task.id]); + // Check if task is running const isTaskRunning = task.status === "running" || task.status === "initializing" || task.status === "starting"; // Check if task is in a terminal state (can be continued/reopened) @@ -897,7 +916,7 @@ export function TaskDetail({ {/* Worktree Files Panel - show changed files in the worktree */} {(task.status === "done" || task.status === "failed" || task.status === "merged" || task.status === "running") && ( - <WorktreeFilesPanel taskId={task.id} /> + <WorktreeFilesPanel taskId={task.id} onFileClick={handleWorktreeFileClick} /> )} {/* Patches List Panel - show exported patches for this task */} @@ -920,6 +939,24 @@ export function TaskDetail({ </div> )} + {/* Worktree File Diff Modal */} + {(worktreeFileDiff !== null || worktreeFileDiffLoading) && ( + <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> + <div className="max-w-4xl w-full max-h-[80vh]"> + <OverlayDiffViewer + diff={worktreeFileDiff || ""} + loading={worktreeFileDiffLoading} + onClose={() => { + setWorktreeFileDiff(null); + setWorktreeFileDiffPath(null); + setWorktreeFileDiffLoading(false); + }} + title={worktreeFileDiffPath ? `Diff: ${worktreeFileDiffPath}` : "File Diff"} + /> + </div> + </div> + )} + {/* PR Preview Modal */} {showPRPreview && ( <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> diff --git a/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx b/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx index b529588..bb3361d 100644 --- a/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx +++ b/makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx @@ -4,6 +4,7 @@ import { getWorktreeInfo } from "../../lib/api"; interface WorktreeFilesPanelProps { taskId: string; + onFileClick?: (filePath: string) => void; } /** Get status badge styling based on file status */ @@ -35,7 +36,7 @@ function getStatusStyle(status: string): { color: string; bgColor: string; label } } -export function WorktreeFilesPanel({ taskId }: WorktreeFilesPanelProps) { +export function WorktreeFilesPanel({ taskId, onFileClick }: WorktreeFilesPanelProps) { const [worktreeInfo, setWorktreeInfo] = useState<WorktreeInfo | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); @@ -116,6 +117,7 @@ export function WorktreeFilesPanel({ taskId }: WorktreeFilesPanelProps) { const { stats, files } = worktreeInfo; const displayFiles = expanded ? files : files.slice(0, 10); + const isClickable = !!onFileClick; return ( <div className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)]"> @@ -152,7 +154,8 @@ export function WorktreeFilesPanel({ taskId }: WorktreeFilesPanelProps) { return ( <div key={file.path} - className="flex items-center gap-2 px-3 py-1.5 hover:bg-[rgba(117,170,252,0.03)]" + className={`flex items-center gap-2 px-3 py-1.5 ${onFileClick ? 'cursor-pointer hover:bg-[rgba(117,170,252,0.08)]' : 'hover:bg-[rgba(117,170,252,0.03)]'}`} + onClick={() => onFileClick?.(file.path)} > {/* Status badge */} <span |
