summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-15 03:26:28 +0000
committersoryu <soryu@soryu.co>2026-01-15 03:26:28 +0000
commiteeafe072bc6bb81459f7d087b48fc921afe9cc11 (patch)
tree7f835993edd732f8ff66d756391dedffe3d44e90 /makima/frontend
parentc61a2b9b9c988f5460f85980d4ddf285f1a730b5 (diff)
downloadsoryu-eeafe072bc6bb81459f7d087b48fc921afe9cc11.tar.gz
soryu-eeafe072bc6bb81459f7d087b48fc921afe9cc11.zip
Automatically derive repo URL and add notifications for input
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/components/SupervisorQuestionNotification.tsx135
-rw-r--r--makima/frontend/src/components/listen/ContractPickerModal.tsx118
-rw-r--r--makima/frontend/src/components/listen/ControlPanel.tsx32
-rw-r--r--makima/frontend/src/components/mesh/TaskDetail.tsx6
-rw-r--r--makima/frontend/src/contexts/SupervisorQuestionsContext.tsx94
-rw-r--r--makima/frontend/src/lib/api.ts125
-rw-r--r--makima/frontend/src/main.tsx15
-rw-r--r--makima/frontend/src/routes/mesh.tsx9
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo1
9 files changed, 513 insertions, 22 deletions
diff --git a/makima/frontend/src/components/SupervisorQuestionNotification.tsx b/makima/frontend/src/components/SupervisorQuestionNotification.tsx
new file mode 100644
index 0000000..6a71de2
--- /dev/null
+++ b/makima/frontend/src/components/SupervisorQuestionNotification.tsx
@@ -0,0 +1,135 @@
+import { useState } from "react";
+import { useNavigate } from "react-router";
+import { useSupervisorQuestions } from "../contexts/SupervisorQuestionsContext";
+import type { PendingQuestion } from "../lib/api";
+
+export function SupervisorQuestionNotification() {
+ const navigate = useNavigate();
+ const { pendingQuestions, submitAnswer } = useSupervisorQuestions();
+ const [expandedQuestion, setExpandedQuestion] = useState<string | null>(null);
+ const [response, setResponse] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ if (pendingQuestions.length === 0) {
+ return null;
+ }
+
+ const handleGoToTask = (taskId: string) => {
+ navigate(`/mesh/${taskId}`);
+ };
+
+ const handleExpand = (questionId: string) => {
+ setExpandedQuestion(expandedQuestion === questionId ? null : questionId);
+ setResponse("");
+ };
+
+ const handleSubmit = async (question: PendingQuestion) => {
+ if (!response.trim()) return;
+
+ setSubmitting(true);
+ const success = await submitAnswer(question.questionId, response.trim());
+ setSubmitting(false);
+
+ if (success) {
+ setExpandedQuestion(null);
+ setResponse("");
+ }
+ };
+
+ const handleChoiceSelect = async (question: PendingQuestion, choice: string) => {
+ setSubmitting(true);
+ await submitAnswer(question.questionId, choice);
+ setSubmitting(false);
+ };
+
+ return (
+ <div className="fixed bottom-4 right-4 z-50 max-w-md space-y-2">
+ {pendingQuestions.map((question) => (
+ <div
+ key={question.questionId}
+ className="bg-[#0d1b2d] border border-amber-500/50 rounded-lg shadow-lg overflow-hidden"
+ >
+ {/* Header */}
+ <div className="flex items-center justify-between px-4 py-3 bg-amber-900/30">
+ <div className="flex items-center gap-2">
+ <span className="text-amber-400 text-lg">?</span>
+ <span className="font-mono text-sm text-amber-300 uppercase">
+ Supervisor Question
+ </span>
+ </div>
+ <div className="flex items-center gap-2">
+ <button
+ onClick={() => handleGoToTask(question.taskId)}
+ className="px-2 py-1 font-mono text-xs text-amber-400 hover:text-amber-300 transition-colors"
+ title="Go to task"
+ >
+ View Task
+ </button>
+ <button
+ onClick={() => handleExpand(question.questionId)}
+ className="px-2 py-1 font-mono text-xs text-amber-400 border border-amber-500/30 hover:border-amber-400/50 transition-colors uppercase"
+ >
+ {expandedQuestion === question.questionId ? "Collapse" : "Answer"}
+ </button>
+ </div>
+ </div>
+
+ {/* Question preview */}
+ <div className="px-4 py-3">
+ {question.context && (
+ <div className="text-xs text-[#8b949e] font-mono mb-1 uppercase">
+ {question.context}
+ </div>
+ )}
+ <p className="text-sm text-[#dbe7ff] font-mono">
+ {question.question}
+ </p>
+ </div>
+
+ {/* Expanded answer section */}
+ {expandedQuestion === question.questionId && (
+ <div className="px-4 pb-4 border-t border-amber-500/20 pt-3">
+ {question.choices.length > 0 ? (
+ // Choice buttons
+ <div className="space-y-2">
+ <p className="text-xs text-[#8b949e] font-mono uppercase mb-2">
+ Select an option:
+ </p>
+ {question.choices.map((choice, idx) => (
+ <button
+ key={idx}
+ onClick={() => handleChoiceSelect(question, choice)}
+ disabled={submitting}
+ className="w-full px-3 py-2 text-left font-mono text-sm text-[#dbe7ff] bg-[#0a1628] border border-[#3f6fb3] hover:border-amber-400/50 hover:bg-amber-900/20 disabled:opacity-50 transition-colors"
+ >
+ {choice}
+ </button>
+ ))}
+ </div>
+ ) : (
+ // Free-form text input
+ <div className="space-y-2">
+ <textarea
+ value={response}
+ onChange={(e) => setResponse(e.target.value)}
+ placeholder="Type your response..."
+ rows={3}
+ className="w-full px-3 py-2 bg-[#0a1628] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-amber-400 resize-none"
+ disabled={submitting}
+ />
+ <button
+ onClick={() => handleSubmit(question)}
+ disabled={submitting || !response.trim()}
+ className="w-full px-4 py-2 font-mono text-xs text-[#0a1628] bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed transition-colors uppercase"
+ >
+ {submitting ? "Submitting..." : "Submit Response"}
+ </button>
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ ))}
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/listen/ContractPickerModal.tsx b/makima/frontend/src/components/listen/ContractPickerModal.tsx
new file mode 100644
index 0000000..961ccba
--- /dev/null
+++ b/makima/frontend/src/components/listen/ContractPickerModal.tsx
@@ -0,0 +1,118 @@
+import { useEffect, useRef } from "react";
+import type { ContractOption } from "./ControlPanel";
+
+interface ContractPickerModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ contracts: ContractOption[];
+ selectedContractId: string | null;
+ onSelect: (contractId: string | null) => void;
+ loading?: boolean;
+}
+
+export function ContractPickerModal({
+ isOpen,
+ onClose,
+ contracts,
+ selectedContractId,
+ onSelect,
+ loading,
+}: ContractPickerModalProps) {
+ const modalRef = useRef<HTMLDivElement>(null);
+
+ useEffect(() => {
+ if (!isOpen) return;
+
+ function handleKeyDown(e: KeyboardEvent) {
+ if (e.key === "Escape") {
+ onClose();
+ }
+ }
+
+ function handleClickOutside(e: MouseEvent) {
+ if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
+ onClose();
+ }
+ }
+
+ document.addEventListener("keydown", handleKeyDown);
+ document.addEventListener("mousedown", handleClickOutside);
+
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [isOpen, onClose]);
+
+ if (!isOpen) return null;
+
+ const handleSelect = (contractId: string | null) => {
+ onSelect(contractId);
+ onClose();
+ };
+
+ return (
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
+ <div
+ ref={modalRef}
+ className="panel p-4 w-[300px] max-h-[400px] flex flex-col gap-3"
+ >
+ <div className="flex items-center justify-between">
+ <h2 className="font-mono text-sm text-[#dbe7ff] uppercase tracking-wide">
+ Select Contract
+ </h2>
+ <button
+ onClick={onClose}
+ className="font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors"
+ >
+ [X]
+ </button>
+ </div>
+
+ <div className="flex-1 overflow-y-auto flex flex-col gap-1 min-h-0">
+ {loading ? (
+ <div className="font-mono text-xs text-[#9bc3ff] text-center py-4">
+ Loading...
+ </div>
+ ) : (
+ <>
+ <button
+ onClick={() => handleSelect(null)}
+ className={`w-full text-left px-3 py-2 font-mono text-xs border transition-colors ${
+ selectedContractId === null
+ ? "bg-[#0f3c78]/50 border-[#3f6fb3] text-[#dbe7ff]"
+ : "bg-[#0d1b2d] border-[#0f3c78] text-[#9bc3ff] hover:border-[#3f6fb3] hover:text-[#dbe7ff]"
+ }`}
+ >
+ <span className="uppercase tracking-wide">Ephemeral</span>
+ <span className="block text-[10px] text-[#75aafc] mt-0.5">
+ Transcript not saved
+ </span>
+ </button>
+
+ {contracts.map((contract) => (
+ <button
+ key={contract.id}
+ onClick={() => handleSelect(contract.id)}
+ className={`w-full text-left px-3 py-2 font-mono text-xs border transition-colors ${
+ selectedContractId === contract.id
+ ? "bg-[#0f3c78]/50 border-[#3f6fb3] text-[#dbe7ff]"
+ : "bg-[#0d1b2d] border-[#0f3c78] text-[#9bc3ff] hover:border-[#3f6fb3] hover:text-[#dbe7ff]"
+ }`}
+ >
+ <span className="block truncate">{contract.name}</span>
+ </button>
+ ))}
+
+ {contracts.length === 0 && (
+ <div className="font-mono text-xs text-[#9bc3ff] text-center py-4">
+ No contracts available
+ </div>
+ )}
+ </>
+ )}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/components/listen/ControlPanel.tsx b/makima/frontend/src/components/listen/ControlPanel.tsx
index 35834d4..f0e5702 100644
--- a/makima/frontend/src/components/listen/ControlPanel.tsx
+++ b/makima/frontend/src/components/listen/ControlPanel.tsx
@@ -1,5 +1,7 @@
+import { useState } from "react";
import { Logo } from "../Logo";
import type { MicrophoneStatus } from "../../hooks/useMicrophone";
+import { ContractPickerModal } from "./ContractPickerModal";
export interface ContractOption {
id: string;
@@ -53,9 +55,12 @@ export function ControlPanel({
onContractChange,
contractsLoading,
}: ControlPanelProps) {
+ const [isModalOpen, setIsModalOpen] = useState(false);
const statusText = getStatusText(isListening, micStatus);
const isRequesting = micStatus === "requesting";
+ const selectedContract = contracts.find((c) => c.id === selectedContractId);
+
return (
<div className="panel p-4 flex flex-col items-center justify-center gap-3">
{/* Logo button */}
@@ -147,21 +152,24 @@ export function ControlPanel({
>
New
</button>
- <select
- value={selectedContractId || ""}
- onChange={(e) => onContractChange(e.target.value || null)}
+ <button
+ onClick={() => setIsModalOpen(true)}
disabled={isListening || contractsLoading}
- className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#0f3c78] focus:border-[#3f6fb3] transition-colors disabled:opacity-50 disabled:cursor-not-allowed uppercase tracking-wide"
- title={selectedContractId ? "Saving to selected contract" : "Transcript not saved"}
+ className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0d1b2d] border border-[#0f3c78] hover:border-[#3f6fb3] transition-colors disabled:opacity-50 disabled:cursor-not-allowed uppercase tracking-wide"
+ title={selectedContract ? `Saving to: ${selectedContract.name}` : "Transcript not saved"}
>
- <option value="">Ephemeral Transcript</option>
- {contracts.map((contract) => (
- <option key={contract.id} value={contract.id}>
- {contract.name}
- </option>
- ))}
- </select>
+ {selectedContract ? "Contract" : "Ephemeral"}
+ </button>
</div>
+
+ <ContractPickerModal
+ isOpen={isModalOpen}
+ onClose={() => setIsModalOpen(false)}
+ contracts={contracts}
+ selectedContractId={selectedContractId}
+ onSelect={onContractChange}
+ loading={contractsLoading}
+ />
</div>
);
}
diff --git a/makima/frontend/src/components/mesh/TaskDetail.tsx b/makima/frontend/src/components/mesh/TaskDetail.tsx
index 967b1d1..8e853e7 100644
--- a/makima/frontend/src/components/mesh/TaskDetail.tsx
+++ b/makima/frontend/src/components/mesh/TaskDetail.tsx
@@ -144,6 +144,10 @@ export function TaskDetail({
const isTaskRunning = task.status === "running" || task.status === "initializing" || task.status === "starting";
// Check if task is in a terminal state (can be continued/reopened)
const isTaskTerminal = task.status === "done" || task.status === "failed" || task.status === "merged";
+ // Check if this is a supervisor task
+ const isSupervisor = task.isSupervisor === true;
+ // Show continue for supervisors (always) or terminal states for other tasks
+ const canContinue = isSupervisor || isTaskTerminal;
// Calculate subtask statistics
const subtaskStats = useMemo(
@@ -356,7 +360,7 @@ export function TaskDetail({
)}
</div>
)}
- {isTaskTerminal && (
+ {canContinue && (
<button
onClick={() => onContinue(task.id)}
className="px-3 py-1 font-mono text-xs text-cyan-400 border border-cyan-400/30 hover:border-cyan-400/50 hover:bg-cyan-400/10 transition-colors uppercase flex items-center gap-1"
diff --git a/makima/frontend/src/contexts/SupervisorQuestionsContext.tsx b/makima/frontend/src/contexts/SupervisorQuestionsContext.tsx
new file mode 100644
index 0000000..aa1bb12
--- /dev/null
+++ b/makima/frontend/src/contexts/SupervisorQuestionsContext.tsx
@@ -0,0 +1,94 @@
+import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from "react";
+import { listPendingQuestions, answerQuestion, type PendingQuestion } from "../lib/api";
+import { useAuth } from "./AuthContext";
+
+interface SupervisorQuestionsContextValue {
+ pendingQuestions: PendingQuestion[];
+ loading: boolean;
+ error: string | null;
+ refreshQuestions: () => Promise<void>;
+ submitAnswer: (questionId: string, response: string) => Promise<boolean>;
+}
+
+const SupervisorQuestionsContext = createContext<SupervisorQuestionsContextValue | null>(null);
+
+export function SupervisorQuestionsProvider({ children }: { children: ReactNode }) {
+ const { isAuthenticated } = useAuth();
+ const [pendingQuestions, setPendingQuestions] = useState<PendingQuestion[]>([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<string | null>(null);
+
+ const refreshQuestions = useCallback(async () => {
+ if (!isAuthenticated) return;
+
+ try {
+ setLoading(true);
+ const questions = await listPendingQuestions();
+ if (questions.length > 0) {
+ console.log("[SupervisorQuestions] Received questions:", questions);
+ }
+ setPendingQuestions(questions);
+ setError(null);
+ } catch (err) {
+ // Log but don't spam
+ console.warn("[SupervisorQuestions] Failed to fetch:", err);
+ setError(err instanceof Error ? err.message : "Failed to load questions");
+ } finally {
+ setLoading(false);
+ }
+ }, [isAuthenticated]);
+
+ const submitAnswer = useCallback(async (questionId: string, response: string): Promise<boolean> => {
+ try {
+ const result = await answerQuestion(questionId, response);
+ if (result.success) {
+ // Remove the question from local state
+ setPendingQuestions(prev => prev.filter(q => q.questionId !== questionId));
+ }
+ return result.success;
+ } catch (err) {
+ console.error("Failed to submit answer:", err);
+ return false;
+ }
+ }, []);
+
+ // Poll for questions every 5 seconds when authenticated
+ useEffect(() => {
+ if (!isAuthenticated) {
+ setPendingQuestions([]);
+ return;
+ }
+
+ // Initial fetch (delayed slightly to ensure auth is ready)
+ const initialTimeout = setTimeout(refreshQuestions, 500);
+
+ // Poll periodically (every 5 seconds for responsiveness)
+ const interval = setInterval(refreshQuestions, 5000);
+ return () => {
+ clearTimeout(initialTimeout);
+ clearInterval(interval);
+ };
+ }, [isAuthenticated, refreshQuestions]);
+
+ return (
+ <SupervisorQuestionsContext.Provider
+ value={{
+ pendingQuestions,
+ loading,
+ error,
+ refreshQuestions,
+ submitAnswer,
+ }}
+ >
+ {children}
+ </SupervisorQuestionsContext.Provider>
+ );
+}
+
+export function useSupervisorQuestions() {
+ const context = useContext(SupervisorQuestionsContext);
+ if (!context) {
+ throw new Error("useSupervisorQuestions must be used within SupervisorQuestionsProvider");
+ }
+ return context;
+}
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index d7ac8b6..2ea1128 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -590,6 +590,9 @@ export interface Task {
version: number;
createdAt: string;
updatedAt: string;
+
+ // Supervisor flag
+ isSupervisor: boolean;
}
export interface TaskWithSubtasks extends Task {
@@ -892,6 +895,75 @@ export async function checkTargetExists(
return res.json();
}
+// =============================================================================
+// Task Recovery (Daemon Failover)
+// =============================================================================
+
+/** Request to reassign a task to a new daemon */
+export interface ReassignTaskRequest {
+ targetDaemonId?: string;
+ includeContext?: boolean;
+}
+
+/** Response from reassigning a task */
+export interface ReassignTaskResponse {
+ task: Task;
+ daemonId: string;
+ oldTaskId: string;
+ contextIncluded: boolean;
+ contextEntries: number;
+}
+
+/**
+ * Reassign a task to a new daemon after daemon disconnect.
+ * Creates a new task with conversation context, deletes the old one.
+ */
+export async function reassignTask(
+ taskId: string,
+ options?: ReassignTaskRequest
+): Promise<ReassignTaskResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/reassign`, {
+ method: "POST",
+ body: JSON.stringify(options || {}),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to reassign task: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Request to continue a task */
+export interface ContinueTaskRequest {
+ targetDaemonId?: string;
+}
+
+/** Response from continuing a task */
+export interface ContinueTaskResponse {
+ task: Task;
+ daemonId: string;
+ contextEntries: number;
+}
+
+/**
+ * Continue a task after daemon disconnect by restarting it with conversation context.
+ * Unlike reassign, this keeps the same task ID.
+ */
+export async function continueTask(
+ taskId: string,
+ options?: ContinueTaskRequest
+): Promise<ContinueTaskResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/continue`, {
+ method: "POST",
+ body: JSON.stringify(options || {}),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to continue task: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
export async function listSubtasks(taskId: string): Promise<TaskListResponse> {
const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/subtasks`);
if (!res.ok) {
@@ -1848,3 +1920,56 @@ export async function getTemplate(id: string): Promise<FileTemplate> {
}
return res.json();
}
+
+// =============================================================================
+// Supervisor Question Types and Functions
+// =============================================================================
+
+export interface PendingQuestion {
+ questionId: string;
+ taskId: string;
+ contractId: string;
+ question: string;
+ choices: string[];
+ context: string | null;
+ createdAt: string;
+}
+
+export interface AnswerQuestionRequest {
+ response: string;
+}
+
+export interface AnswerQuestionResponse {
+ success: boolean;
+}
+
+/**
+ * Get all pending supervisor questions for the current user.
+ */
+export async function listPendingQuestions(): Promise<PendingQuestion[]> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/questions`);
+ if (!res.ok) {
+ throw new Error(`Failed to list questions: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Answer a pending supervisor question.
+ */
+export async function answerQuestion(
+ questionId: string,
+ response: string
+): Promise<AnswerQuestionResponse> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/mesh/questions/${questionId}/answer`,
+ {
+ method: "POST",
+ body: JSON.stringify({ response }),
+ }
+ );
+ if (!res.ok) {
+ throw new Error(`Failed to answer question: ${res.statusText}`);
+ }
+ return res.json();
+}
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index 496a569..5d389fc 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -3,7 +3,9 @@ import { createRoot } from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router";
import "./index.css";
import { AuthProvider } from "./contexts/AuthContext";
+import { SupervisorQuestionsProvider } from "./contexts/SupervisorQuestionsContext";
import { GridOverlay } from "./components/GridOverlay";
+import { SupervisorQuestionNotification } from "./components/SupervisorQuestionNotification";
import { ProtectedRoute } from "./components/ProtectedRoute";
import HomePage from "./routes/_index";
import ListenPage from "./routes/listen";
@@ -17,9 +19,11 @@ import SettingsPage from "./routes/settings";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AuthProvider>
- <BrowserRouter>
- <GridOverlay />
- <Routes>
+ <SupervisorQuestionsProvider>
+ <BrowserRouter>
+ <GridOverlay />
+ <SupervisorQuestionNotification />
+ <Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route
@@ -94,8 +98,9 @@ createRoot(document.getElementById("root")!).render(
</ProtectedRoute>
}
/>
- </Routes>
- </BrowserRouter>
+ </Routes>
+ </BrowserRouter>
+ </SupervisorQuestionsProvider>
</AuthProvider>
</StrictMode>
);
diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx
index d067865..ed5a6d0 100644
--- a/makima/frontend/src/routes/mesh.tsx
+++ b/makima/frontend/src/routes/mesh.tsx
@@ -8,7 +8,7 @@ import { UnifiedMeshChatInput } from "../components/mesh/UnifiedMeshChatInput";
import { useTasks } from "../hooks/useTasks";
import { useTaskSubscription, type TaskUpdateEvent, type TaskOutputEvent } from "../hooks/useTaskSubscription";
import type { TaskWithSubtasks, MeshChatContext, ContractSummary, ContractWithRelations, DaemonDirectory } from "../lib/api";
-import { startTask as startTaskApi, stopTask as stopTaskApi, getTaskOutput, listContracts, getContract, getDaemonDirectories } from "../lib/api";
+import { startTask as startTaskApi, stopTask as stopTaskApi, getTaskOutput, listContracts, getContract, getDaemonDirectories, continueTask as continueTaskApi } from "../lib/api";
import { DirectoryInput } from "../components/mesh/DirectoryInput";
import { useAuth } from "../contexts/AuthContext";
@@ -374,9 +374,10 @@ export default function MeshPage() {
const handleContinue = useCallback(
async (taskId: string) => {
try {
- // Start the task again from terminal state
- const updated = await startTaskApi(taskId);
- setTaskDetail((prev) => prev ? { ...prev, ...updated } : prev);
+ // Continue the task with conversation context from previous run
+ const result = await continueTaskApi(taskId);
+ console.log(`[Mesh] Task continued with ${result.contextEntries} context entries`);
+ setTaskDetail((prev) => prev ? { ...prev, ...result.task } : prev);
} catch (e) {
console.error("Failed to continue task:", e);
alert(e instanceof Error ? e.message : "Failed to continue task");
diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo
new file mode 100644
index 0000000..7af14b5
--- /dev/null
+++ b/makima/frontend/tsconfig.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file