From 87044a747b47bd83249d61a45842c7f7b2eae56d Mon Sep 17 00:00:00 2001
From: soryu
Date: Sun, 11 Jan 2026 05:52:14 +0000
Subject: Contract system
---
.../frontend/src/components/JapaneseHoverText.tsx | 77 ++
makima/frontend/src/components/Masthead.tsx | 6 +-
makima/frontend/src/components/NavStrip.tsx | 2 +
.../src/components/contracts/ContractCliInput.tsx | 974 +++++++++++++++++++++
.../src/components/contracts/ContractDetail.tsx | 794 +++++++++++++++++
.../src/components/contracts/ContractList.tsx | 176 ++++
.../src/components/contracts/PhaseBadge.tsx | 54 ++
.../contracts/PhaseDeliverablesPanel.tsx | 301 +++++++
.../src/components/contracts/PhaseHint.tsx | 90 ++
.../src/components/contracts/PhaseProgressBar.tsx | 142 +++
.../components/contracts/QuickActionButtons.tsx | 217 +++++
.../src/components/contracts/RepositoryPanel.tsx | 260 ++++++
.../components/contracts/TaskDerivationPreview.tsx | 221 +++++
.../frontend/src/components/files/BodyRenderer.tsx | 376 ++++++++
.../frontend/src/components/files/FileDetail.tsx | 2 +
makima/frontend/src/components/files/FileList.tsx | 179 +---
.../src/components/files/RepoSyncIndicator.tsx | 190 ++++
.../src/components/listen/ControlPanel.tsx | 33 +-
makima/frontend/src/components/mesh/TaskDetail.tsx | 12 +
makima/frontend/src/components/mesh/TaskList.tsx | 215 +++--
makima/frontend/src/components/mesh/TaskOutput.tsx | 96 ++
makima/frontend/src/components/mesh/TaskTree.tsx | 390 +++++++++
.../src/components/workflow/PhaseColumn.tsx | 123 +++
.../src/components/workflow/WorkflowBoard.tsx | 54 ++
.../components/workflow/WorkflowContractCard.tsx | 53 ++
makima/frontend/src/hooks/useContracts.ts | 308 +++++++
makima/frontend/src/hooks/useWebSocket.ts | 3 +-
makima/frontend/src/lib/api.ts | 542 +++++++++++-
makima/frontend/src/lib/markdown.ts | 228 +++++
makima/frontend/src/main.tsx | 26 +
makima/frontend/src/routes/_index.tsx | 6 +-
makima/frontend/src/routes/contracts.tsx | 614 +++++++++++++
makima/frontend/src/routes/files.tsx | 221 ++++-
makima/frontend/src/routes/listen.tsx | 45 +-
makima/frontend/src/routes/mesh.tsx | 250 +++++-
makima/frontend/src/routes/settings.tsx | 113 +++
makima/frontend/src/routes/workflow.tsx | 205 +++++
37 files changed, 7348 insertions(+), 250 deletions(-)
create mode 100644 makima/frontend/src/components/JapaneseHoverText.tsx
create mode 100644 makima/frontend/src/components/contracts/ContractCliInput.tsx
create mode 100644 makima/frontend/src/components/contracts/ContractDetail.tsx
create mode 100644 makima/frontend/src/components/contracts/ContractList.tsx
create mode 100644 makima/frontend/src/components/contracts/PhaseBadge.tsx
create mode 100644 makima/frontend/src/components/contracts/PhaseDeliverablesPanel.tsx
create mode 100644 makima/frontend/src/components/contracts/PhaseHint.tsx
create mode 100644 makima/frontend/src/components/contracts/PhaseProgressBar.tsx
create mode 100644 makima/frontend/src/components/contracts/QuickActionButtons.tsx
create mode 100644 makima/frontend/src/components/contracts/RepositoryPanel.tsx
create mode 100644 makima/frontend/src/components/contracts/TaskDerivationPreview.tsx
create mode 100644 makima/frontend/src/components/files/RepoSyncIndicator.tsx
create mode 100644 makima/frontend/src/components/mesh/TaskTree.tsx
create mode 100644 makima/frontend/src/components/workflow/PhaseColumn.tsx
create mode 100644 makima/frontend/src/components/workflow/WorkflowBoard.tsx
create mode 100644 makima/frontend/src/components/workflow/WorkflowContractCard.tsx
create mode 100644 makima/frontend/src/hooks/useContracts.ts
create mode 100644 makima/frontend/src/lib/markdown.ts
create mode 100644 makima/frontend/src/routes/contracts.tsx
create mode 100644 makima/frontend/src/routes/workflow.tsx
(limited to 'makima/frontend/src')
diff --git a/makima/frontend/src/components/JapaneseHoverText.tsx b/makima/frontend/src/components/JapaneseHoverText.tsx
new file mode 100644
index 0000000..3e60ee2
--- /dev/null
+++ b/makima/frontend/src/components/JapaneseHoverText.tsx
@@ -0,0 +1,77 @@
+import { useState, useCallback, useRef } from "react";
+
+const GLYPHS = "▒▓░█#@*+:-/[]{}<>_";
+
+interface JapaneseHoverTextProps {
+ japanese: string;
+ english: string;
+ className?: string;
+}
+
+/**
+ * Displays Japanese text, transitions to English on hover with scramble effect
+ */
+export function JapaneseHoverText({
+ japanese,
+ english,
+ className = "",
+}: JapaneseHoverTextProps) {
+ const [isHovered, setIsHovered] = useState(false);
+ const [displayText, setDisplayText] = useState(english);
+ const timerRef = useRef | null>(null);
+ const iterationRef = useRef(0);
+
+ const scrambleToEnglish = useCallback(() => {
+ setIsHovered(true);
+
+ // Clear any existing animation
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ }
+
+ iterationRef.current = 0;
+
+ timerRef.current = setInterval(() => {
+ const text = english;
+ const iteration = iterationRef.current;
+
+ const display = text
+ .split("")
+ .map((char, index) => {
+ if (index < iteration) return char;
+ return GLYPHS.charAt(Math.floor(Math.random() * GLYPHS.length));
+ })
+ .join("");
+
+ setDisplayText(display);
+ iterationRef.current += 1;
+
+ if (iteration > text.length + 2) {
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
+ setDisplayText(english);
+ }
+ }, 26);
+ }, [english]);
+
+ const resetToJapanese = useCallback(() => {
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ timerRef.current = null;
+ }
+ setIsHovered(false);
+ setDisplayText(english);
+ }, [english]);
+
+ return (
+
+ {isHovered ? displayText : japanese}
+
+ );
+}
diff --git a/makima/frontend/src/components/Masthead.tsx b/makima/frontend/src/components/Masthead.tsx
index afe385e..bc184bd 100644
--- a/makima/frontend/src/components/Masthead.tsx
+++ b/makima/frontend/src/components/Masthead.tsx
@@ -1,6 +1,7 @@
import { Link } from "react-router";
import { LogoMark } from "./Logo";
import { NavStrip } from "./NavStrip";
+import { JapaneseHoverText } from "./JapaneseHoverText";
interface MastheadProps {
showTicker?: boolean;
@@ -18,7 +19,10 @@ export function Masthead({ showTicker = false, showNav = true }: MastheadProps)
makima.jp
- Control System
+
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 642e9a3..48abe09 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -11,6 +11,8 @@ interface NavLink {
const NAV_LINKS: NavLink[] = [
{ label: "Listen", href: "/listen" },
{ label: "Files", href: "/files", requiresAuth: true },
+ { label: "Contracts", href: "/contracts", requiresAuth: true },
+ { label: "Board", href: "/workflow", requiresAuth: true },
{ label: "Mesh", href: "/mesh", requiresAuth: true },
];
diff --git a/makima/frontend/src/components/contracts/ContractCliInput.tsx b/makima/frontend/src/components/contracts/ContractCliInput.tsx
new file mode 100644
index 0000000..821d03c
--- /dev/null
+++ b/makima/frontend/src/components/contracts/ContractCliInput.tsx
@@ -0,0 +1,974 @@
+import { useState, useCallback, useRef, useEffect, useMemo } from "react";
+import {
+ getContractChatHistory,
+ clearContractChatHistory,
+ startTask,
+ sendTaskMessage,
+ type UserQuestion,
+ type ContractWithRelations,
+ type TaskStatus,
+} from "../../lib/api";
+import { SimpleMarkdown } from "../SimpleMarkdown";
+import {
+ QuickActionButtons,
+ type QuickAction,
+} from "./QuickActionButtons";
+import { TaskDerivationPreview, type ParsedTask } from "./TaskDerivationPreview";
+import { useTaskSubscription, type TaskOutputEvent } from "../../hooks/useTaskSubscription";
+
+interface ContractCliInputProps {
+ contractId: string;
+ contract: ContractWithRelations;
+ onUpdate: () => void;
+}
+
+interface Message {
+ id: string;
+ type: "user" | "assistant" | "error" | "question";
+ content: string;
+ toolCalls?: { name: string; success: boolean; message: string }[];
+ questions?: UserQuestion[];
+ quickActions?: QuickAction[];
+}
+
+export function ContractCliInput({ contractId, contract, onUpdate }: ContractCliInputProps) {
+ const [input, setInput] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [historyLoading, setHistoryLoading] = useState(true);
+ const [messages, setMessages] = useState([]);
+ const [expanded, setExpanded] = useState(false);
+ const [fullscreen, setFullscreen] = useState(false);
+ const [pendingQuestions, setPendingQuestions] = useState(null);
+ const [userAnswers, setUserAnswers] = useState
+
+ {/* Connected Daemons */}
+
+
+
+
+ Daemons
+
+ {daemons.length > 0 && (
+
+ ({daemons.filter(d => d.status === "connected").length} connected / {daemons.length} total)
+
+ )}
+
+
+
+
+ {daemonsError && {daemonsError}}
+
+ {daemonsLoading && daemons.length === 0 ? (
+ Loading...
+ ) : daemons.length === 0 ? (
+
+
No daemons connected
+
+ Start a daemon to enable task execution
+
+
+ ) : (
+
+ {daemons.map((daemon) => (
+
+
+
+ {daemon.hostname || "Unknown Host"}
+
+
+ {daemon.status}
+
+
+
+
+ Tasks
+
+ {daemon.currentTaskCount} / {daemon.maxConcurrentTasks}
+
+
+
+ Connected
+
+ {new Date(daemon.connectedAt).toLocaleString()}
+
+
+ {daemon.machineId && (
+
+ Machine
+
+ {daemon.machineId.substring(0, 16)}...
+
+
+ )}
+
+
+ ))}
+
+ )}
+
{/* Right Column */}
diff --git a/makima/frontend/src/routes/workflow.tsx b/makima/frontend/src/routes/workflow.tsx
new file mode 100644
index 0000000..cb72e9e
--- /dev/null
+++ b/makima/frontend/src/routes/workflow.tsx
@@ -0,0 +1,205 @@
+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 } 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 (
+
+ );
+ }
+
+ // Don't render if not authenticated (will redirect)
+ if (isAuthConfigured && !isAuthenticated) {
+ return null;
+ }
+
+ return ;
+}
+
+function WorkflowPageContent() {
+ const navigate = useNavigate();
+ const { contracts, loading, error, changePhase, saveContract } = useContracts();
+ const [statusFilter, setStatusFilter] = useState("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]
+ );
+
+ 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 (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Header with filter and create button */}
+
+
+
+ Board
+
+ {/* Status filter */}
+
+ {(["all", "active", "completed", "archived"] as StatusFilter[]).map(
+ (status) => (
+
+ )
+ )}
+
+
+
+
+
+ {/* Create contract modal */}
+ {isCreating && (
+
+
+
+ Create Contract
+
+
+
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();
+ }}
+ />
+
+
+
+
+
+
+
+ )}
+
+ {/* Board */}
+
+ {loading ? (
+
+ ) : filteredContracts.length === 0 && statusFilter === "all" ? (
+
+
+
+ No contracts yet
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
--
cgit v1.2.3