diff options
Diffstat (limited to 'makima/frontend')
| -rw-r--r-- | makima/frontend/src/components/SupervisorQuestionNotification.tsx | 135 | ||||
| -rw-r--r-- | makima/frontend/src/components/listen/ContractPickerModal.tsx | 118 | ||||
| -rw-r--r-- | makima/frontend/src/components/listen/ControlPanel.tsx | 32 | ||||
| -rw-r--r-- | makima/frontend/src/components/mesh/TaskDetail.tsx | 6 | ||||
| -rw-r--r-- | makima/frontend/src/contexts/SupervisorQuestionsContext.tsx | 94 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 125 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 15 | ||||
| -rw-r--r-- | makima/frontend/src/routes/mesh.tsx | 9 | ||||
| -rw-r--r-- | makima/frontend/tsconfig.tsbuildinfo | 1 |
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 |
