summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-06 04:08:11 +0000
committersoryu <soryu@soryu.co>2026-01-11 03:01:13 +0000
commit8b17a175c3e7e27b789812eba4e3cd760beadb10 (patch)
tree7864dcaa2fa9db47fdfd4e8bfdb0b1dde832aa33 /makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx
parentf79c416c58557d2f946aa5332989afdfa8c021cd (diff)
downloadsoryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.tar.gz
soryu-8b17a175c3e7e27b789812eba4e3cd760beadb10.zip
Initial Control system
Diffstat (limited to 'makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx')
-rw-r--r--makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx262
1 files changed, 262 insertions, 0 deletions
diff --git a/makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx b/makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx
new file mode 100644
index 0000000..3621b08
--- /dev/null
+++ b/makima/frontend/src/components/mesh/InlineSubtaskEditor.tsx
@@ -0,0 +1,262 @@
+import { useState, useCallback, useEffect } from "react";
+import type { TaskWithSubtasks, TaskStatus } from "../../lib/api";
+import { getTask, updateTask } from "../../lib/api";
+
+interface InlineSubtaskEditorProps {
+ subtaskId: string;
+ onClose: () => void;
+ onUpdated: () => void;
+ onNavigate?: (taskId: string) => void;
+}
+
+function getStatusColor(status: TaskStatus): string {
+ switch (status) {
+ case "pending":
+ return "text-[#9bc3ff]";
+ case "running":
+ return "text-green-400";
+ case "paused":
+ return "text-yellow-400";
+ case "blocked":
+ return "text-orange-400";
+ case "done":
+ return "text-emerald-400";
+ case "failed":
+ return "text-red-400";
+ case "merged":
+ return "text-purple-400";
+ default:
+ return "text-[#9bc3ff]";
+ }
+}
+
+function getStatusBgColor(status: TaskStatus): string {
+ switch (status) {
+ case "pending":
+ return "bg-[rgba(117,170,252,0.1)]";
+ case "running":
+ return "bg-green-400/10";
+ case "paused":
+ return "bg-yellow-400/10";
+ case "blocked":
+ return "bg-orange-400/10";
+ case "done":
+ return "bg-emerald-400/10";
+ case "failed":
+ return "bg-red-400/10";
+ case "merged":
+ return "bg-purple-400/10";
+ default:
+ return "bg-[rgba(117,170,252,0.1)]";
+ }
+}
+
+export function InlineSubtaskEditor({
+ subtaskId,
+ onClose,
+ onUpdated,
+ onNavigate,
+}: InlineSubtaskEditorProps) {
+ const [subtask, setSubtask] = useState<TaskWithSubtasks | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [isEditing, setIsEditing] = useState(false);
+ const [editName, setEditName] = useState("");
+ const [editDescription, setEditDescription] = useState("");
+ const [editPlan, setEditPlan] = useState("");
+
+ // Load subtask details
+ useEffect(() => {
+ setLoading(true);
+ getTask(subtaskId)
+ .then((task) => {
+ setSubtask(task);
+ setEditName(task.name);
+ setEditDescription(task.description || "");
+ setEditPlan(task.plan);
+ })
+ .catch((err) => {
+ console.error("Failed to load subtask:", err);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }, [subtaskId]);
+
+ const handleSave = useCallback(async () => {
+ if (!subtask || saving) return;
+ setSaving(true);
+ try {
+ await updateTask(subtaskId, {
+ name: editName,
+ description: editDescription || undefined,
+ plan: editPlan,
+ version: subtask.version,
+ });
+ // Refresh subtask
+ const updated = await getTask(subtaskId);
+ setSubtask(updated);
+ setIsEditing(false);
+ onUpdated();
+ } catch (err) {
+ console.error("Failed to save subtask:", err);
+ } finally {
+ setSaving(false);
+ }
+ }, [subtask, subtaskId, editName, editDescription, editPlan, saving, onUpdated]);
+
+ const handleCancel = useCallback(() => {
+ if (subtask) {
+ setEditName(subtask.name);
+ setEditDescription(subtask.description || "");
+ setEditPlan(subtask.plan);
+ }
+ setIsEditing(false);
+ }, [subtask]);
+
+ if (loading) {
+ return (
+ <div className="p-4 bg-[rgba(0,0,0,0.2)] border-l-2 border-[#3f6fb3]">
+ <div className="font-mono text-xs text-[#75aafc]">Loading subtask...</div>
+ </div>
+ );
+ }
+
+ if (!subtask) {
+ return (
+ <div className="p-4 bg-[rgba(0,0,0,0.2)] border-l-2 border-red-400">
+ <div className="font-mono text-xs text-red-400">Failed to load subtask</div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="bg-[rgba(0,0,0,0.2)] border-l-2 border-[#3f6fb3]">
+ {/* Header */}
+ <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.15)]">
+ <div className="flex items-center gap-2">
+ <button
+ onClick={onClose}
+ className="font-mono text-[10px] text-[#555] hover:text-[#75aafc]"
+ >
+ [close]
+ </button>
+ <span
+ className={`px-1.5 py-0.5 font-mono text-[9px] uppercase ${getStatusColor(
+ subtask.status as TaskStatus
+ )} ${getStatusBgColor(subtask.status as TaskStatus)} border border-current/20`}
+ >
+ {subtask.status}
+ </span>
+ {onNavigate && (
+ <button
+ onClick={() => onNavigate(subtaskId)}
+ className="font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff]"
+ >
+ [open full view]
+ </button>
+ )}
+ </div>
+ <div className="flex items-center gap-2">
+ {isEditing ? (
+ <>
+ <button
+ onClick={handleCancel}
+ disabled={saving}
+ className="px-2 py-0.5 font-mono text-[10px] text-[#555] hover:text-[#9bc3ff] disabled:opacity-50"
+ >
+ Cancel
+ </button>
+ <button
+ onClick={handleSave}
+ disabled={saving}
+ className="px-2 py-0.5 font-mono text-[10px] text-green-400 border border-green-400/30 hover:border-green-400/50 disabled:opacity-50"
+ >
+ {saving ? "..." : "Save"}
+ </button>
+ </>
+ ) : (
+ <button
+ onClick={() => setIsEditing(true)}
+ className="px-2 py-0.5 font-mono text-[10px] text-[#75aafc] hover:text-[#9bc3ff]"
+ >
+ Edit
+ </button>
+ )}
+ </div>
+ </div>
+
+ {/* Content */}
+ <div className="p-3 space-y-3">
+ {/* Name */}
+ {isEditing ? (
+ <input
+ type="text"
+ value={editName}
+ onChange={(e) => setEditName(e.target.value)}
+ className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-sm px-2 py-1 outline-none focus:border-[#3f6fb3]"
+ placeholder="Subtask name"
+ />
+ ) : (
+ <div className="font-mono text-sm text-[#dbe7ff]">{subtask.name}</div>
+ )}
+
+ {/* Description */}
+ {isEditing ? (
+ <textarea
+ value={editDescription}
+ onChange={(e) => setEditDescription(e.target.value)}
+ className="w-full bg-transparent border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] min-h-[40px] resize-y"
+ placeholder="Description (optional)"
+ />
+ ) : subtask.description ? (
+ <div className="font-mono text-xs text-[#75aafc]">{subtask.description}</div>
+ ) : null}
+
+ {/* Plan */}
+ <div className="space-y-1">
+ <div className="font-mono text-[10px] text-[#555] uppercase">Plan</div>
+ {isEditing ? (
+ <textarea
+ value={editPlan}
+ onChange={(e) => setEditPlan(e.target.value)}
+ className="w-full bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.25)] text-[#dbe7ff] font-mono text-xs px-2 py-1 outline-none focus:border-[#3f6fb3] min-h-[100px] resize-y"
+ placeholder="Plan/instructions..."
+ />
+ ) : (
+ <pre className="bg-[rgba(0,0,0,0.2)] border border-[rgba(117,170,252,0.15)] p-2 font-mono text-xs text-[#9bc3ff] whitespace-pre-wrap max-h-[150px] overflow-y-auto">
+ {subtask.plan}
+ </pre>
+ )}
+ </div>
+
+ {/* Progress/Error */}
+ {subtask.progressSummary && (
+ <div className="font-mono text-[10px] text-[#75aafc]">
+ <span className="text-[#555]">Progress:</span> {subtask.progressSummary}
+ </div>
+ )}
+ {subtask.errorMessage && (
+ <div className="font-mono text-[10px] text-red-400">
+ <span className="text-red-400/50">Error:</span> {subtask.errorMessage}
+ </div>
+ )}
+
+ {/* Nested subtasks indicator */}
+ {subtask.subtasks.length > 0 && (
+ <div className="font-mono text-[10px] text-[#555]">
+ Has {subtask.subtasks.length} subtask{subtask.subtasks.length > 1 ? "s" : ""}
+ {onNavigate && (
+ <button
+ onClick={() => onNavigate(subtaskId)}
+ className="ml-2 text-[#75aafc] hover:text-[#9bc3ff]"
+ >
+ [view all]
+ </button>
+ )}
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}