huskies: merge 473_refactor_split_chat_tsx_into_smaller_components
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
import * as React from "react";
|
||||
import { api, type ChatWebSocket } from "../api/client";
|
||||
import type { ChatInputHandle } from "../components/ChatInput";
|
||||
import type { Message, ProviderConfig } from "../types";
|
||||
|
||||
const { useCallback, useRef, useState } = React;
|
||||
|
||||
type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||
|
||||
interface UseChatSendParams {
|
||||
messages: Message[];
|
||||
loading: boolean;
|
||||
model: string;
|
||||
enableTools: boolean;
|
||||
claudeSessionId: string | null;
|
||||
streamingContent: string;
|
||||
setClaudeSessionId: SetState<string | null>;
|
||||
setMessages: SetState<Message[]>;
|
||||
setLoading: SetState<boolean>;
|
||||
setStreamingContent: SetState<string>;
|
||||
setStreamingThinking: SetState<string>;
|
||||
setActivityStatus: SetState<string | null>;
|
||||
setSideQuestion: SetState<{
|
||||
question: string;
|
||||
response: string;
|
||||
loading: boolean;
|
||||
} | null>;
|
||||
chatInputRef: React.RefObject<ChatInputHandle | null>;
|
||||
wsRef: React.MutableRefObject<ChatWebSocket | null>;
|
||||
queuedMessagesRef: React.MutableRefObject<{ id: string; text: string }[]>;
|
||||
setQueuedMessages: SetState<{ id: string; text: string }[]>;
|
||||
queueIdCounterRef: React.MutableRefObject<number>;
|
||||
clearMessages: () => void;
|
||||
projectPath: string;
|
||||
}
|
||||
|
||||
export interface UseChatSendResult {
|
||||
sendMessage: (text: string) => Promise<void>;
|
||||
sendMessageBatch: (texts: string[]) => Promise<void>;
|
||||
cancelGeneration: () => Promise<void>;
|
||||
handleSaveApiKey: () => Promise<void>;
|
||||
clearSession: () => Promise<void>;
|
||||
showApiKeyDialog: boolean;
|
||||
setShowApiKeyDialog: SetState<boolean>;
|
||||
apiKeyInput: string;
|
||||
setApiKeyInput: SetState<string>;
|
||||
}
|
||||
|
||||
export function useChatSend({
|
||||
messages,
|
||||
loading,
|
||||
model,
|
||||
enableTools,
|
||||
claudeSessionId,
|
||||
streamingContent,
|
||||
setClaudeSessionId,
|
||||
setMessages,
|
||||
setLoading,
|
||||
setStreamingContent,
|
||||
setStreamingThinking,
|
||||
setActivityStatus,
|
||||
setSideQuestion,
|
||||
chatInputRef,
|
||||
wsRef,
|
||||
queuedMessagesRef,
|
||||
setQueuedMessages,
|
||||
queueIdCounterRef,
|
||||
clearMessages,
|
||||
projectPath,
|
||||
}: UseChatSendParams): UseChatSendResult {
|
||||
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
|
||||
const [apiKeyInput, setApiKeyInput] = useState("");
|
||||
const pendingMessageRef = useRef<string>("");
|
||||
|
||||
const sendMessage = useCallback(
|
||||
async (messageText: string) => {
|
||||
if (!messageText.trim()) return;
|
||||
|
||||
if (/^\/reset\s*$/i.test(messageText)) {
|
||||
setMessages([]);
|
||||
setClaudeSessionId(null);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setActivityStatus(null);
|
||||
setMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Session reset. Starting a fresh conversation.",
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
const slashMatch = messageText.match(/^\/(\S+)(?:\s+([\s\S]*))?$/);
|
||||
if (slashMatch) {
|
||||
const cmd = slashMatch[1].toLowerCase();
|
||||
const args = (slashMatch[2] ?? "").trim();
|
||||
|
||||
if (cmd !== "btw") {
|
||||
const knownCommands = new Set([
|
||||
"status",
|
||||
"assign",
|
||||
"start",
|
||||
"show",
|
||||
"move",
|
||||
"delete",
|
||||
"cost",
|
||||
"git",
|
||||
"overview",
|
||||
"rebuild",
|
||||
"loc",
|
||||
"help",
|
||||
"ambient",
|
||||
"htop",
|
||||
"rmtree",
|
||||
"timer",
|
||||
"unblock",
|
||||
"unreleased",
|
||||
"setup",
|
||||
]);
|
||||
|
||||
if (knownCommands.has(cmd)) {
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "user", content: messageText },
|
||||
]);
|
||||
try {
|
||||
const result = await api.botCommand(cmd, args, undefined);
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "assistant", content: result.response },
|
||||
]);
|
||||
} catch (e) {
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{
|
||||
role: "assistant",
|
||||
content: `**Error running command:** ${e}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "user", content: messageText },
|
||||
{
|
||||
role: "assistant",
|
||||
content: `Unknown command: \`/${cmd}\`. Type \`/help\` to see available commands.`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const btwMatch = messageText.match(/^\/btw\s+(.+)/s);
|
||||
if (btwMatch) {
|
||||
const question = btwMatch[1].trim();
|
||||
setSideQuestion({ question, response: "", loading: true });
|
||||
|
||||
const isClaudeCode = model === "claude-code-pty";
|
||||
const provider = isClaudeCode
|
||||
? "claude-code"
|
||||
: model.startsWith("claude-")
|
||||
? "anthropic"
|
||||
: "ollama";
|
||||
const config: ProviderConfig = {
|
||||
provider,
|
||||
model,
|
||||
base_url: "http://localhost:11434",
|
||||
enable_tools: false,
|
||||
};
|
||||
wsRef.current?.sendSideQuestion(question, messages, config);
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
const newItem = {
|
||||
id: String(queueIdCounterRef.current++),
|
||||
text: messageText,
|
||||
};
|
||||
queuedMessagesRef.current = [...queuedMessagesRef.current, newItem];
|
||||
setQueuedMessages([...queuedMessagesRef.current]);
|
||||
return;
|
||||
}
|
||||
|
||||
const isClaudeCode = model === "claude-code-pty";
|
||||
if (!isClaudeCode && model.startsWith("claude-")) {
|
||||
const hasKey = await api.getAnthropicApiKeyExists();
|
||||
if (!hasKey) {
|
||||
pendingMessageRef.current = messageText;
|
||||
setShowApiKeyDialog(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fileRefs = [...messageText.matchAll(/(^|[\s\n])@([^\s@]+)/g)].map(
|
||||
(m) => m[2],
|
||||
);
|
||||
let expandedText = messageText;
|
||||
if (fileRefs.length > 0) {
|
||||
const expansions = await Promise.allSettled(
|
||||
fileRefs.map(async (ref) => {
|
||||
const contents = await api.readFile(ref);
|
||||
return { ref, contents };
|
||||
}),
|
||||
);
|
||||
for (const result of expansions) {
|
||||
if (result.status === "fulfilled") {
|
||||
expandedText += `\n\n[File: ${result.value.ref}]\n\`\`\`\n${result.value.contents}\n\`\`\``;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userMsg: Message = { role: "user", content: expandedText };
|
||||
const newHistory = [...messages, userMsg];
|
||||
|
||||
setMessages(newHistory);
|
||||
setLoading(true);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setActivityStatus(null);
|
||||
|
||||
try {
|
||||
const provider = isClaudeCode
|
||||
? "claude-code"
|
||||
: model.startsWith("claude-")
|
||||
? "anthropic"
|
||||
: "ollama";
|
||||
const config: ProviderConfig = {
|
||||
provider,
|
||||
model,
|
||||
base_url: "http://localhost:11434",
|
||||
enable_tools: enableTools,
|
||||
...(isClaudeCode && claudeSessionId
|
||||
? { session_id: claudeSessionId }
|
||||
: {}),
|
||||
};
|
||||
wsRef.current?.sendChat(newHistory, config);
|
||||
} catch (e) {
|
||||
console.error("Chat error:", e);
|
||||
const errorMessage = String(e);
|
||||
if (!errorMessage.includes("Chat cancelled by user")) {
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "assistant", content: `**Error:** ${e}` },
|
||||
]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
messages,
|
||||
loading,
|
||||
model,
|
||||
enableTools,
|
||||
claudeSessionId,
|
||||
setClaudeSessionId,
|
||||
setMessages,
|
||||
setLoading,
|
||||
setStreamingContent,
|
||||
setStreamingThinking,
|
||||
setActivityStatus,
|
||||
setSideQuestion,
|
||||
wsRef,
|
||||
queuedMessagesRef,
|
||||
setQueuedMessages,
|
||||
queueIdCounterRef,
|
||||
],
|
||||
);
|
||||
|
||||
const sendMessageBatch = useCallback(
|
||||
async (messageTexts: string[]) => {
|
||||
if (messageTexts.length === 0) return;
|
||||
|
||||
const userMsgs: Message[] = messageTexts.map((text) => ({
|
||||
role: "user",
|
||||
content: text,
|
||||
}));
|
||||
const newHistory = [...messages, ...userMsgs];
|
||||
|
||||
setMessages(newHistory);
|
||||
setLoading(true);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setActivityStatus(null);
|
||||
|
||||
try {
|
||||
const isClaudeCode = model === "claude-code-pty";
|
||||
const provider = isClaudeCode
|
||||
? "claude-code"
|
||||
: model.startsWith("claude-")
|
||||
? "anthropic"
|
||||
: "ollama";
|
||||
const config: ProviderConfig = {
|
||||
provider,
|
||||
model,
|
||||
base_url: "http://localhost:11434",
|
||||
enable_tools: enableTools,
|
||||
...(isClaudeCode && claudeSessionId
|
||||
? { session_id: claudeSessionId }
|
||||
: {}),
|
||||
};
|
||||
wsRef.current?.sendChat(newHistory, config);
|
||||
} catch (e) {
|
||||
console.error("Chat error:", e);
|
||||
const errorMessage = String(e);
|
||||
if (!errorMessage.includes("Chat cancelled by user")) {
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "assistant", content: `**Error:** ${e}` },
|
||||
]);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
messages,
|
||||
model,
|
||||
enableTools,
|
||||
claudeSessionId,
|
||||
setMessages,
|
||||
setLoading,
|
||||
setStreamingContent,
|
||||
setStreamingThinking,
|
||||
setActivityStatus,
|
||||
wsRef,
|
||||
],
|
||||
);
|
||||
|
||||
const cancelGeneration = useCallback(async () => {
|
||||
if (queuedMessagesRef.current.length > 0) {
|
||||
const queued = queuedMessagesRef.current
|
||||
.map((item) => item.text)
|
||||
.join("\n");
|
||||
chatInputRef.current?.appendToInput(queued);
|
||||
}
|
||||
queuedMessagesRef.current = [];
|
||||
setQueuedMessages([]);
|
||||
try {
|
||||
wsRef.current?.cancel();
|
||||
await api.cancelChat();
|
||||
|
||||
if (streamingContent) {
|
||||
setMessages((prev: Message[]) => [
|
||||
...prev,
|
||||
{ role: "assistant", content: streamingContent },
|
||||
]);
|
||||
setStreamingContent("");
|
||||
}
|
||||
|
||||
setStreamingThinking("");
|
||||
setLoading(false);
|
||||
setActivityStatus(null);
|
||||
} catch (e) {
|
||||
console.error("Failed to cancel chat:", e);
|
||||
}
|
||||
}, [
|
||||
streamingContent,
|
||||
queuedMessagesRef,
|
||||
setQueuedMessages,
|
||||
chatInputRef,
|
||||
wsRef,
|
||||
setMessages,
|
||||
setStreamingContent,
|
||||
setStreamingThinking,
|
||||
setLoading,
|
||||
setActivityStatus,
|
||||
]);
|
||||
|
||||
const handleSaveApiKey = useCallback(async () => {
|
||||
if (!apiKeyInput.trim()) return;
|
||||
|
||||
try {
|
||||
await api.setAnthropicApiKey(apiKeyInput);
|
||||
setShowApiKeyDialog(false);
|
||||
setApiKeyInput("");
|
||||
|
||||
const pendingMessage = pendingMessageRef.current;
|
||||
pendingMessageRef.current = "";
|
||||
|
||||
if (pendingMessage.trim()) {
|
||||
sendMessage(pendingMessage);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to save API key:", e);
|
||||
alert(`Failed to save API key: ${e}`);
|
||||
}
|
||||
}, [apiKeyInput, sendMessage]);
|
||||
|
||||
const clearSession = useCallback(async () => {
|
||||
const confirmed = window.confirm(
|
||||
"Are you sure? This will clear all messages and reset the conversation context.",
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
try {
|
||||
await api.cancelChat();
|
||||
wsRef.current?.cancel();
|
||||
} catch (e) {
|
||||
console.error("Failed to cancel chat:", e);
|
||||
}
|
||||
|
||||
clearMessages();
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
setLoading(false);
|
||||
setActivityStatus(null);
|
||||
setClaudeSessionId(null);
|
||||
try {
|
||||
localStorage.removeItem(`storykit-claude-session-id:${projectPath}`);
|
||||
} catch {
|
||||
// Ignore — quota or security errors.
|
||||
}
|
||||
}
|
||||
}, [
|
||||
wsRef,
|
||||
clearMessages,
|
||||
setStreamingContent,
|
||||
setStreamingThinking,
|
||||
setLoading,
|
||||
setActivityStatus,
|
||||
setClaudeSessionId,
|
||||
projectPath,
|
||||
]);
|
||||
|
||||
return {
|
||||
sendMessage,
|
||||
sendMessageBatch,
|
||||
cancelGeneration,
|
||||
handleSaveApiKey,
|
||||
clearSession,
|
||||
showApiKeyDialog,
|
||||
setShowApiKeyDialog,
|
||||
apiKeyInput,
|
||||
setApiKeyInput,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import * as React from "react";
|
||||
import type { PipelineState, WizardStateData } from "../api/client";
|
||||
import { api, ChatWebSocket } from "../api/client";
|
||||
import type { LogEntry } from "../components/ServerLogsPanel";
|
||||
import type { Message } from "../types";
|
||||
import { formatToolActivity } from "../utils/chatUtils";
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
|
||||
type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||
|
||||
interface UseChatWebSocketParams {
|
||||
setMessages: SetState<Message[]>;
|
||||
setLoading: SetState<boolean>;
|
||||
setClaudeSessionId: SetState<string | null>;
|
||||
queuedMessagesRef: React.MutableRefObject<{ id: string; text: string }[]>;
|
||||
setQueuedMessages: SetState<{ id: string; text: string }[]>;
|
||||
setPendingAutoSendBatch: SetState<string[] | null>;
|
||||
}
|
||||
|
||||
interface ReconciliationEvent {
|
||||
id: string;
|
||||
storyId: string;
|
||||
status: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface UseChatWebSocketResult {
|
||||
wsRef: React.MutableRefObject<ChatWebSocket | null>;
|
||||
wsConnected: boolean;
|
||||
streamingContent: string;
|
||||
setStreamingContent: SetState<string>;
|
||||
streamingThinking: string;
|
||||
setStreamingThinking: SetState<string>;
|
||||
activityStatus: string | null;
|
||||
setActivityStatus: SetState<string | null>;
|
||||
permissionQueue: {
|
||||
requestId: string;
|
||||
toolName: string;
|
||||
toolInput: Record<string, unknown>;
|
||||
}[];
|
||||
setPermissionQueue: SetState<
|
||||
{
|
||||
requestId: string;
|
||||
toolName: string;
|
||||
toolInput: Record<string, unknown>;
|
||||
}[]
|
||||
>;
|
||||
pipeline: PipelineState;
|
||||
pipelineVersion: number;
|
||||
reconciliationActive: boolean;
|
||||
reconciliationEvents: ReconciliationEvent[];
|
||||
agentConfigVersion: number;
|
||||
agentStateVersion: number;
|
||||
needsOnboarding: boolean;
|
||||
setNeedsOnboarding: SetState<boolean>;
|
||||
wizardState: WizardStateData | null;
|
||||
setWizardState: SetState<WizardStateData | null>;
|
||||
sideQuestion: {
|
||||
question: string;
|
||||
response: string;
|
||||
loading: boolean;
|
||||
} | null;
|
||||
setSideQuestion: SetState<{
|
||||
question: string;
|
||||
response: string;
|
||||
loading: boolean;
|
||||
} | null>;
|
||||
serverLogs: LogEntry[];
|
||||
storyTokenCosts: Map<string, number>;
|
||||
}
|
||||
|
||||
export function useChatWebSocket({
|
||||
setMessages,
|
||||
setLoading,
|
||||
setClaudeSessionId,
|
||||
queuedMessagesRef,
|
||||
setQueuedMessages,
|
||||
setPendingAutoSendBatch,
|
||||
}: UseChatWebSocketParams): UseChatWebSocketResult {
|
||||
const wsRef = useRef<ChatWebSocket | null>(null);
|
||||
const [wsConnected, setWsConnected] = useState(false);
|
||||
const [streamingContent, setStreamingContent] = useState("");
|
||||
const [streamingThinking, setStreamingThinking] = useState("");
|
||||
const [activityStatus, setActivityStatus] = useState<string | null>(null);
|
||||
const [permissionQueue, setPermissionQueue] = useState<
|
||||
{
|
||||
requestId: string;
|
||||
toolName: string;
|
||||
toolInput: Record<string, unknown>;
|
||||
}[]
|
||||
>([]);
|
||||
const [pipeline, setPipeline] = useState<PipelineState>({
|
||||
backlog: [],
|
||||
current: [],
|
||||
qa: [],
|
||||
merge: [],
|
||||
done: [],
|
||||
});
|
||||
const [pipelineVersion, setPipelineVersion] = useState(0);
|
||||
const [reconciliationActive, setReconciliationActive] = useState(false);
|
||||
const [reconciliationEvents, setReconciliationEvents] = useState<
|
||||
ReconciliationEvent[]
|
||||
>([]);
|
||||
const reconciliationEventIdRef = useRef(0);
|
||||
const [agentConfigVersion, setAgentConfigVersion] = useState(0);
|
||||
const [agentStateVersion, setAgentStateVersion] = useState(0);
|
||||
const [needsOnboarding, setNeedsOnboarding] = useState(false);
|
||||
const [wizardState, setWizardState] = useState<WizardStateData | null>(null);
|
||||
const [sideQuestion, setSideQuestion] = useState<{
|
||||
question: string;
|
||||
response: string;
|
||||
loading: boolean;
|
||||
} | null>(null);
|
||||
const [serverLogs, setServerLogs] = useState<LogEntry[]>([]);
|
||||
const [storyTokenCosts, setStoryTokenCosts] = useState<Map<string, number>>(
|
||||
new Map(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new ChatWebSocket();
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.connect({
|
||||
onToken: (content) => {
|
||||
setStreamingContent((prev: string) => prev + content);
|
||||
},
|
||||
onThinkingToken: (content) => {
|
||||
setStreamingThinking((prev: string) => prev + content);
|
||||
},
|
||||
onUpdate: (history) => {
|
||||
setMessages(history);
|
||||
setStreamingContent("");
|
||||
setStreamingThinking("");
|
||||
const last = history[history.length - 1];
|
||||
if (last?.role === "assistant" && !last.tool_calls) {
|
||||
setLoading(false);
|
||||
setActivityStatus(null);
|
||||
if (queuedMessagesRef.current.length > 0) {
|
||||
const batch = queuedMessagesRef.current.map((item) => item.text);
|
||||
queuedMessagesRef.current = [];
|
||||
setQueuedMessages([]);
|
||||
setPendingAutoSendBatch(batch);
|
||||
}
|
||||
}
|
||||
},
|
||||
onSessionId: (sessionId) => {
|
||||
setClaudeSessionId(sessionId);
|
||||
},
|
||||
onError: (message) => {
|
||||
console.error("WebSocket error:", message);
|
||||
setLoading(false);
|
||||
setActivityStatus(null);
|
||||
const markdownMessage = message.replace(
|
||||
/(https?:\/\/[^\s]+)/g,
|
||||
"[$1]($1)",
|
||||
);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ role: "assistant", content: markdownMessage },
|
||||
]);
|
||||
if (queuedMessagesRef.current.length > 0) {
|
||||
const batch = queuedMessagesRef.current.map((item) => item.text);
|
||||
queuedMessagesRef.current = [];
|
||||
setQueuedMessages([]);
|
||||
setPendingAutoSendBatch(batch);
|
||||
}
|
||||
},
|
||||
onPipelineState: (state) => {
|
||||
setPipeline(state);
|
||||
setPipelineVersion((v) => v + 1);
|
||||
const allItems = [
|
||||
...state.backlog,
|
||||
...state.current,
|
||||
...state.qa,
|
||||
...state.merge,
|
||||
...state.done,
|
||||
];
|
||||
for (const item of allItems) {
|
||||
api
|
||||
.getTokenCost(item.story_id)
|
||||
.then((cost) => {
|
||||
if (cost.total_cost_usd > 0) {
|
||||
setStoryTokenCosts((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(item.story_id, cost.total_cost_usd);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore — cost data may not exist yet.
|
||||
});
|
||||
}
|
||||
},
|
||||
onPermissionRequest: (requestId, toolName, toolInput) => {
|
||||
setPermissionQueue((prev) => [
|
||||
...prev,
|
||||
{ requestId, toolName, toolInput },
|
||||
]);
|
||||
},
|
||||
onActivity: (toolName) => {
|
||||
setActivityStatus(formatToolActivity(toolName));
|
||||
},
|
||||
onReconciliationProgress: (storyId, status, message) => {
|
||||
if (status === "done") {
|
||||
setReconciliationActive(false);
|
||||
} else {
|
||||
setReconciliationActive(true);
|
||||
setReconciliationEvents((prev) => {
|
||||
const id = String(reconciliationEventIdRef.current++);
|
||||
const next = [...prev, { id, storyId, status, message }];
|
||||
// Keep only the last 8 events to avoid the banner growing too tall.
|
||||
return next.slice(-8);
|
||||
});
|
||||
}
|
||||
},
|
||||
onAgentConfigChanged: () => {
|
||||
setAgentConfigVersion((v) => v + 1);
|
||||
},
|
||||
onAgentStateChanged: () => {
|
||||
setAgentStateVersion((v) => v + 1);
|
||||
},
|
||||
onOnboardingStatus: (onboarding: boolean) => {
|
||||
setNeedsOnboarding(onboarding);
|
||||
},
|
||||
onWizardState: (state: WizardStateData) => {
|
||||
setWizardState(state);
|
||||
},
|
||||
onSideQuestionToken: (content) => {
|
||||
setSideQuestion((prev) =>
|
||||
prev ? { ...prev, response: prev.response + content } : prev,
|
||||
);
|
||||
},
|
||||
onSideQuestionDone: (response) => {
|
||||
setSideQuestion((prev) =>
|
||||
prev ? { ...prev, response, loading: false } : prev,
|
||||
);
|
||||
},
|
||||
onLogEntry: (timestamp, level, message) => {
|
||||
setServerLogs((prev) => [...prev, { timestamp, level, message }]);
|
||||
},
|
||||
onConnected: () => {
|
||||
setWsConnected(true);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
ws.close();
|
||||
wsRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
wsRef,
|
||||
wsConnected,
|
||||
streamingContent,
|
||||
setStreamingContent,
|
||||
streamingThinking,
|
||||
setStreamingThinking,
|
||||
activityStatus,
|
||||
setActivityStatus,
|
||||
permissionQueue,
|
||||
setPermissionQueue,
|
||||
pipeline,
|
||||
pipelineVersion,
|
||||
reconciliationActive,
|
||||
reconciliationEvents,
|
||||
agentConfigVersion,
|
||||
agentStateVersion,
|
||||
needsOnboarding,
|
||||
setNeedsOnboarding,
|
||||
wizardState,
|
||||
setWizardState,
|
||||
sideQuestion,
|
||||
setSideQuestion,
|
||||
serverLogs,
|
||||
storyTokenCosts,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user