From 857e717e6343fa5c2ae96664bdc64741d5ba6830 Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 17 May 2026 21:22:34 +0100 Subject: chore: remove LLM module + all dependent surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wholesale removal of the LLM integration layer. ~14,200 LOC deleted across backend and frontend. All chat-driven UIs go with it. ## Backend - Delete `src/llm/` (7,400 LOC): claude/groq clients, contract_tools, contract_evaluator, discuss_tools, mesh_tools, phase_guidance, task_output, templates, markdown round-trip, tools, transcript_analyzer. - Delete handlers wholly dependent on LLM: - `chat.rs` (file-level LLM chat at /files/{id}/chat) - `mesh_chat.rs` (mesh & task LLM chat + history) - `templates.rs` (/contract-types listing) - Strip LLM uses from `mesh_daemon.rs`: - `compute_action_directive` (used phase_guidance::check_deliverables_met to nudge supervisors with "all tasks done" messages). The auto-PR path below still fires when all tasks finish, so no behaviour lost. - `crate::llm::markdown_to_body` → inline 1-line replacement that wraps markdown content in a single BodyElement::Markdown. The editor re-parses on display, so round-trip is preserved. - Drop routes: /files/{id}/chat, /mesh/chat, /mesh/chat/history, /mesh/tasks/{id}/chat, /contract-types. - Drop the matching openapi registrations. ## Frontend - Delete components that were LLM-only: - `mesh/UnifiedMeshChatInput.tsx` - `listen/DiscussContractModal.tsx` - `listen/TranscriptAnalysisPanel.tsx` - `listen/ContractPickerModal.tsx` - `files/CliInput.tsx` - Delete the entire /listen page (its primary value-add was voice → LLM analysis → contract creation; without LLM the page is just a transcript display with no obvious user purpose). - Delete `hooks/useMeshChatHistory.ts` and `lib/listenApi.ts` (transcript-analysis API client to the already-Phase-5-removed listen handlers). - Strip api.ts of LLM exports: LlmModel, ChatMessage/Request/Response, UserQuestion/Answer, chatWithFile, MeshChat* types & functions, getMeshChatHistory, clearMeshChatHistory, chatWithMeshContext, ContractTypeTemplate, listContractTypes, chatWithContract, getContractChatHistory, clearContractChatHistory, discussContract, PhaseDefinition, DeliverableDefinition. - mesh.tsx: drop UnifiedMeshChatInput render + the chatContext memo + handleTaskUpdatedFromCli (only consumer was the input). - files.tsx: drop CliInput render + handleGenerateFromElement + handleBodyUpdate + handleClearFocus + suggestedPrompt state (all CliInput-only). - NavStrip: drop the /listen link. - main.tsx: drop the /listen route. ## Net diff: 37 files changed, 58 insertions, 14,281 deletions. Co-Authored-By: Claude Opus 4.7 (1M context) --- makima/frontend/src/routes/files.tsx | 88 ++--------- makima/frontend/src/routes/listen.tsx | 277 ---------------------------------- makima/frontend/src/routes/mesh.tsx | 37 +---- 3 files changed, 17 insertions(+), 385 deletions(-) delete mode 100644 makima/frontend/src/routes/listen.tsx (limited to 'makima/frontend/src/routes') diff --git a/makima/frontend/src/routes/files.tsx b/makima/frontend/src/routes/files.tsx index b232aa0..5d757a4 100644 --- a/makima/frontend/src/routes/files.tsx +++ b/makima/frontend/src/routes/files.tsx @@ -3,7 +3,6 @@ import { useParams, useNavigate } from "react-router"; import { Masthead } from "../components/Masthead"; import { FileList } from "../components/files/FileList"; import { FileDetail, type FocusedElement } from "../components/files/FileDetail"; -import { CliInput } from "../components/files/CliInput"; import { ConflictNotification } from "../components/files/ConflictNotification"; import { UpdateNotification } from "../components/files/UpdateNotification"; import { useFiles } from "../hooks/useFiles"; @@ -57,7 +56,6 @@ function FilesPageContent() { const [remoteUpdate, setRemoteUpdate] = useState(null); const [remoteFileData, setRemoteFileData] = useState(null); const [focusedElement, setFocusedElement] = useState(null); - const [suggestedPrompt, setSuggestedPrompt] = useState(null); const [createdTask, setCreatedTask] = useState(null); // Contract selection modal state for task creation const [showContractModal, setShowContractModal] = useState(false); @@ -246,18 +244,8 @@ function FilesPageContent() { [editFile, fileDetail, updateHasLocalChanges] ); - const handleBodyUpdate = useCallback( - (body: BodyElement[], summary: string | null) => { - if (fileDetail) { - setFileDetail({ - ...fileDetail, - body, - summary, - }); - } - }, - [fileDetail] - ); + // handleBodyUpdate was the CliInput callback for AI-driven body + // rewrites. CliInput is gone with the LLM module. const handleBodyElementUpdate = useCallback( async (index: number, element: BodyElement) => { @@ -423,9 +411,7 @@ function FilesPageContent() { setFocusedElement(element); }, []); - const handleClearFocus = useCallback(() => { - setFocusedElement(null); - }, []); + // handleClearFocus was passed to CliInput; both gone now. // Convert element to a different type const handleConvertElement = useCallback( @@ -506,55 +492,10 @@ function FilesPageContent() { [fileDetail, id, editFile, updateHasLocalChanges, focusedElement] ); - // Generate from element - focus on it and pre-fill a prompt - const handleGenerateFromElement = useCallback( - (index: number, action: string) => { - if (!fileDetail) return; - - const element = fileDetail.body[index]; - if (!element) return; - - // Get preview text - let preview = ""; - switch (element.type) { - case "heading": - case "paragraph": - preview = element.text.slice(0, 50); - break; - case "code": - preview = element.content.slice(0, 50); - break; - case "list": - preview = element.items[0]?.slice(0, 40) || ""; - break; - default: - preview = "Element"; - } - - // Focus on the element - setFocusedElement({ - index, - type: element.type, - preview: preview + (preview.length >= 50 ? "..." : ""), - }); - - // Set suggested prompt based on action - let prompt = ""; - switch (action) { - case "elaborate": - prompt = "Elaborate and expand on this content"; - break; - case "summarize": - prompt = "Summarize this content"; - break; - case "extract_actions": - prompt = "Extract action items from this content"; - break; - } - setSuggestedPrompt(prompt); - }, - [fileDetail] - ); + // handleGenerateFromElement was an LLM elaborate/summarise/extract + // affordance that piped a suggested prompt into CliInput. Both + // CliInput and the LLM module are gone; the handler + its prop on + // FileDetail are removed. // Create a mesh task from an element - shows contract selection modal const handleCreateTaskFromElement = useCallback( @@ -762,7 +703,6 @@ function FilesPageContent() { onBodyElementDelete={handleBodyElementDelete} onBodyElementDuplicate={handleBodyElementDuplicate} onConvertElement={handleConvertElement} - onGenerateFromElement={handleGenerateFromElement} onCreateTaskFromElement={handleCreateTaskFromElement} onEditingChange={updateIsActivelyEditing} hasPendingRemoteUpdate={!!remoteUpdate} @@ -779,16 +719,10 @@ function FilesPageContent() { onClearVersionSelection={clearSelectedVersion} /> -
- setSuggestedPrompt(null)} - /> -
+ {/* CliInput (file-level LLM chat) removed alongside the LLM + module. The file detail view is now a pure read/edit + surface; AI-driven file editing was the primary value of + this composer and went with the LLM removal. */} ) : id && detailLoading ? (
diff --git a/makima/frontend/src/routes/listen.tsx b/makima/frontend/src/routes/listen.tsx deleted file mode 100644 index a53cbd9..0000000 --- a/makima/frontend/src/routes/listen.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import { useState, useCallback, useMemo, useEffect, useRef } from "react"; -import { Masthead } from "../components/Masthead"; -import { SpeakerPanel } from "../components/listen/SpeakerPanel"; -import { TranscriptPanel } from "../components/listen/TranscriptPanel"; -import { ControlPanel, type ContractOption } from "../components/listen/ControlPanel"; -import { TranscriptAnalysisPanel } from "../components/listen/TranscriptAnalysisPanel"; -import { DiscussContractModal } from "../components/listen/DiscussContractModal"; -import { useMicrophone } from "../hooks/useMicrophone"; -import { useWebSocket } from "../hooks/useWebSocket"; -import { listContracts, type CreatedContractInfo } from "../lib/api"; -import { useAuth } from "../contexts/AuthContext"; - -export default function ListenPage() { - const [isListening, setIsListening] = useState(false); - const [activeSpeaker, setActiveSpeaker] = useState(null); - const [permissionRequested, setPermissionRequested] = useState(false); - const isListeningRef = useRef(false); - - // Contract selection state - const [contracts, setContracts] = useState([]); - const [selectedContractId, setSelectedContractId] = useState(null); - const [contractsLoading, setContractsLoading] = useState(true); - const { session, isAuthenticated } = useAuth(); - - // Saved transcript state for analysis - const [savedTranscript, setSavedTranscript] = useState<{ - fileId: string; - contractId: string; - } | null>(null); - - // Discuss contract modal state - const [isDiscussModalOpen, setIsDiscussModalOpen] = useState(false); - - // Fetch contracts on mount - useEffect(() => { - if (!isAuthenticated) { - setContractsLoading(false); - return; - } - - async function fetchContracts() { - try { - const response = await listContracts(); - setContracts( - response.contracts.map((c) => ({ - id: c.id, - name: c.name, - })) - ); - } catch (err) { - console.error("Failed to fetch contracts:", err); - } finally { - setContractsLoading(false); - } - } - fetchContracts(); - }, [isAuthenticated]); - - // Keep ref in sync with state for use in callbacks - useEffect(() => { - isListeningRef.current = isListening; - }, [isListening]); - - const ws = useWebSocket({ - onTranscript: (transcript) => { - // Track active speaker - if (!transcript.isFinal) { - setActiveSpeaker(transcript.speaker); - } - }, - onStopped: () => { - setIsListening(false); - setActiveSpeaker(null); - }, - onTranscriptSaved: (fileId, contractId) => { - // Store the saved transcript info for analysis - setSavedTranscript({ fileId, contractId }); - }, - }); - - const wsRef = useRef(ws); - useEffect(() => { - wsRef.current = ws; - }, [ws]); - - const handleAudioData = useCallback((samples: Float32Array) => { - if (wsRef.current.isConnected && isListeningRef.current) { - wsRef.current.sendAudio(samples); - } - }, []); - - const mic = useMicrophone({ - sampleRate: 16000, - onAudioData: handleAudioData, - }); - - // Request microphone permission on page load - useEffect(() => { - if (!permissionRequested) { - setPermissionRequested(true); - mic.requestPermission(); - } - }, [permissionRequested, mic.requestPermission]); - - // Derive unique speakers from transcripts - const speakers = useMemo(() => { - const speakerSet = new Set(); - ws.transcripts.forEach((t) => speakerSet.add(t.speaker)); - - return Array.from(speakerSet).map((speaker) => ({ - id: speaker, - label: speaker, - isActive: speaker === activeSpeaker, - })); - }, [ws.transcripts, activeSpeaker]); - - // Clear active speaker after a delay - useEffect(() => { - if (activeSpeaker) { - const timer = setTimeout(() => setActiveSpeaker(null), 1500); - return () => clearTimeout(timer); - } - }, [activeSpeaker]); - - const handleToggle = useCallback(async () => { - if (isListening) { - // Stop listening - mic.stop(); - ws.stopSession("user_stopped"); - setIsListening(false); - setActiveSpeaker(null); - return; - } - - // If permission was denied or errored, try requesting again - if (mic.status === "denied" || mic.status === "error") { - const permitted = await mic.requestPermission(); - if (!permitted) { - return; - } - } - - // Start listening - start the microphone - const micStarted = await mic.start(); - if (!micStarted) { - // Microphone permission denied or error - return; - } - - // Microphone started, now connect to WebSocket - const connected = await ws.connect(); - if (!connected) { - // Connection failed - stop the microphone - mic.stop(); - return; - } - - // Both microphone and WebSocket are ready - start the session - // Pass contract_id and auth token if available - const authToken = session?.access_token || null; - ws.startSession(mic.sampleRate, mic.channels, selectedContractId, authToken); - setIsListening(true); - }, [isListening, mic, ws, selectedContractId, session]); - - const handleNew = useCallback(() => { - // Stop current session - backend auto-saves transcript on disconnect - mic.stop(); - if (ws.isConnected) { - ws.stopSession("new_session"); - } - ws.clearTranscripts(); - ws.disconnect(); - setIsListening(false); - setActiveSpeaker(null); - setSavedTranscript(null); - }, [mic, ws]); - - const handleCloseAnalysis = useCallback(() => { - setSavedTranscript(null); - }, []); - - // Get current transcript context for discussion - const transcriptContext = useMemo(() => { - if (ws.transcripts.length === 0) return undefined; - return ws.transcripts - .map(t => `[${t.speaker}]: ${t.text}`) - .join("\n"); - }, [ws.transcripts]); - - const handleOpenDiscussModal = useCallback(() => { - setIsDiscussModalOpen(true); - }, []); - - const handleContractCreated = useCallback((contract: CreatedContractInfo) => { - // Add to contracts list and select it - setContracts(prev => [ - { id: contract.id, name: contract.name }, - ...prev, - ]); - setSelectedContractId(contract.id); - // Close the modal after a short delay to show success - setTimeout(() => setIsDiscussModalOpen(false), 2000); - }, []); - - const error = ws.error || mic.error; - - return ( -
- - -
- {/* Speaker Panel - top left on desktop, hidden on mobile */} -
- -
- - {/* Transcript Panel - right side, spans 2 rows on desktop */} -
- -
- - {/* Control Panel - bottom left on desktop */} -
- 0} - onToggle={handleToggle} - onNew={handleNew} - error={error} - contracts={contracts} - selectedContractId={selectedContractId} - onContractChange={setSelectedContractId} - onDiscussContract={handleOpenDiscussModal} - contractsLoading={contractsLoading} - connectionStatus={ws.status} - /> -
-
- - {/* Transcript Analysis Panel - shown after recording stops and transcript is saved */} - {savedTranscript && !isListening && ( -
-
- { - // Refresh contracts list and select the new contract - setContracts((prev) => [ - { id: response.contractId, name: response.contractName }, - ...prev, - ]); - setSelectedContractId(response.contractId); - }} - onContractUpdated={() => { - // Keep the current selection - }} - onClose={handleCloseAnalysis} - /> -
-
- )} - - {/* Discuss Contract Modal */} - setIsDiscussModalOpen(false)} - transcriptContext={transcriptContext} - onContractCreated={handleContractCreated} - /> -
- ); -} diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx index f210227..362b2e0 100644 --- a/makima/frontend/src/routes/mesh.tsx +++ b/makima/frontend/src/routes/mesh.tsx @@ -4,11 +4,10 @@ import { Masthead } from "../components/Masthead"; import { TaskList } from "../components/mesh/TaskList"; import { TaskDetail } from "../components/mesh/TaskDetail"; import { TaskOutput } from "../components/mesh/TaskOutput"; -import { UnifiedMeshChatInput } from "../components/mesh/UnifiedMeshChatInput"; import { ContractCompleteQuestion } from "../components/mesh/ContractCompleteQuestion"; import { useTasks } from "../hooks/useTasks"; import { useTaskSubscription, type TaskUpdateEvent, type TaskOutputEvent } from "../hooks/useTaskSubscription"; -import type { TaskWithSubtasks, MeshChatContext, ContractSummary, ContractWithRelations, DaemonDirectory, TaskSummary, RepositoryHistoryEntry } from "../lib/api"; +import type { TaskWithSubtasks, ContractSummary, ContractWithRelations, DaemonDirectory, TaskSummary, RepositoryHistoryEntry } from "../lib/api"; import { startTask as startTaskApi, stopTask as stopTaskApi, getTaskOutput, listContracts, getContract, getDaemonDirectories, continueTask as continueTaskApi, resumeSupervisor, branchTask, getRepositorySuggestions, getTaskDiff } from "../lib/api"; import { DirectoryInput } from "../components/mesh/DirectoryInput"; import { useAuth } from "../contexts/AuthContext"; @@ -650,28 +649,8 @@ export default function MeshPage() { setShowRepoSuggestions(false); }, []); - // Callback when task is updated via CLI - const handleTaskUpdatedFromCli = useCallback(async () => { - if (id) { - const updated = await fetchTask(id); - if (updated) { - setTaskDetail(updated); - } - } - // Also refresh the task list - fetchTasks(); - }, [id, fetchTask, fetchTasks]); - - // Calculate chat context based on current view - const chatContext: MeshChatContext = useMemo(() => { - if (!id) { - return { type: "mesh" }; - } - if (taskDetail?.parentTaskId) { - return { type: "subtask", taskId: id, parentTaskId: taskDetail.parentTaskId }; - } - return { type: "task", taskId: id }; - }, [id, taskDetail?.parentTaskId]); + // handleTaskUpdatedFromCli + chatContext drove the deleted + // UnifiedMeshChatInput; gone with the LLM module. // Handle resizing of the split panel const handleResizeStart = useCallback((e: MouseEvent) => { @@ -904,13 +883,9 @@ export default function MeshPage() {
)} - {/* Mesh Chat Input - always rendered to persist state across navigation */} -
- -
+ {/* UnifiedMeshChatInput removed alongside the LLM module. The + mesh page is now a pure task viewer; spawning / editing + tasks via natural language chat went with LLM removal. */} -- cgit v1.2.3