docs: add mtui to changelog
This commit is contained in:
parent
8232c857dc
commit
6fbe8b2b46
@ -6,6 +6,7 @@ Docs: https://docs.molt.bot
|
|||||||
Status: beta.
|
Status: beta.
|
||||||
|
|
||||||
### Highlights
|
### Highlights
|
||||||
|
- CLI: add modern TUI (mtui) using React and Ink. (#4419) Thanks @M1Vj.
|
||||||
- Rebrand: rename the npm package/CLI to `moltbot`, keep a `moltbot` compatibility shim, move extensions to the `@moltbot/*` scope, and update bot.molt bundle IDs/labels/logging subsystems. Thanks @thewilloftheshadow.
|
- Rebrand: rename the npm package/CLI to `moltbot`, keep a `moltbot` compatibility shim, move extensions to the `@moltbot/*` scope, and update bot.molt bundle IDs/labels/logging subsystems. Thanks @thewilloftheshadow.
|
||||||
- New channels/plugins: Twitch plugin; Google Chat (beta) with Workspace Add-on events + typing indicator. (#1612, #1635) Thanks @tyler6204, @iHildy.
|
- New channels/plugins: Twitch plugin; Google Chat (beta) with Workspace Add-on events + typing indicator. (#1612, #1635) Thanks @tyler6204, @iHildy.
|
||||||
- Security hardening: gateway auth defaults required, hook token query-param deprecation, Windows ACL audits, mDNS minimal discovery, and SSH target option injection fix. (#4001, #2016, #1957, #1882, #2200)
|
- Security hardening: gateway auth defaults required, hook token query-param deprecation, Windows ACL audits, mDNS minimal discovery, and SSH target option injection fix. (#4001, #2016, #1957, #1882, #2200)
|
||||||
|
|||||||
@ -19,27 +19,25 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => {
|
|||||||
const { exit } = useApp();
|
const { exit } = useApp();
|
||||||
const gateway = useGateway();
|
const gateway = useGateway();
|
||||||
const { showThinking, setShowThinking } = useSettings();
|
const { showThinking, setShowThinking } = useSettings();
|
||||||
const [connectionStatus, setConnectionStatus] = useState<
|
const [connectionStatus, setConnectionStatus] = useState<"connecting" | "connected" | "disconnected">("connecting");
|
||||||
"connecting" | "connected" | "disconnected"
|
|
||||||
>("connecting");
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [overlay, setOverlay] = useState<{ type: "model" | "agent"; items: any[] } | null>(null);
|
const [overlay, setOverlay] = useState<{ type: "model" | "agent"; items: any[] } | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
status,
|
status,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
addMessage,
|
addMessage,
|
||||||
sessionInfo,
|
sessionInfo,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
refreshSessionInfo,
|
refreshSessionInfo,
|
||||||
loadHistory,
|
loadHistory
|
||||||
} = useChat(options.session || "main");
|
} = useChat(options.session || "main");
|
||||||
|
|
||||||
const { handleLocalShell, handleSlashCommand } = useCommands(
|
const { handleLocalShell, handleSlashCommand } = useCommands(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
addMessage,
|
addMessage,
|
||||||
refreshSessionInfo,
|
refreshSessionInfo
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -67,12 +65,9 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => {
|
|||||||
} else if (value.startsWith("/")) {
|
} else if (value.startsWith("/")) {
|
||||||
if (value === "/model") {
|
if (value === "/model") {
|
||||||
const models = await gateway.listModels();
|
const models = await gateway.listModels();
|
||||||
setOverlay({
|
setOverlay({
|
||||||
type: "model",
|
type: "model",
|
||||||
items: models.map((m) => ({
|
items: models.map(m => ({ label: `${m.provider}/${m.id}`, value: `${m.provider}/${m.id}` }))
|
||||||
label: `${m.provider}/${m.id}`,
|
|
||||||
value: `${m.provider}/${m.id}`,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await handleSlashCommand(value);
|
await handleSlashCommand(value);
|
||||||
@ -98,48 +93,31 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => {
|
|||||||
total: sessionInfo.totalTokens,
|
total: sessionInfo.totalTokens,
|
||||||
context: sessionInfo.contextTokens,
|
context: sessionInfo.contextTokens,
|
||||||
remaining: (sessionInfo.contextTokens ?? 0) - (sessionInfo.totalTokens ?? 0),
|
remaining: (sessionInfo.contextTokens ?? 0) - (sessionInfo.totalTokens ?? 0),
|
||||||
percent:
|
percent: sessionInfo.totalTokens && sessionInfo.contextTokens ? (sessionInfo.totalTokens / sessionInfo.contextTokens) * 100 : null
|
||||||
sessionInfo.totalTokens && sessionInfo.contextTokens
|
|
||||||
? (sessionInfo.totalTokens / sessionInfo.contextTokens) * 100
|
|
||||||
: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" padding={1} minHeight={20}>
|
<Box flexDirection="column" padding={1} minHeight={20}>
|
||||||
<Box borderStyle="round" borderColor="cyan" paddingX={1} marginBottom={1}>
|
<Box borderStyle="round" borderColor="cyan" paddingX={1} marginBottom={1}>
|
||||||
<Text bold color="white">
|
<Text bold color="white">Moltbot MTUI</Text>
|
||||||
Moltbot MTUI
|
|
||||||
</Text>
|
|
||||||
<Box flexGrow={1} />
|
<Box flexGrow={1} />
|
||||||
<Box paddingX={2}>
|
<Box paddingX={2}>
|
||||||
<Text dimColor>{sessionInfo.model || "no model"}</Text>
|
<Text dimColor>{sessionInfo.model || "no model"}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Text
|
<Text color={connectionStatus === "connected" ? "green" : connectionStatus === "connecting" ? "yellow" : "red"}>
|
||||||
color={
|
|
||||||
connectionStatus === "connected"
|
|
||||||
? "green"
|
|
||||||
: connectionStatus === "connecting"
|
|
||||||
? "yellow"
|
|
||||||
: "red"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{connectionStatus === "connecting" && <Spinner type="dots" />} {connectionStatus}
|
{connectionStatus === "connecting" && <Spinner type="dots" />} {connectionStatus}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{overlay ? (
|
{overlay ? (
|
||||||
<Box flexGrow={1} justifyContent="center" alignItems="center">
|
<Box flexGrow={1} justifyContent="center" alignItems="center">
|
||||||
<Selector
|
<Selector
|
||||||
title={`Select ${overlay.type}`}
|
title={`Select ${overlay.type}`}
|
||||||
items={overlay.items}
|
items={overlay.items}
|
||||||
onSelect={async (item) => {
|
onSelect={async (item) => {
|
||||||
if (overlay.type === "model") {
|
if (overlay.type === "model") {
|
||||||
await gateway.patchSession({ key: sessionKey, model: item.value });
|
await gateway.patchSession({ key: sessionKey, model: item.value });
|
||||||
addMessage({
|
addMessage({ id: Math.random().toString(), role: "system", content: `Model set to ${item.value}` });
|
||||||
id: Math.random().toString(),
|
|
||||||
role: "system",
|
|
||||||
content: `Model set to ${item.value}`,
|
|
||||||
});
|
|
||||||
await refreshSessionInfo();
|
await refreshSessionInfo();
|
||||||
}
|
}
|
||||||
setOverlay(null);
|
setOverlay(null);
|
||||||
@ -159,9 +137,7 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => {
|
|||||||
|
|
||||||
{status === "running" && !overlay && (
|
{status === "running" && !overlay && (
|
||||||
<Box paddingX={1} marginBottom={1}>
|
<Box paddingX={1} marginBottom={1}>
|
||||||
<Text color="yellow">
|
<Text color="yellow"><Spinner type="dots" /> Assistant is thinking...</Text>
|
||||||
<Spinner type="dots" /> Assistant is thinking...
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -178,7 +154,7 @@ const ChatApp: React.FC<{ options: TuiOptions }> = ({ options }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<InputBar onSubmit={handleSubmit} status={status} />
|
<InputBar onSubmit={handleSubmit} status={status} />
|
||||||
|
|
||||||
<Box paddingX={1} marginTop={1}>
|
<Box paddingX={1} marginTop={1}>
|
||||||
<Text dimColor>Ctrl+C exit | Ctrl+T think | /model | /reset | !ls</Text>
|
<Text dimColor>Ctrl+C exit | Ctrl+T think | /model | /reset | !ls</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -19,9 +19,7 @@ export const InputBar: React.FC<InputBarProps> = ({ onSubmit, status }) => {
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Box paddingX={1} borderStyle="single" borderColor={status === "idle" ? "cyan" : "yellow"}>
|
<Box paddingX={1} borderStyle="single" borderColor={status === "idle" ? "cyan" : "yellow"}>
|
||||||
<Text bold color="cyan">
|
<Text bold color="cyan">moltbot {">"} </Text>
|
||||||
moltbot {">"}{" "}
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
|||||||
@ -30,37 +30,25 @@ export const MessageView: React.FC<MessageViewProps> = ({ message }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
{showThinking && (
|
{showThinking && (
|
||||||
<Box paddingLeft={2} borderStyle="single" borderColor="gray">
|
<Box paddingLeft={2} borderStyle="single" borderColor="gray">
|
||||||
<Text italic dimColor>
|
<Text italic dimColor>{message.thinking}</Text>
|
||||||
{message.thinking}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{renderContent(message.content)}
|
{renderContent(message.content)}
|
||||||
|
|
||||||
{message.tools && message.tools.length > 0 && (
|
{message.tools && message.tools.length > 0 && (
|
||||||
<Box flexDirection="column" marginTop={1}>
|
<Box flexDirection="column" marginTop={1}>
|
||||||
{message.tools.map((tool) => (
|
{message.tools.map((tool) => (
|
||||||
<Box
|
<Box key={tool.id} flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1} marginBottom={1}>
|
||||||
key={tool.id}
|
<Text bold color="cyan">Tool: {tool.name}</Text>
|
||||||
flexDirection="column"
|
|
||||||
borderStyle="round"
|
|
||||||
borderColor="gray"
|
|
||||||
paddingX={1}
|
|
||||||
marginBottom={1}
|
|
||||||
>
|
|
||||||
<Text bold color="cyan">
|
|
||||||
Tool: {tool.name}
|
|
||||||
</Text>
|
|
||||||
<Text dimColor>Args: {JSON.stringify(tool.args)}</Text>
|
<Text dimColor>Args: {JSON.stringify(tool.args)}</Text>
|
||||||
{tool.isStreaming && <Text color="yellow">Running...</Text>}
|
{tool.isStreaming && <Text color="yellow">Running...</Text>}
|
||||||
{tool.result && (
|
{tool.result && (
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
<Text color={tool.isError ? "red" : "gray"}>
|
<Text color={tool.isError ? "red" : "gray"}>
|
||||||
Result:{" "}
|
Result: {typeof tool.result === "string" ? tool.result : JSON.stringify(tool.result)}
|
||||||
{typeof tool.result === "string" ? tool.result : JSON.stringify(tool.result)}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -17,10 +17,9 @@ type SelectorProps = {
|
|||||||
export const Selector: React.FC<SelectorProps> = ({ title, items, onSelect, onCancel }) => {
|
export const Selector: React.FC<SelectorProps> = ({ title, items, onSelect, onCancel }) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const filteredItems = items.filter(
|
const filteredItems = items.filter(item =>
|
||||||
(item) =>
|
item.label.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
item.label.toLowerCase().includes(query.toLowerCase()) ||
|
item.value.toLowerCase().includes(query.toLowerCase())
|
||||||
item.value.toLowerCase().includes(query.toLowerCase()),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useInput((input, key) => {
|
useInput((input, key) => {
|
||||||
@ -28,10 +27,10 @@ export const Selector: React.FC<SelectorProps> = ({ title, items, onSelect, onCa
|
|||||||
onCancel();
|
onCancel();
|
||||||
}
|
}
|
||||||
if (!key.ctrl && !key.meta && input.length === 1 && !key.return) {
|
if (!key.ctrl && !key.meta && input.length === 1 && !key.return) {
|
||||||
setQuery((q) => q + input);
|
setQuery(q => q + input);
|
||||||
}
|
}
|
||||||
if (key.backspace || key.delete) {
|
if (key.backspace || key.delete) {
|
||||||
setQuery((q) => q.slice(0, -1));
|
setQuery(q => q.slice(0, -1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import type { TuiOptions } from "../../tui/tui-types.js";
|
|||||||
|
|
||||||
const GatewayContext = createContext<GatewayChatClient | null>(null);
|
const GatewayContext = createContext<GatewayChatClient | null>(null);
|
||||||
|
|
||||||
export const GatewayProvider: React.FC<{ options: TuiOptions; children: React.ReactNode }> = ({
|
export const GatewayProvider: React.FC<{ options: TuiOptions; children: React.ReactNode }> = ({ options, children }) => {
|
||||||
options,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const client = useMemo(() => new GatewayChatClient(options), [options]);
|
const client = useMemo(() => new GatewayChatClient(options), [options]);
|
||||||
|
|
||||||
return <GatewayContext.Provider value={client}>{children}</GatewayContext.Provider>;
|
return (
|
||||||
|
<GatewayContext.Provider value={client}>
|
||||||
|
{children}
|
||||||
|
</GatewayContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGateway = () => {
|
export const useGateway = () => {
|
||||||
|
|||||||
@ -14,9 +14,7 @@ export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
const [toolsExpanded, setToolsExpanded] = useState(false);
|
const [toolsExpanded, setToolsExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContext.Provider
|
<SettingsContext.Provider value={{ showThinking, setShowThinking, toolsExpanded, setToolsExpanded }}>
|
||||||
value={{ showThinking, setShowThinking, toolsExpanded, setToolsExpanded }}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</SettingsContext.Provider>
|
</SettingsContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export const useChat = (initialSessionKey: string) => {
|
|||||||
|
|
||||||
const refreshSessionInfo = useCallback(async () => {
|
const refreshSessionInfo = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const statusRes = (await gateway.getStatus()) as any;
|
const statusRes = await gateway.getStatus() as any;
|
||||||
if (statusRes?.sessions?.recent) {
|
if (statusRes?.sessions?.recent) {
|
||||||
const current = statusRes.sessions.recent.find((s: any) => s.key === sessionKey);
|
const current = statusRes.sessions.recent.find((s: any) => s.key === sessionKey);
|
||||||
if (current) {
|
if (current) {
|
||||||
@ -61,23 +61,15 @@ export const useChat = (initialSessionKey: string) => {
|
|||||||
const last = prev[prev.length - 1];
|
const last = prev[prev.length - 1];
|
||||||
if (last && last.id === payload.runId) {
|
if (last && last.id === payload.runId) {
|
||||||
return [
|
return [
|
||||||
...prev.slice(0, -1),
|
...prev.slice(0, -1),
|
||||||
{
|
{
|
||||||
...last,
|
...last,
|
||||||
content: last.content + (content || ""),
|
content: last.content + (content || ""),
|
||||||
thinking: last.thinking + (thinking || ""),
|
thinking: last.thinking + (thinking || "")
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [...prev, { id: payload.runId, role: "assistant", content: content || "", thinking: thinking || "" }];
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: payload.runId,
|
|
||||||
role: "assistant",
|
|
||||||
content: content || "",
|
|
||||||
thinking: thinking || "",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
setStatus("streaming");
|
setStatus("streaming");
|
||||||
} else if (payload.state === "final") {
|
} else if (payload.state === "final") {
|
||||||
@ -89,25 +81,16 @@ export const useChat = (initialSessionKey: string) => {
|
|||||||
const last = prev[prev.length - 1];
|
const last = prev[prev.length - 1];
|
||||||
if (last && last.id === payload.runId) {
|
if (last && last.id === payload.runId) {
|
||||||
return [
|
return [
|
||||||
...prev.slice(0, -1),
|
...prev.slice(0, -1),
|
||||||
{
|
{
|
||||||
...last,
|
...last,
|
||||||
content: content || last.content,
|
content: content || last.content,
|
||||||
thinking: thinking || last.thinking,
|
thinking: thinking || last.thinking,
|
||||||
isFinal: true,
|
isFinal: true
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [...prev, { id: payload.runId, role: "assistant", content: content || "", thinking: thinking || "", isFinal: true }];
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: payload.runId,
|
|
||||||
role: "assistant",
|
|
||||||
content: content || "",
|
|
||||||
thinking: thinking || "",
|
|
||||||
isFinal: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
setActiveRunId(null);
|
setActiveRunId(null);
|
||||||
setStatus("idle");
|
setStatus("idle");
|
||||||
@ -122,33 +105,22 @@ export const useChat = (initialSessionKey: string) => {
|
|||||||
if (payload.stream === "tool") {
|
if (payload.stream === "tool") {
|
||||||
const data = payload.data as any;
|
const data = payload.data as any;
|
||||||
const { phase, toolCallId, name: toolName } = data;
|
const { phase, toolCallId, name: toolName } = data;
|
||||||
|
|
||||||
setMessages((prev) => {
|
setMessages((prev) => {
|
||||||
const last = prev[prev.length - 1];
|
const last = prev[prev.length - 1];
|
||||||
if (!last || last.role !== "assistant") return prev;
|
if (!last || last.role !== "assistant") return prev;
|
||||||
|
|
||||||
const tools = last.tools || [];
|
const tools = last.tools || [];
|
||||||
let nextTools = [...tools];
|
let nextTools = [...tools];
|
||||||
|
|
||||||
if (phase === "start") {
|
if (phase === "start") {
|
||||||
nextTools.push({
|
nextTools.push({ id: toolCallId, name: toolName, args: data.args, isStreaming: true });
|
||||||
id: toolCallId,
|
|
||||||
name: toolName,
|
|
||||||
args: data.args,
|
|
||||||
isStreaming: true,
|
|
||||||
});
|
|
||||||
} else if (phase === "update") {
|
} else if (phase === "update") {
|
||||||
nextTools = nextTools.map((t) =>
|
nextTools = nextTools.map(t => t.id === toolCallId ? { ...t, result: data.partialResult } : t);
|
||||||
t.id === toolCallId ? { ...t, result: data.partialResult } : t,
|
|
||||||
);
|
|
||||||
} else if (phase === "result") {
|
} else if (phase === "result") {
|
||||||
nextTools = nextTools.map((t) =>
|
nextTools = nextTools.map(t => t.id === toolCallId ? { ...t, result: data.result, isError: data.isError, isStreaming: false } : t);
|
||||||
t.id === toolCallId
|
|
||||||
? { ...t, result: data.result, isError: data.isError, isStreaming: false }
|
|
||||||
: t,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...prev.slice(0, -1), { ...last, tools: nextTools }];
|
return [...prev.slice(0, -1), { ...last, tools: nextTools }];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -164,14 +136,14 @@ export const useChat = (initialSessionKey: string) => {
|
|||||||
|
|
||||||
const loadHistory = useCallback(async () => {
|
const loadHistory = useCallback(async () => {
|
||||||
try {
|
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)) {
|
if (Array.isArray(history?.messages)) {
|
||||||
const msgs = history.messages.map((m: any) => ({
|
const msgs = history.messages.map((m: any) => ({
|
||||||
id: m.id || Math.random().toString(),
|
id: m.id || Math.random().toString(),
|
||||||
role: m.role,
|
role: m.role,
|
||||||
content: extractContentFromMessage(m) || "",
|
content: extractContentFromMessage(m) || "",
|
||||||
thinking: extractThinkingFromMessage(m) || "",
|
thinking: extractThinkingFromMessage(m) || "",
|
||||||
isFinal: true,
|
isFinal: true
|
||||||
}));
|
}));
|
||||||
setMessages(msgs);
|
setMessages(msgs);
|
||||||
}
|
}
|
||||||
@ -180,26 +152,12 @@ export const useChat = (initialSessionKey: string) => {
|
|||||||
}
|
}
|
||||||
}, [gateway, sessionKey]);
|
}, [gateway, sessionKey]);
|
||||||
|
|
||||||
const sendMessage = useCallback(
|
const sendMessage = useCallback(async (text: string) => {
|
||||||
async (text: string) => {
|
addMessage({ id: Math.random().toString(), role: "user", content: text });
|
||||||
addMessage({ id: Math.random().toString(), role: "user", content: text });
|
setStatus("running");
|
||||||
setStatus("running");
|
const { runId } = await gateway.sendChat({ sessionKey, message: text });
|
||||||
const { runId } = await gateway.sendChat({ sessionKey, message: text });
|
setActiveRunId(runId);
|
||||||
setActiveRunId(runId);
|
}, [gateway, sessionKey, addMessage]);
|
||||||
},
|
|
||||||
[gateway, sessionKey, addMessage],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return { messages, status, sendMessage, addMessage, sessionInfo, sessionKey, setSessionKey, refreshSessionInfo, loadHistory, activeRunId };
|
||||||
messages,
|
|
||||||
status,
|
|
||||||
sendMessage,
|
|
||||||
addMessage,
|
|
||||||
sessionInfo,
|
|
||||||
sessionKey,
|
|
||||||
setSessionKey,
|
|
||||||
refreshSessionInfo,
|
|
||||||
loadHistory,
|
|
||||||
activeRunId,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,71 +6,49 @@ import { spawn } from "node:child_process";
|
|||||||
export const useCommands = (
|
export const useCommands = (
|
||||||
sessionKey: string,
|
sessionKey: string,
|
||||||
addMessage: (msg: Message) => void,
|
addMessage: (msg: Message) => void,
|
||||||
refreshSessionInfo: () => Promise<void>,
|
refreshSessionInfo: () => Promise<void>
|
||||||
) => {
|
) => {
|
||||||
const gateway = useGateway();
|
const gateway = useGateway();
|
||||||
|
|
||||||
const handleLocalShell = useCallback(
|
const handleLocalShell = useCallback(async (line: string) => {
|
||||||
async (line: string) => {
|
const cmd = line.slice(1);
|
||||||
const cmd = line.slice(1);
|
addMessage({ id: Math.random().toString(), role: "system", content: `[local] $ ${cmd}` });
|
||||||
addMessage({ id: Math.random().toString(), role: "system", content: `[local] $ ${cmd}` });
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
return new Promise<void>((resolve) => {
|
const child = spawn(cmd, { shell: true });
|
||||||
const child = spawn(cmd, { shell: true });
|
let output = "";
|
||||||
let output = "";
|
|
||||||
|
child.stdout.on("data", (data) => { output += data.toString(); });
|
||||||
child.stdout.on("data", (data) => {
|
child.stderr.on("data", (data) => { output += data.toString(); });
|
||||||
output += data.toString();
|
|
||||||
});
|
child.on("close", (code) => {
|
||||||
child.stderr.on("data", (data) => {
|
addMessage({ id: Math.random().toString(), role: "system", content: output.trim() || `Exit code: ${code}` });
|
||||||
output += data.toString();
|
resolve();
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
|
||||||
addMessage({
|
|
||||||
id: Math.random().toString(),
|
|
||||||
role: "system",
|
|
||||||
content: output.trim() || `Exit code: ${code}`,
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
[addMessage],
|
}, [addMessage]);
|
||||||
);
|
|
||||||
|
|
||||||
const handleSlashCommand = useCallback(
|
const handleSlashCommand = useCallback(async (text: string) => {
|
||||||
async (text: string) => {
|
const parts = text.slice(1).split(" ");
|
||||||
const parts = text.slice(1).split(" ");
|
const command = parts[0];
|
||||||
const command = parts[0];
|
const args = parts.slice(1).join(" ");
|
||||||
const args = parts.slice(1).join(" ");
|
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "reset":
|
case "reset":
|
||||||
await gateway.resetSession(sessionKey);
|
await gateway.resetSession(sessionKey);
|
||||||
addMessage({ id: Math.random().toString(), role: "system", content: "Session reset." });
|
addMessage({ id: Math.random().toString(), role: "system", content: "Session reset." });
|
||||||
break;
|
break;
|
||||||
case "model":
|
case "model":
|
||||||
if (args) {
|
if (args) {
|
||||||
await gateway.patchSession({ key: sessionKey, model: args });
|
await gateway.patchSession({ key: sessionKey, model: args });
|
||||||
addMessage({
|
addMessage({ id: Math.random().toString(), role: "system", content: `Model set to ${args}` });
|
||||||
id: Math.random().toString(),
|
await refreshSessionInfo();
|
||||||
role: "system",
|
}
|
||||||
content: `Model set to ${args}`,
|
break;
|
||||||
});
|
default:
|
||||||
await refreshSessionInfo();
|
addMessage({ id: Math.random().toString(), role: "system", content: `Unknown command: /${command}` });
|
||||||
}
|
}
|
||||||
break;
|
}, [gateway, sessionKey, addMessage, refreshSessionInfo]);
|
||||||
default:
|
|
||||||
addMessage({
|
|
||||||
id: Math.random().toString(),
|
|
||||||
role: "system",
|
|
||||||
content: `Unknown command: /${command}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[gateway, sessionKey, addMessage, refreshSessionInfo],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { handleLocalShell, handleSlashCommand };
|
return { handleLocalShell, handleSlashCommand };
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user