diff options
| author | soryu <soryu@soryu.co> | 2026-02-14 21:29:26 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-14 21:29:26 +0000 |
| commit | 9aadbc7958d39d181c0dd0600e2b7c30bb6c391a (patch) | |
| tree | ef8bed9718c39041191b58a284ee31f5d8d32521 /makima/frontend/src/routes | |
| parent | c1e55ce4fec79f9909b957f86bd7fa8b76939746 (diff) | |
| download | soryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.tar.gz soryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.zip | |
Makima system improvements: Orders, directive questions, PR creation fix, bug fixes (#62)
* feat: soryu-co/soryu - makima: Fix directive goal update bug - stale closure issue
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Create Orders database schema and backend API
* feat: soryu-co/soryu - makima: Fix task Claude instance not receiving user inputs from input box
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Build Orders frontend page replacing the Board page
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Fix directive PR creation system
Diffstat (limited to 'makima/frontend/src/routes')
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 3 | ||||
| -rw-r--r-- | makima/frontend/src/routes/mesh.tsx | 2 | ||||
| -rw-r--r-- | makima/frontend/src/routes/orders.tsx | 238 | ||||
| -rw-r--r-- | makima/frontend/src/routes/workflow.tsx | 250 |
4 files changed, 241 insertions, 252 deletions
diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx index ca4437c..643cfee 100644 --- a/makima/frontend/src/routes/directives.tsx +++ b/makima/frontend/src/routes/directives.tsx @@ -12,7 +12,7 @@ export default function DirectivesPage() { const navigate = useNavigate(); const { id: selectedId } = useParams<{ id: string }>(); const { directives, loading: listLoading, create, remove } = useDirectives(); - const { directive, refresh: refreshDetail, start, pause, advance, completeStep, failStep, skipStep, updateGoal, cleanupTasks } = useDirective(selectedId); + const { directive, refresh: refreshDetail, update, start, pause, advance, completeStep, failStep, skipStep, updateGoal, cleanupTasks } = useDirective(selectedId); const [showCreate, setShowCreate] = useState(false); const [newTitle, setNewTitle] = useState(""); @@ -207,6 +207,7 @@ export default function DirectivesPage() { onFailStep={failStep} onSkipStep={skipStep} onUpdateGoal={updateGoal} + onUpdate={update} onDelete={handleDelete} onRefresh={refreshDetail} onCleanupTasks={cleanupTasks} diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx index cb4a77c..1d1db84 100644 --- a/makima/frontend/src/routes/mesh.tsx +++ b/makima/frontend/src/routes/mesh.tsx @@ -852,7 +852,7 @@ export default function MeshPage() { <div className="flex-1 min-h-0 overflow-hidden"> <TaskOutput entries={taskOutputEntries} - isStreaming={isStreaming || taskDetail.status === "running"} + isStreaming={isStreaming || taskDetail.status === "running" || taskDetail.status === "starting"} viewingSubtaskName={viewingSubtaskName} onClearSubtaskView={viewingSubtaskId ? () => { setViewingSubtaskId(null); diff --git a/makima/frontend/src/routes/orders.tsx b/makima/frontend/src/routes/orders.tsx new file mode 100644 index 0000000..735c557 --- /dev/null +++ b/makima/frontend/src/routes/orders.tsx @@ -0,0 +1,238 @@ +import { useState, useEffect } from "react"; +import { useParams, useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { OrderList } from "../components/orders/OrderList"; +import { OrderDetail } from "../components/orders/OrderDetail"; +import { useOrders, useOrder } from "../hooks/useOrders"; +import { useDirectives } from "../hooks/useDirectives"; +import { useAuth } from "../contexts/AuthContext"; +import type { OrderStatus, OrderType, OrderPriority } from "../lib/api"; + +export default function OrdersPage() { + const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); + const navigate = useNavigate(); + const { id: selectedId } = useParams<{ id: string }>(); + + const [statusFilter, setStatusFilter] = useState<OrderStatus | undefined>(undefined); + const [typeFilter, setTypeFilter] = useState<OrderType | undefined>(undefined); + const { orders, loading: listLoading, create, refresh: refreshList } = useOrders(statusFilter, typeFilter); + const { order, refresh: refreshDetail, update, remove: removeOrder, linkDirective, linkContract, convertToStep } = useOrder(selectedId); + const { directives } = useDirectives(); + + const [showCreate, setShowCreate] = useState(false); + const [newTitle, setNewTitle] = useState(""); + const [newDesc, setNewDesc] = useState(""); + const [newPriority, setNewPriority] = useState<OrderPriority>("medium"); + const [newType, setNewType] = useState<OrderType>("feature"); + + useEffect(() => { + if (!authLoading && isAuthConfigured && !isAuthenticated) { + navigate("/login"); + } + }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); + + if (authLoading) { + return ( + <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> + <Masthead showNav /> + <main className="flex-1 flex items-center justify-center"> + <p className="text-[#7788aa] font-mono text-sm">Loading...</p> + </main> + </div> + ); + } + + const handleCreate = async () => { + if (!newTitle.trim()) return; + try { + const o = await create({ + title: newTitle.trim(), + description: newDesc.trim() || undefined, + priority: newPriority, + orderType: newType, + }); + setShowCreate(false); + setNewTitle(""); + setNewDesc(""); + setNewPriority("medium"); + setNewType("feature"); + navigate(`/orders/${o.id}`); + } catch (e) { + console.error("Failed to create order:", e); + } + }; + + const handleDelete = async () => { + if (!selectedId) return; + if (!window.confirm("Delete this order?")) return; + try { + await removeOrder(); + await refreshList(); + navigate("/orders"); + } catch (e) { + console.error("Failed to delete:", e); + } + }; + + const handleUpdate = async (req: Parameters<typeof update>[0]) => { + await update(req); + await refreshList(); + }; + + const handleLinkDirective = async (directiveId: string) => { + await linkDirective(directiveId); + await refreshList(); + }; + + const handleLinkContract = async (contractId: string) => { + await linkContract(contractId); + await refreshList(); + }; + + const handleConvertToStep = async (directiveId: string) => { + await convertToStep(directiveId); + await refreshList(); + }; + + const priorityOptions: { value: OrderPriority; label: string }[] = [ + { value: "critical", label: "Critical" }, + { value: "high", label: "High" }, + { value: "medium", label: "Medium" }, + { value: "low", label: "Low" }, + { value: "none", label: "None" }, + ]; + + const typeOptions: { value: OrderType; label: string }[] = [ + { value: "feature", label: "Feature" }, + { value: "bug", label: "Bug" }, + { value: "spike", label: "Spike" }, + { value: "chore", label: "Chore" }, + { value: "improvement", label: "Improvement" }, + ]; + + return ( + <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> + <Masthead showNav /> + <main className="flex-1 flex overflow-hidden" style={{ height: "calc(100vh - 80px)" }}> + {/* Left: List */} + <div className="w-[280px] shrink-0 border-r border-dashed border-[rgba(117,170,252,0.2)] overflow-hidden flex flex-col"> + <OrderList + orders={orders} + selectedId={selectedId ?? null} + onSelect={(id) => navigate(`/orders/${id}`)} + onCreate={() => setShowCreate(true)} + statusFilter={statusFilter} + onStatusFilter={setStatusFilter} + typeFilter={typeFilter} + onTypeFilter={setTypeFilter} + /> + </div> + + {/* Right: Detail or Create */} + <div className="flex-1 overflow-hidden"> + {showCreate ? ( + <div className="p-4 max-w-lg"> + <h2 className="text-[14px] font-mono text-white font-medium mb-4"> + New Order + </h2> + <div className="flex flex-col gap-3"> + <div> + <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1"> + Title + </label> + <input + value={newTitle} + onChange={(e) => setNewTitle(e.target.value)} + placeholder="Order title..." + className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white" + onKeyDown={(e) => { + if (e.key === "Enter" && newTitle.trim()) handleCreate(); + }} + /> + </div> + <div> + <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1"> + Description (optional) + </label> + <textarea + value={newDesc} + onChange={(e) => setNewDesc(e.target.value)} + placeholder="Describe the order..." + rows={4} + className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white resize-y" + /> + </div> + <div className="flex gap-4"> + <div className="flex-1"> + <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1"> + Priority + </label> + <select + value={newPriority} + onChange={(e) => setNewPriority(e.target.value as OrderPriority)} + className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white" + > + {priorityOptions.map((p) => ( + <option key={p.value} value={p.value}>{p.label}</option> + ))} + </select> + </div> + <div className="flex-1"> + <label className="text-[10px] font-mono text-[#9bc3ff] uppercase tracking-wide block mb-1"> + Type + </label> + <select + value={newType} + onChange={(e) => setNewType(e.target.value as OrderType)} + className="w-full bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1.5 text-[12px] font-mono text-white" + > + {typeOptions.map((t) => ( + <option key={t.value} value={t.value}>{t.label}</option> + ))} + </select> + </div> + </div> + <div className="flex gap-2"> + <button + type="button" + onClick={handleCreate} + disabled={!newTitle.trim()} + className="text-[11px] font-mono text-emerald-400 hover:text-emerald-300 border border-emerald-800 rounded px-3 py-1 disabled:opacity-50" + > + Create + </button> + <button + type="button" + onClick={() => setShowCreate(false)} + className="text-[11px] font-mono text-[#7788aa] hover:text-white border border-[#2a3a5a] rounded px-3 py-1" + > + Cancel + </button> + </div> + </div> + </div> + ) : selectedId && order ? ( + <OrderDetail + order={order} + directives={directives} + onUpdate={handleUpdate} + onDelete={handleDelete} + onLinkDirective={handleLinkDirective} + onLinkContract={handleLinkContract} + onConvertToStep={handleConvertToStep} + onRefresh={refreshDetail} + /> + ) : ( + <div className="flex-1 flex items-center justify-center h-full"> + <p className="text-[#556677] font-mono text-[12px]"> + {listLoading + ? "Loading..." + : "Select an order or create a new one"} + </p> + </div> + )} + </div> + </main> + </div> + ); +} diff --git a/makima/frontend/src/routes/workflow.tsx b/makima/frontend/src/routes/workflow.tsx deleted file mode 100644 index e122092..0000000 --- a/makima/frontend/src/routes/workflow.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import { useState, useCallback, useEffect, useMemo } from "react"; -import { useNavigate } from "react-router"; -import { Masthead } from "../components/Masthead"; -import { WorkflowBoard } from "../components/workflow/WorkflowBoard"; -import { useContracts } from "../hooks/useContracts"; -import { useAuth } from "../contexts/AuthContext"; -import type { ContractPhase, ContractStatus, ContractSummary } from "../lib/api"; - -type StatusFilter = "all" | ContractStatus; - -export default function WorkflowPage() { - const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); - const navigate = useNavigate(); - - // Redirect to login if not authenticated (when auth is configured) - useEffect(() => { - if (!authLoading && isAuthConfigured && !isAuthenticated) { - navigate("/login"); - } - }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); - - // Show loading while checking auth - if (authLoading) { - return ( - <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex items-center justify-center"> - <p className="text-[#7788aa] font-mono text-sm">Loading...</p> - </main> - </div> - ); - } - - // Don't render if not authenticated (will redirect) - if (isAuthConfigured && !isAuthenticated) { - return null; - } - - return <WorkflowPageContent />; -} - -function WorkflowPageContent() { - const navigate = useNavigate(); - const { contracts, loading, error, changePhase, saveContract, editContract, removeContract } = useContracts(); - const [statusFilter, setStatusFilter] = useState<StatusFilter>("all"); - const [isCreating, setIsCreating] = useState(false); - const [newContractName, setNewContractName] = useState(""); - - // Filter contracts by status - const filteredContracts = useMemo(() => { - if (statusFilter === "all") { - return contracts; - } - return contracts.filter((c) => c.status === statusFilter); - }, [contracts, statusFilter]); - - const handleContractClick = useCallback( - (contractId: string) => { - navigate(`/contracts/${contractId}`); - }, - [navigate] - ); - - const handlePhaseChange = useCallback( - async (contractId: string, newPhase: ContractPhase) => { - await changePhase(contractId, newPhase); - }, - [changePhase] - ); - - // Context menu handlers - const handleContextMarkComplete = useCallback( - async (contract: ContractSummary) => { - await editContract(contract.id, { status: "completed", version: contract.version }); - }, - [editContract] - ); - - const handleContextMarkActive = useCallback( - async (contract: ContractSummary) => { - await editContract(contract.id, { status: "active", version: contract.version }); - }, - [editContract] - ); - - const handleContextArchive = useCallback( - async (contract: ContractSummary) => { - await editContract(contract.id, { status: "archived", version: contract.version }); - }, - [editContract] - ); - - const handleContextDelete = useCallback( - async (contract: ContractSummary) => { - if (confirm(`Are you sure you want to delete "${contract.name}"?`)) { - await removeContract(contract.id); - } - }, - [removeContract] - ); - - const handleContextGoToSupervisor = useCallback( - (contract: ContractSummary) => { - if (contract.supervisorTaskId) { - navigate(`/mesh/${contract.supervisorTaskId}`); - } - }, - [navigate] - ); - - const handleCreateContract = useCallback(async () => { - if (!newContractName.trim()) return; - const contract = await saveContract({ - name: newContractName.trim(), - }); - if (contract) { - setNewContractName(""); - setIsCreating(false); - navigate(`/contracts/${contract.id}`); - } - }, [newContractName, saveContract, navigate]); - - const handleCancelCreate = useCallback(() => { - setNewContractName(""); - setIsCreating(false); - }, []); - - return ( - <div className="relative z-10 h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex flex-col p-4 pt-2 gap-4 overflow-hidden"> - {error && ( - <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm shrink-0"> - {error} - </div> - )} - - {/* Header with filter and create button */} - <div className="flex items-center justify-between shrink-0"> - <div className="flex items-center gap-4"> - <h1 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider"> - Board - </h1> - {/* Status filter */} - <div className="flex items-center gap-1"> - {(["all", "active", "completed", "archived"] as StatusFilter[]).map( - (status) => ( - <button - key={status} - onClick={() => setStatusFilter(status)} - className={` - px-2 py-1 font-mono text-[10px] uppercase transition-colors - ${ - statusFilter === status - ? "bg-[rgba(117,170,252,0.1)] text-[#9bc3ff] border border-[rgba(117,170,252,0.3)]" - : "text-[#555] border border-transparent hover:text-[#75aafc]" - } - `} - > - {status} - </button> - ) - )} - </div> - </div> - <button - onClick={() => setIsCreating(true)} - className="px-3 py-1.5 font-mono text-xs text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors" - > - + New Contract - </button> - </div> - - {/* Create contract modal */} - {isCreating && ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> - <div className="w-full max-w-md p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)]"> - <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> - Create Contract - </h3> - <div className="space-y-4"> - <input - type="text" - value={newContractName} - onChange={(e) => setNewContractName(e.target.value)} - placeholder="Contract name" - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" - autoFocus - onKeyDown={(e) => { - if (e.key === "Enter") handleCreateContract(); - if (e.key === "Escape") handleCancelCreate(); - }} - /> - <div className="flex gap-2 justify-end"> - <button - onClick={handleCancelCreate} - className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - Cancel - </button> - <button - onClick={handleCreateContract} - disabled={!newContractName.trim()} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - > - Create - </button> - </div> - </div> - </div> - </div> - )} - - {/* Board */} - <div className="flex-1 min-h-0 overflow-hidden"> - {loading ? ( - <div className="h-full flex items-center justify-center"> - <p className="font-mono text-sm text-[#555]">Loading...</p> - </div> - ) : filteredContracts.length === 0 && statusFilter === "all" ? ( - <div className="h-full flex items-center justify-center"> - <div className="text-center"> - <p className="font-mono text-sm text-[#555] mb-4"> - No contracts yet - </p> - <button - onClick={() => setIsCreating(true)} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" - > - + Create First Contract - </button> - </div> - </div> - ) : ( - <WorkflowBoard - contracts={filteredContracts} - onContractClick={handleContractClick} - onPhaseChange={handlePhaseChange} - onMarkComplete={handleContextMarkComplete} - onMarkActive={handleContextMarkActive} - onArchive={handleContextArchive} - onDelete={handleContextDelete} - onGoToSupervisor={handleContextGoToSupervisor} - /> - )} - </div> - </main> - </div> - ); -} |
