summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-14 21:29:26 +0000
committerGitHub <noreply@github.com>2026-02-14 21:29:26 +0000
commit9aadbc7958d39d181c0dd0600e2b7c30bb6c391a (patch)
treeef8bed9718c39041191b58a284ee31f5d8d32521 /makima/frontend/src/routes
parentc1e55ce4fec79f9909b957f86bd7fa8b76939746 (diff)
downloadsoryu-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.tsx3
-rw-r--r--makima/frontend/src/routes/mesh.tsx2
-rw-r--r--makima/frontend/src/routes/orders.tsx238
-rw-r--r--makima/frontend/src/routes/workflow.tsx250
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>
- );
-}