diff --git a/src/mtui/App.tsx b/src/mtui/App.tsx index e2d991843..710a576dc 100644 --- a/src/mtui/App.tsx +++ b/src/mtui/App.tsx @@ -19,25 +19,27 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => { const { exit } = useApp(); const gateway = useGateway(); const { showThinking, setShowThinking } = useSettings(); - const [connectionStatus, setConnectionStatus] = useState<"connecting" | "connected" | "disconnected">("connecting"); + const [connectionStatus, setConnectionStatus] = useState< + "connecting" | "connected" | "disconnected" + >("connecting"); const [error, setError] = useState(null); const [overlay, setOverlay] = useState<{ type: "model" | "agent"; items: any[] } | null>(null); - - const { - messages, - status, - sendMessage, - addMessage, - sessionInfo, - sessionKey, + + const { + messages, + status, + sendMessage, + addMessage, + sessionInfo, + sessionKey, refreshSessionInfo, - loadHistory + loadHistory, } = useChat(options.session || "main"); const { handleLocalShell, handleSlashCommand } = useCommands( - sessionKey, - addMessage, - refreshSessionInfo + sessionKey, + addMessage, + refreshSessionInfo, ); useEffect(() => { @@ -65,9 +67,12 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => { } else if (value.startsWith("/")) { if (value === "/model") { const models = await gateway.listModels(); - setOverlay({ - type: "model", - items: models.map(m => ({ label: `${m.provider}/${m.id}`, value: `${m.provider}/${m.id}` })) + setOverlay({ + type: "model", + items: models.map((m) => ({ + label: `${m.provider}/${m.id}`, + value: `${m.provider}/${m.id}`, + })), }); } else { await handleSlashCommand(value); @@ -93,31 +98,48 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => { total: sessionInfo.totalTokens, context: sessionInfo.contextTokens, remaining: (sessionInfo.contextTokens ?? 0) - (sessionInfo.totalTokens ?? 0), - percent: sessionInfo.totalTokens && sessionInfo.contextTokens ? (sessionInfo.totalTokens / sessionInfo.contextTokens) * 100 : null + percent: + sessionInfo.totalTokens && sessionInfo.contextTokens + ? (sessionInfo.totalTokens / sessionInfo.contextTokens) * 100 + : null, }); return ( - Moltbot MTUI + + Moltbot MTUI + {sessionInfo.model || "no model"} - + {connectionStatus === "connecting" && } {connectionStatus} {overlay ? ( - { if (overlay.type === "model") { await gateway.patchSession({ key: sessionKey, model: item.value }); - addMessage({ id: Math.random().toString(), role: "system", content: `Model set to ${item.value}` }); + addMessage({ + id: Math.random().toString(), + role: "system", + content: `Model set to ${item.value}`, + }); await refreshSessionInfo(); } setOverlay(null); @@ -137,7 +159,9 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => { {status === "running" && !overlay && ( - Assistant is thinking... + + Assistant is thinking... + )} @@ -154,7 +178,7 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => { - + Ctrl+C exit | Ctrl+T think | /model | /reset | !ls diff --git a/src/mtui/components/InputBar.tsx b/src/mtui/components/InputBar.tsx index c13bb0da3..66cc6db3a 100644 --- a/src/mtui/components/InputBar.tsx +++ b/src/mtui/components/InputBar.tsx @@ -19,7 +19,9 @@ export const InputBar: React.FC = ({ onSubmit, status }) => { return ( - moltbot {">"} + + moltbot {">"}{" "} + = ({ message }) => { {showThinking && ( - {message.thinking} + + {message.thinking} + )} )} - + {renderContent(message.content)} - + {message.tools && message.tools.length > 0 && ( {message.tools.map((tool) => ( - - Tool: {tool.name} + + + Tool: {tool.name} + Args: {JSON.stringify(tool.args)} {tool.isStreaming && Running...} {tool.result && ( - Result: {typeof tool.result === "string" ? tool.result : JSON.stringify(tool.result)} + Result:{" "} + {typeof tool.result === "string" ? tool.result : JSON.stringify(tool.result)} )} diff --git a/src/mtui/components/Selector.tsx b/src/mtui/components/Selector.tsx index a2d9f0f5d..29f65eef5 100644 --- a/src/mtui/components/Selector.tsx +++ b/src/mtui/components/Selector.tsx @@ -17,9 +17,10 @@ type SelectorProps = { export const Selector: React.FC = ({ title, items, onSelect, onCancel }) => { const [query, setQuery] = useState(""); - const filteredItems = items.filter(item => - item.label.toLowerCase().includes(query.toLowerCase()) || - item.value.toLowerCase().includes(query.toLowerCase()) + const filteredItems = items.filter( + (item) => + item.label.toLowerCase().includes(query.toLowerCase()) || + item.value.toLowerCase().includes(query.toLowerCase()), ); useInput((input, key) => { @@ -27,10 +28,10 @@ export const Selector: React.FC = ({ title, items, onSelect, onCa onCancel(); } if (!key.ctrl && !key.meta && input.length === 1 && !key.return) { - setQuery(q => q + input); + setQuery((q) => q + input); } if (key.backspace || key.delete) { - setQuery(q => q.slice(0, -1)); + setQuery((q) => q.slice(0, -1)); } }); diff --git a/src/mtui/context/GatewayContext.tsx b/src/mtui/context/GatewayContext.tsx index fb502ac28..e36349de5 100644 --- a/src/mtui/context/GatewayContext.tsx +++ b/src/mtui/context/GatewayContext.tsx @@ -4,14 +4,13 @@ import type { TuiOptions } from "../../tui/tui-types.js"; const GatewayContext = createContext(null); -export const GatewayProvider: React.FC<{ options: TuiOptions; children: React.ReactNode }> = ({ options, children }) => { +export const GatewayProvider: React.FC<{ options: TuiOptions; children: React.ReactNode }> = ({ + options, + children, +}) => { const client = useMemo(() => new GatewayChatClient(options), [options]); - - return ( - - {children} - - ); + + return {children}; }; export const useGateway = () => { diff --git a/src/mtui/context/SettingsContext.tsx b/src/mtui/context/SettingsContext.tsx index e99c34e07..82e687673 100644 --- a/src/mtui/context/SettingsContext.tsx +++ b/src/mtui/context/SettingsContext.tsx @@ -14,7 +14,9 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil const [toolsExpanded, setToolsExpanded] = useState(false); return ( - + {children} ); diff --git a/src/mtui/hooks/useChat.ts b/src/mtui/hooks/useChat.ts index 53b74deca..ab8db9de1 100644 --- a/src/mtui/hooks/useChat.ts +++ b/src/mtui/hooks/useChat.ts @@ -32,7 +32,7 @@ export const useChat = (initialSessionKey: string) => { const refreshSessionInfo = useCallback(async () => { try { - const statusRes = await gateway.getStatus() as any; + const statusRes = (await gateway.getStatus()) as any; if (statusRes?.sessions?.recent) { const current = statusRes.sessions.recent.find((s: any) => s.key === sessionKey); if (current) { @@ -61,15 +61,23 @@ export const useChat = (initialSessionKey: string) => { const last = prev[prev.length - 1]; if (last && last.id === payload.runId) { return [ - ...prev.slice(0, -1), - { - ...last, - content: last.content + (content || ""), - thinking: last.thinking + (thinking || "") - } + ...prev.slice(0, -1), + { + ...last, + content: last.content + (content || ""), + thinking: last.thinking + (thinking || ""), + }, ]; } - return [...prev, { id: payload.runId, role: "assistant", content: content || "", thinking: thinking || "" }]; + return [ + ...prev, + { + id: payload.runId, + role: "assistant", + content: content || "", + thinking: thinking || "", + }, + ]; }); setStatus("streaming"); } else if (payload.state === "final") { @@ -81,16 +89,25 @@ export const useChat = (initialSessionKey: string) => { const last = prev[prev.length - 1]; if (last && last.id === payload.runId) { return [ - ...prev.slice(0, -1), - { - ...last, - content: content || last.content, + ...prev.slice(0, -1), + { + ...last, + content: content || last.content, thinking: thinking || last.thinking, - isFinal: true - } + isFinal: true, + }, ]; } - return [...prev, { id: payload.runId, role: "assistant", content: content || "", thinking: thinking || "", isFinal: true }]; + return [ + ...prev, + { + id: payload.runId, + role: "assistant", + content: content || "", + thinking: thinking || "", + isFinal: true, + }, + ]; }); setActiveRunId(null); setStatus("idle"); @@ -105,22 +122,33 @@ export const useChat = (initialSessionKey: string) => { if (payload.stream === "tool") { const data = payload.data as any; const { phase, toolCallId, name: toolName } = data; - + setMessages((prev) => { const last = prev[prev.length - 1]; if (!last || last.role !== "assistant") return prev; - + const tools = last.tools || []; let nextTools = [...tools]; - + if (phase === "start") { - nextTools.push({ id: toolCallId, name: toolName, args: data.args, isStreaming: true }); + nextTools.push({ + id: toolCallId, + name: toolName, + args: data.args, + isStreaming: true, + }); } else if (phase === "update") { - nextTools = nextTools.map(t => t.id === toolCallId ? { ...t, result: data.partialResult } : t); + nextTools = nextTools.map((t) => + t.id === toolCallId ? { ...t, result: data.partialResult } : t, + ); } else if (phase === "result") { - nextTools = nextTools.map(t => t.id === toolCallId ? { ...t, result: data.result, isError: data.isError, isStreaming: false } : t); + nextTools = nextTools.map((t) => + t.id === toolCallId + ? { ...t, result: data.result, isError: data.isError, isStreaming: false } + : t, + ); } - + return [...prev.slice(0, -1), { ...last, tools: nextTools }]; }); } @@ -136,14 +164,14 @@ export const useChat = (initialSessionKey: string) => { const loadHistory = useCallback(async () => { try { - const history = await gateway.loadHistory({ sessionKey, limit: 100 }) as any; + const history = (await gateway.loadHistory({ sessionKey, limit: 100 })) as any; if (Array.isArray(history?.messages)) { const msgs = history.messages.map((m: any) => ({ id: m.id || Math.random().toString(), role: m.role, content: extractContentFromMessage(m) || "", thinking: extractThinkingFromMessage(m) || "", - isFinal: true + isFinal: true, })); setMessages(msgs); } @@ -152,12 +180,26 @@ export const useChat = (initialSessionKey: string) => { } }, [gateway, sessionKey]); - const sendMessage = useCallback(async (text: string) => { - addMessage({ id: Math.random().toString(), role: "user", content: text }); - setStatus("running"); - const { runId } = await gateway.sendChat({ sessionKey, message: text }); - setActiveRunId(runId); - }, [gateway, sessionKey, addMessage]); + const sendMessage = useCallback( + async (text: string) => { + addMessage({ id: Math.random().toString(), role: "user", content: text }); + setStatus("running"); + const { runId } = await gateway.sendChat({ sessionKey, message: text }); + setActiveRunId(runId); + }, + [gateway, sessionKey, addMessage], + ); - return { messages, status, sendMessage, addMessage, sessionInfo, sessionKey, setSessionKey, refreshSessionInfo, loadHistory, activeRunId }; + return { + messages, + status, + sendMessage, + addMessage, + sessionInfo, + sessionKey, + setSessionKey, + refreshSessionInfo, + loadHistory, + activeRunId, + }; }; diff --git a/src/mtui/hooks/useCommands.ts b/src/mtui/hooks/useCommands.ts index 34fcdeb17..d3e32e49c 100644 --- a/src/mtui/hooks/useCommands.ts +++ b/src/mtui/hooks/useCommands.ts @@ -6,49 +6,71 @@ import { spawn } from "node:child_process"; export const useCommands = ( sessionKey: string, addMessage: (msg: Message) => void, - refreshSessionInfo: () => Promise + refreshSessionInfo: () => Promise, ) => { const gateway = useGateway(); - const handleLocalShell = useCallback(async (line: string) => { - const cmd = line.slice(1); - addMessage({ id: Math.random().toString(), role: "system", content: `[local] $ ${cmd}` }); - - return new Promise((resolve) => { - const child = spawn(cmd, { shell: true }); - let output = ""; - - child.stdout.on("data", (data) => { output += data.toString(); }); - child.stderr.on("data", (data) => { output += data.toString(); }); - - child.on("close", (code) => { - addMessage({ id: Math.random().toString(), role: "system", content: output.trim() || `Exit code: ${code}` }); - resolve(); + const handleLocalShell = useCallback( + async (line: string) => { + const cmd = line.slice(1); + addMessage({ id: Math.random().toString(), role: "system", content: `[local] $ ${cmd}` }); + + return new Promise((resolve) => { + const child = spawn(cmd, { shell: true }); + let output = ""; + + child.stdout.on("data", (data) => { + output += data.toString(); + }); + child.stderr.on("data", (data) => { + output += data.toString(); + }); + + child.on("close", (code) => { + addMessage({ + id: Math.random().toString(), + role: "system", + content: output.trim() || `Exit code: ${code}`, + }); + resolve(); + }); }); - }); - }, [addMessage]); + }, + [addMessage], + ); - const handleSlashCommand = useCallback(async (text: string) => { - const parts = text.slice(1).split(" "); - const command = parts[0]; - const args = parts.slice(1).join(" "); + const handleSlashCommand = useCallback( + async (text: string) => { + const parts = text.slice(1).split(" "); + const command = parts[0]; + const args = parts.slice(1).join(" "); - switch (command) { - case "reset": - await gateway.resetSession(sessionKey); - addMessage({ id: Math.random().toString(), role: "system", content: "Session reset." }); - break; - case "model": - if (args) { - await gateway.patchSession({ key: sessionKey, model: args }); - addMessage({ id: Math.random().toString(), role: "system", content: `Model set to ${args}` }); - await refreshSessionInfo(); - } - break; - default: - addMessage({ id: Math.random().toString(), role: "system", content: `Unknown command: /${command}` }); - } - }, [gateway, sessionKey, addMessage, refreshSessionInfo]); + switch (command) { + case "reset": + await gateway.resetSession(sessionKey); + addMessage({ id: Math.random().toString(), role: "system", content: "Session reset." }); + break; + case "model": + if (args) { + await gateway.patchSession({ key: sessionKey, model: args }); + addMessage({ + id: Math.random().toString(), + role: "system", + content: `Model set to ${args}`, + }); + await refreshSessionInfo(); + } + break; + default: + addMessage({ + id: Math.random().toString(), + role: "system", + content: `Unknown command: /${command}`, + }); + } + }, + [gateway, sessionKey, addMessage, refreshSessionInfo], + ); return { handleLocalShell, handleSlashCommand }; };