summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components')
-rw-r--r--makima/frontend/src/components/directives/TaskSlideOutPanel.tsx145
-rw-r--r--makima/frontend/src/components/mesh/GitActionsPanel.tsx40
-rw-r--r--makima/frontend/src/components/mesh/TaskDetail.tsx41
-rw-r--r--makima/frontend/src/components/mesh/WorktreeFilesPanel.tsx7
4 files changed, 205 insertions, 28 deletions
diff --git a/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx b/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx
index 29fce23..176728c 100644
--- a/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx
+++ b/makima/frontend/src/components/directives/TaskSlideOutPanel.tsx
@@ -3,7 +3,8 @@ import { useTaskSubscription } from "../../hooks/useTaskSubscription";
import type { TaskOutputEvent } from "../../hooks/useTaskSubscription";
import { TaskOutput } from "../mesh/TaskOutput";
import { WorktreeFilesPanel } from "../mesh/WorktreeFilesPanel";
-import { getTaskOutput } from "../../lib/api";
+import { OverlayDiffViewer } from "../mesh/OverlayDiffViewer";
+import { getTaskOutput, getTaskDiff } from "../../lib/api";
interface TaskSlideOutPanelProps {
taskId: string;
@@ -21,21 +22,37 @@ export function TaskSlideOutPanel({
const [entries, setEntries] = useState<TaskOutputEvent[]>([]);
const [isStreaming, setIsStreaming] = useState(false);
const [loadingHistory, setLoadingHistory] = useState(false);
+ const [showDiff, setShowDiff] = useState(false);
+ const [diffContent, setDiffContent] = useState<string>("");
+ const [diffLoading, setDiffLoading] = useState(false);
// Escape key handler
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
- if (e.key === "Escape") onClose();
+ // If diff is showing, close diff first; otherwise close panel
+ if (e.key === "Escape") {
+ if (selectedFileDiff !== null || diffLoading) {
+ setSelectedFileDiff(null);
+ setSelectedFilePath(null);
+ setDiffLoading(false);
+ } else {
+ onClose();
+ }
+ }
};
if (isOpen) document.addEventListener("keydown", handleEsc);
return () => document.removeEventListener("keydown", handleEsc);
- }, [isOpen, onClose]);
+ }, [isOpen, onClose, selectedFileDiff, diffLoading]);
// Load historical output when panel opens with a taskId
useEffect(() => {
if (!isOpen || !taskId) {
setEntries([]);
setIsStreaming(false);
+ // Reset diff state when panel closes
+ setSelectedFileDiff(null);
+ setSelectedFilePath(null);
+ setDiffLoading(false);
return;
}
@@ -98,6 +115,23 @@ export function TaskSlideOutPanel({
[]
);
+ // Handle file click to show diff
+ const handleFileClick = useCallback(async (_filePath: string) => {
+ if (!taskId) return;
+ setDiffLoading(true);
+ try {
+ const result = await getTaskDiff(taskId);
+ if (result.success && result.diff) {
+ setDiffContent(result.diff);
+ setShowDiff(true);
+ }
+ } catch (e) {
+ console.error("Failed to get diff:", e);
+ } finally {
+ setDiffLoading(false);
+ }
+ }, [taskId]);
+
// Subscribe to live output
useTaskSubscription({
taskId: isOpen ? taskId : null,
@@ -106,6 +140,8 @@ export function TaskSlideOutPanel({
onUpdate: handleUpdate,
});
+ const showingDiff = selectedFileDiff !== null || diffLoading;
+
return (
<>
{/* Backdrop overlay */}
@@ -125,13 +161,27 @@ export function TaskSlideOutPanel({
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-dashed border-[rgba(117,170,252,0.25)] shrink-0">
<div className="flex items-center gap-2 min-w-0 flex-1">
+ {showingDiff && (
+ <button
+ type="button"
+ onClick={() => {
+ setSelectedFileDiff(null);
+ setSelectedFilePath(null);
+ setDiffLoading(false);
+ }}
+ className="text-[#75aafc] hover:text-white font-mono text-xs transition-colors shrink-0 mr-1"
+ title="Back to worktree view"
+ >
+ &larr;
+ </button>
+ )}
<span className="text-[10px] font-mono text-[#75aafc] uppercase tracking-wide shrink-0">
- Task
+ {showingDiff ? "Diff" : "Task"}
</span>
<span className="text-[12px] font-mono text-white truncate">
- {taskName || taskId}
+ {showingDiff ? (selectedFilePath || "Loading...") : (taskName || taskId)}
</span>
- {isStreaming && (
+ {!showingDiff && isStreaming && (
<span className="flex items-center gap-1 px-1.5 py-0.5 bg-green-400/10 border border-green-400/30 shrink-0">
<span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
<span className="text-green-400 font-mono text-[9px] uppercase">
@@ -141,39 +191,88 @@ export function TaskSlideOutPanel({
)}
</div>
<button
+ onClick={async () => {
+ if (!taskId) return;
+ setDiffLoading(true);
+ try {
+ const result = await getTaskDiff(taskId);
+ if (result.success && result.diff) {
+ setDiffContent(result.diff);
+ setShowDiff(true);
+ }
+ } catch (e) {
+ console.error("Failed to get diff:", e);
+ } finally {
+ setDiffLoading(false);
+ }
+ }}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] transition-colors px-1.5 py-0.5 border border-[rgba(117,170,252,0.2)] hover:border-[rgba(117,170,252,0.4)] shrink-0"
+ >
+ {diffLoading ? "Loading..." : "View Diff"}
+ </button>
+ <button
type="button"
onClick={onClose}
className="text-[#7788aa] hover:text-white font-mono text-sm transition-colors ml-2 shrink-0 w-6 h-6 flex items-center justify-center"
>
- ✕
+ &#x2715;
</button>
</div>
{/* Content */}
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
- {/* Task Output section (~60% height) */}
- <div className="flex-[3] min-h-0 flex flex-col border-b border-[rgba(117,170,252,0.15)]">
- {loadingHistory ? (
- <div className="flex-1 flex items-center justify-center">
- <span className="font-mono text-xs text-[#555] animate-pulse">
- Loading output...
- </span>
- </div>
- ) : (
- <TaskOutput
- entries={entries}
- isStreaming={isStreaming}
- taskId={taskId}
+ {showingDiff ? (
+ /* Diff view replaces the worktree panel content */
+ <div className="flex-1 min-h-0 overflow-y-auto">
+ <OverlayDiffViewer
+ diff={selectedFileDiff || ""}
+ loading={diffLoading}
+ onClose={() => {
+ setSelectedFileDiff(null);
+ setSelectedFilePath(null);
+ setDiffLoading(false);
+ }}
+ title={selectedFilePath ? `Diff: ${selectedFilePath}` : "File Diff"}
/>
- )}
- </div>
+ </div>
+ ) : (
+ <>
+ {/* Task Output section (~60% height) */}
+ <div className="flex-[3] min-h-0 flex flex-col border-b border-[rgba(117,170,252,0.15)]">
+ {loadingHistory ? (
+ <div className="flex-1 flex items-center justify-center">
+ <span className="font-mono text-xs text-[#555] animate-pulse">
+ Loading output...
+ </span>
+ </div>
+ ) : (
+ <TaskOutput
+ entries={entries}
+ isStreaming={isStreaming}
+ taskId={taskId}
+ />
+ )}
+ </div>
{/* Worktree Changes section (~40% height) */}
<div className="flex-[2] min-h-0 overflow-y-auto">
- {taskId && <WorktreeFilesPanel taskId={taskId} />}
+ {taskId && <WorktreeFilesPanel taskId={taskId} onFileClick={handleFileClick} />}
</div>
</div>
</div>
+
+ {/* Diff modal */}
+ {showDiff && (
+ <div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
+ <div className="max-w-4xl w-full max-h-[80vh]">
+ <OverlayDiffViewer
+ diff={diffContent}
+ onClose={() => setShowDiff(false)}
+ title={`Changes in ${taskName || taskId}`}
+ />
+ </div>
+ </div>
+ )}
</>
);
}
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