2026-03-22 19:07:07 +00:00
|
|
|
import * as React from "react";
|
|
|
|
|
import type { AgentConfigInfo } from "../api/agents";
|
|
|
|
|
import { agentsApi } from "../api/agents";
|
2026-04-04 15:12:03 +00:00
|
|
|
import type { AnthropicModelInfo, OAuthStatus } from "../api/client";
|
|
|
|
|
import { api } from "../api/client";
|
2026-03-22 19:07:07 +00:00
|
|
|
import { useChatHistory } from "../hooks/useChatHistory";
|
2026-04-04 15:12:03 +00:00
|
|
|
import { useChatSend } from "../hooks/useChatSend";
|
|
|
|
|
import { useChatWebSocket } from "../hooks/useChatWebSocket";
|
|
|
|
|
import { estimateTokens, getContextWindowSize } from "../utils/chatUtils";
|
|
|
|
|
import { ApiKeyDialog } from "./ApiKeyDialog";
|
2026-04-15 17:33:56 +00:00
|
|
|
import { BotConfigPage } from "./BotConfigPage";
|
2026-03-22 19:07:07 +00:00
|
|
|
import { ChatHeader } from "./ChatHeader";
|
|
|
|
|
import type { ChatInputHandle } from "./ChatInput";
|
|
|
|
|
import { ChatInput } from "./ChatInput";
|
2026-04-04 15:12:03 +00:00
|
|
|
import { ChatMessageList } from "./ChatMessageList";
|
|
|
|
|
import { ChatPipelinePanel } from "./ChatPipelinePanel";
|
2026-03-22 19:07:07 +00:00
|
|
|
import { HelpOverlay } from "./HelpOverlay";
|
2026-04-04 15:12:03 +00:00
|
|
|
import { PermissionDialog } from "./PermissionDialog";
|
|
|
|
|
import { ReconciliationBanner } from "./ReconciliationBanner";
|
2026-03-22 19:07:07 +00:00
|
|
|
import { SideQuestionOverlay } from "./SideQuestionOverlay";
|
|
|
|
|
|
|
|
|
|
const { useCallback, useEffect, useMemo, useRef, useState } = React;
|
|
|
|
|
|
|
|
|
|
const NARROW_BREAKPOINT = 900;
|
|
|
|
|
|
|
|
|
|
interface ChatProps {
|
|
|
|
|
projectPath: string;
|
|
|
|
|
onCloseProject: () => void;
|
2026-03-31 10:04:52 +00:00
|
|
|
oauthStatus?: OAuthStatus | null;
|
2026-03-22 19:07:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 14:52:18 +00:00
|
|
|
export function Chat({
|
|
|
|
|
projectPath,
|
|
|
|
|
onCloseProject,
|
|
|
|
|
oauthStatus = null,
|
|
|
|
|
}: ChatProps) {
|
2026-03-22 19:07:07 +00:00
|
|
|
const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [model, setModel] = useState("claude-code-pty");
|
|
|
|
|
const [enableTools, setEnableTools] = useState(true);
|
|
|
|
|
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
|
|
|
|
const [claudeModels, setClaudeModels] = useState<string[]>([]);
|
|
|
|
|
const [claudeContextWindowMap, setClaudeContextWindowMap] = useState<
|
|
|
|
|
Map<string, number>
|
|
|
|
|
>(new Map());
|
|
|
|
|
const [hasAnthropicKey, setHasAnthropicKey] = useState(false);
|
|
|
|
|
const [claudeSessionId, setClaudeSessionId] = useState<string | null>(() => {
|
|
|
|
|
try {
|
|
|
|
|
return (
|
|
|
|
|
localStorage.getItem(`storykit-claude-session-id:${projectPath}`) ??
|
|
|
|
|
null
|
|
|
|
|
);
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const [isNarrowScreen, setIsNarrowScreen] = useState(
|
|
|
|
|
window.innerWidth < NARROW_BREAKPOINT,
|
|
|
|
|
);
|
|
|
|
|
const [agentRoster, setAgentRoster] = useState<AgentConfigInfo[]>([]);
|
|
|
|
|
const [selectedWorkItemId, setSelectedWorkItemId] = useState<string | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
2026-04-04 15:12:03 +00:00
|
|
|
const [showHelp, setShowHelp] = useState(false);
|
2026-04-15 17:33:56 +00:00
|
|
|
const [view, setView] = useState<"chat" | "bot-config">("chat");
|
2026-03-22 19:07:07 +00:00
|
|
|
const [queuedMessages, setQueuedMessages] = useState<
|
|
|
|
|
{ id: string; text: string }[]
|
|
|
|
|
>([]);
|
|
|
|
|
const [pendingAutoSendBatch, setPendingAutoSendBatch] = useState<
|
|
|
|
|
string[] | null
|
|
|
|
|
>(null);
|
|
|
|
|
|
|
|
|
|
const chatInputRef = useRef<ChatInputHandle>(null);
|
|
|
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
2026-04-04 15:12:03 +00:00
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
2026-03-22 19:07:07 +00:00
|
|
|
const shouldAutoScrollRef = useRef(true);
|
|
|
|
|
const lastScrollTopRef = useRef(0);
|
|
|
|
|
const userScrolledUpRef = useRef(false);
|
2026-04-04 15:12:03 +00:00
|
|
|
const queuedMessagesRef = useRef<{ id: string; text: string }[]>([]);
|
|
|
|
|
const queueIdCounterRef = useRef(0);
|
|
|
|
|
const onboardingTriggeredRef = useRef(false);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
wsRef,
|
|
|
|
|
wsConnected,
|
|
|
|
|
streamingContent,
|
|
|
|
|
setStreamingContent,
|
|
|
|
|
streamingThinking,
|
|
|
|
|
setStreamingThinking,
|
|
|
|
|
activityStatus,
|
|
|
|
|
setActivityStatus,
|
|
|
|
|
permissionQueue,
|
|
|
|
|
setPermissionQueue,
|
|
|
|
|
pipeline,
|
|
|
|
|
pipelineVersion,
|
|
|
|
|
reconciliationActive,
|
|
|
|
|
reconciliationEvents,
|
|
|
|
|
agentConfigVersion,
|
|
|
|
|
agentStateVersion,
|
|
|
|
|
needsOnboarding,
|
|
|
|
|
setNeedsOnboarding,
|
|
|
|
|
wizardState,
|
|
|
|
|
setWizardState,
|
|
|
|
|
sideQuestion,
|
|
|
|
|
setSideQuestion,
|
|
|
|
|
serverLogs,
|
|
|
|
|
storyTokenCosts,
|
|
|
|
|
} = useChatWebSocket({
|
|
|
|
|
setMessages,
|
|
|
|
|
setLoading,
|
|
|
|
|
setClaudeSessionId,
|
|
|
|
|
queuedMessagesRef,
|
|
|
|
|
setQueuedMessages,
|
|
|
|
|
setPendingAutoSendBatch,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
sendMessage,
|
|
|
|
|
sendMessageBatch,
|
|
|
|
|
cancelGeneration,
|
|
|
|
|
handleSaveApiKey,
|
|
|
|
|
clearSession,
|
|
|
|
|
showApiKeyDialog,
|
|
|
|
|
setShowApiKeyDialog,
|
|
|
|
|
apiKeyInput,
|
|
|
|
|
setApiKeyInput,
|
|
|
|
|
} = useChatSend({
|
|
|
|
|
messages,
|
|
|
|
|
loading,
|
|
|
|
|
model,
|
|
|
|
|
enableTools,
|
|
|
|
|
claudeSessionId,
|
|
|
|
|
streamingContent,
|
|
|
|
|
setClaudeSessionId,
|
|
|
|
|
setMessages,
|
|
|
|
|
setLoading,
|
|
|
|
|
setStreamingContent,
|
|
|
|
|
setStreamingThinking,
|
|
|
|
|
setActivityStatus,
|
|
|
|
|
setSideQuestion,
|
|
|
|
|
chatInputRef,
|
|
|
|
|
wsRef,
|
|
|
|
|
queuedMessagesRef,
|
|
|
|
|
setQueuedMessages,
|
|
|
|
|
queueIdCounterRef,
|
|
|
|
|
clearMessages,
|
|
|
|
|
projectPath,
|
|
|
|
|
});
|
2026-03-22 19:07:07 +00:00
|
|
|
|
|
|
|
|
const busyAgentNames = useMemo(() => {
|
|
|
|
|
const busy = new Set<string>();
|
|
|
|
|
const allItems = [
|
|
|
|
|
...pipeline.backlog,
|
|
|
|
|
...pipeline.current,
|
|
|
|
|
...pipeline.qa,
|
|
|
|
|
...pipeline.merge,
|
|
|
|
|
];
|
|
|
|
|
for (const item of allItems) {
|
|
|
|
|
if (
|
|
|
|
|
item.agent &&
|
|
|
|
|
(item.agent.status === "running" || item.agent.status === "pending")
|
|
|
|
|
) {
|
|
|
|
|
busy.add(item.agent.agent_name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return busy;
|
|
|
|
|
}, [pipeline]);
|
|
|
|
|
|
|
|
|
|
const contextUsage = useMemo(() => {
|
2026-04-04 15:12:03 +00:00
|
|
|
let totalTokens = 200;
|
2026-03-22 19:07:07 +00:00
|
|
|
for (const msg of messages) {
|
|
|
|
|
totalTokens += estimateTokens(msg.content);
|
|
|
|
|
if (msg.tool_calls) {
|
|
|
|
|
totalTokens += estimateTokens(JSON.stringify(msg.tool_calls));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (streamingContent) {
|
|
|
|
|
totalTokens += estimateTokens(streamingContent);
|
|
|
|
|
}
|
|
|
|
|
const contextWindow = getContextWindowSize(model, claudeContextWindowMap);
|
|
|
|
|
return {
|
|
|
|
|
used: totalTokens,
|
|
|
|
|
total: contextWindow,
|
2026-04-04 15:12:03 +00:00
|
|
|
percentage: Math.round((totalTokens / contextWindow) * 100),
|
2026-03-22 19:07:07 +00:00
|
|
|
};
|
|
|
|
|
}, [messages, streamingContent, model, claudeContextWindowMap]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
try {
|
|
|
|
|
if (claudeSessionId !== null) {
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
`storykit-claude-session-id:${projectPath}`,
|
|
|
|
|
claudeSessionId,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
localStorage.removeItem(`storykit-claude-session-id:${projectPath}`);
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore — quota or security errors.
|
|
|
|
|
}
|
|
|
|
|
}, [claudeSessionId, projectPath]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
api
|
|
|
|
|
.getOllamaModels()
|
|
|
|
|
.then(async (models) => {
|
|
|
|
|
if (models.length > 0) {
|
|
|
|
|
const sortedModels = models.sort((a, b) =>
|
|
|
|
|
a.toLowerCase().localeCompare(b.toLowerCase()),
|
|
|
|
|
);
|
|
|
|
|
setAvailableModels(sortedModels);
|
|
|
|
|
try {
|
|
|
|
|
const savedModel = await api.getModelPreference();
|
2026-04-04 15:12:03 +00:00
|
|
|
if (savedModel) setModel(savedModel);
|
2026-03-22 19:07:07 +00:00
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => console.error(err));
|
|
|
|
|
|
|
|
|
|
api
|
|
|
|
|
.getAnthropicApiKeyExists()
|
|
|
|
|
.then((exists) => {
|
|
|
|
|
setHasAnthropicKey(exists);
|
|
|
|
|
if (!exists) return;
|
|
|
|
|
return api.getAnthropicModels().then((models: AnthropicModelInfo[]) => {
|
|
|
|
|
if (models.length > 0) {
|
2026-04-04 15:12:03 +00:00
|
|
|
const sorted = models.sort((a, b) =>
|
2026-03-22 19:07:07 +00:00
|
|
|
a.id.toLowerCase().localeCompare(b.id.toLowerCase()),
|
|
|
|
|
);
|
2026-04-04 15:12:03 +00:00
|
|
|
setClaudeModels(sorted.map((m) => m.id));
|
2026-03-22 19:07:07 +00:00
|
|
|
setClaudeContextWindowMap(
|
2026-04-04 15:12:03 +00:00
|
|
|
new Map(sorted.map((m) => [m.id, m.context_window])),
|
2026-03-22 19:07:07 +00:00
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
setClaudeModels([]);
|
|
|
|
|
setClaudeContextWindowMap(new Map());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error(err);
|
|
|
|
|
setHasAnthropicKey(false);
|
|
|
|
|
setClaudeModels([]);
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-04 15:12:03 +00:00
|
|
|
const handleResize = () =>
|
|
|
|
|
setIsNarrowScreen(window.innerWidth < NARROW_BREAKPOINT);
|
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
|
|
|
return () => window.removeEventListener("resize", handleResize);
|
2026-03-22 19:07:07 +00:00
|
|
|
}, []);
|
|
|
|
|
|
2026-04-04 15:12:03 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
agentsApi
|
|
|
|
|
.getAgentConfig()
|
|
|
|
|
.then(setAgentRoster)
|
|
|
|
|
.catch(() => {
|
|
|
|
|
// Silently ignore — roster unavailable.
|
|
|
|
|
});
|
|
|
|
|
}, [agentConfigVersion]);
|
|
|
|
|
|
2026-03-22 19:07:07 +00:00
|
|
|
const scrollToBottom = useCallback(() => {
|
2026-04-04 15:12:03 +00:00
|
|
|
const el = scrollContainerRef.current;
|
|
|
|
|
if (el) {
|
|
|
|
|
el.scrollTop = el.scrollHeight;
|
|
|
|
|
lastScrollTopRef.current = el.scrollTop;
|
2026-03-22 19:07:07 +00:00
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleScroll = () => {
|
2026-04-04 15:12:03 +00:00
|
|
|
const el = scrollContainerRef.current;
|
|
|
|
|
if (!el) return;
|
|
|
|
|
const currentScrollTop = el.scrollTop;
|
|
|
|
|
const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 5;
|
2026-03-22 19:07:07 +00:00
|
|
|
if (currentScrollTop < lastScrollTopRef.current) {
|
|
|
|
|
userScrolledUpRef.current = true;
|
|
|
|
|
shouldAutoScrollRef.current = false;
|
|
|
|
|
}
|
|
|
|
|
if (isAtBottom) {
|
|
|
|
|
userScrolledUpRef.current = false;
|
|
|
|
|
shouldAutoScrollRef.current = true;
|
|
|
|
|
}
|
|
|
|
|
lastScrollTopRef.current = currentScrollTop;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const autoScrollKey =
|
|
|
|
|
messages.length + streamingContent.length + streamingThinking.length;
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-04 15:12:03 +00:00
|
|
|
if (shouldAutoScrollRef.current && !userScrolledUpRef.current) {
|
2026-03-22 19:07:07 +00:00
|
|
|
scrollToBottom();
|
|
|
|
|
}
|
|
|
|
|
}, [autoScrollKey, scrollToBottom]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (pendingAutoSendBatch && pendingAutoSendBatch.length > 0) {
|
|
|
|
|
const batch = pendingAutoSendBatch;
|
|
|
|
|
setPendingAutoSendBatch(null);
|
|
|
|
|
sendMessageBatch(batch);
|
|
|
|
|
}
|
2026-04-04 15:12:03 +00:00
|
|
|
}, [pendingAutoSendBatch, sendMessageBatch]);
|
2026-03-22 19:07:07 +00:00
|
|
|
|
|
|
|
|
const handleStartAgent = useCallback(
|
|
|
|
|
async (storyId: string, agentName?: string) => {
|
|
|
|
|
try {
|
|
|
|
|
await agentsApi.startAgent(storyId, agentName);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to start agent:", err);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-04 15:12:03 +00:00
|
|
|
const handleStopAgent = useCallback((storyId: string, agentName: string) => {
|
|
|
|
|
agentsApi.stopAgent(storyId, agentName).catch((err: unknown) => {
|
|
|
|
|
console.error("Failed to stop agent:", err);
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
2026-03-22 19:07:07 +00:00
|
|
|
|
2026-04-04 15:12:03 +00:00
|
|
|
const handleDeleteItem = useCallback(
|
|
|
|
|
(item: import("../api/client").PipelineStageItem) => {
|
|
|
|
|
api.deleteStory(item.story_id).catch((err: unknown) => {
|
|
|
|
|
console.error("Failed to delete story:", err);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
);
|
2026-03-22 19:07:07 +00:00
|
|
|
|
2026-04-04 15:12:03 +00:00
|
|
|
const handleRemoveQueuedMessage = useCallback((id: string) => {
|
|
|
|
|
queuedMessagesRef.current = queuedMessagesRef.current.filter(
|
|
|
|
|
(item) => item.id !== id,
|
2026-03-22 19:07:07 +00:00
|
|
|
);
|
2026-04-04 15:12:03 +00:00
|
|
|
setQueuedMessages([...queuedMessagesRef.current]);
|
|
|
|
|
}, []);
|
2026-03-22 19:07:07 +00:00
|
|
|
|
|
|
|
|
const handlePermissionResponse = (approved: boolean, alwaysAllow = false) => {
|
|
|
|
|
const current = permissionQueue[0];
|
|
|
|
|
if (!current) return;
|
|
|
|
|
wsRef.current?.sendPermissionResponse(
|
|
|
|
|
current.requestId,
|
|
|
|
|
approved,
|
|
|
|
|
alwaysAllow,
|
|
|
|
|
);
|
|
|
|
|
setPermissionQueue((prev) => prev.slice(1));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="chat-container"
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
height: "100%",
|
|
|
|
|
backgroundColor: "#171717",
|
|
|
|
|
color: "#ececec",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<ChatHeader
|
|
|
|
|
projectPath={projectPath}
|
|
|
|
|
onCloseProject={onCloseProject}
|
|
|
|
|
contextUsage={contextUsage}
|
|
|
|
|
onClearSession={clearSession}
|
|
|
|
|
model={model}
|
|
|
|
|
availableModels={availableModels}
|
|
|
|
|
claudeModels={claudeModels}
|
|
|
|
|
hasAnthropicKey={hasAnthropicKey}
|
|
|
|
|
onModelChange={(newModel) => {
|
|
|
|
|
setModel(newModel);
|
|
|
|
|
api.setModelPreference(newModel).catch(console.error);
|
|
|
|
|
}}
|
|
|
|
|
enableTools={enableTools}
|
|
|
|
|
onToggleTools={setEnableTools}
|
|
|
|
|
wsConnected={wsConnected}
|
2026-03-31 10:04:52 +00:00
|
|
|
oauthStatus={oauthStatus}
|
2026-04-15 17:33:56 +00:00
|
|
|
onShowBotConfig={() => setView("bot-config")}
|
2026-03-22 19:07:07 +00:00
|
|
|
/>
|
|
|
|
|
|
2026-04-15 17:33:56 +00:00
|
|
|
{view === "bot-config" && (
|
|
|
|
|
<BotConfigPage onBack={() => setView("chat")} />
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-22 19:07:07 +00:00
|
|
|
<div
|
|
|
|
|
data-testid="chat-content-area"
|
|
|
|
|
style={{
|
2026-04-15 17:33:56 +00:00
|
|
|
display: view === "bot-config" ? "none" : "flex",
|
2026-03-22 19:07:07 +00:00
|
|
|
flex: 1,
|
|
|
|
|
minHeight: 0,
|
|
|
|
|
flexDirection: isNarrowScreen ? "column" : "row",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
data-testid="chat-left-column"
|
|
|
|
|
style={{
|
|
|
|
|
display: "flex",
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
flex: "0 0 60%",
|
|
|
|
|
minHeight: 0,
|
|
|
|
|
overflow: "hidden",
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-04-04 15:12:03 +00:00
|
|
|
<ChatMessageList
|
|
|
|
|
messages={messages}
|
|
|
|
|
loading={loading}
|
|
|
|
|
streamingContent={streamingContent}
|
|
|
|
|
streamingThinking={streamingThinking}
|
|
|
|
|
activityStatus={activityStatus}
|
|
|
|
|
wizardState={wizardState}
|
|
|
|
|
setWizardState={setWizardState}
|
|
|
|
|
needsOnboarding={needsOnboarding}
|
|
|
|
|
setNeedsOnboarding={setNeedsOnboarding}
|
|
|
|
|
sendMessage={sendMessage}
|
|
|
|
|
scrollContainerRef={scrollContainerRef}
|
|
|
|
|
messagesEndRef={messagesEndRef}
|
2026-03-22 19:07:07 +00:00
|
|
|
onScroll={handleScroll}
|
2026-04-04 15:12:03 +00:00
|
|
|
onboardingTriggeredRef={onboardingTriggeredRef}
|
|
|
|
|
/>
|
2026-03-22 19:07:07 +00:00
|
|
|
|
|
|
|
|
{reconciliationActive && (
|
2026-04-04 15:12:03 +00:00
|
|
|
<ReconciliationBanner reconciliationEvents={reconciliationEvents} />
|
2026-03-22 19:07:07 +00:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<ChatInput
|
|
|
|
|
ref={chatInputRef}
|
|
|
|
|
loading={loading}
|
|
|
|
|
queuedMessages={queuedMessages}
|
|
|
|
|
onSubmit={sendMessage}
|
|
|
|
|
onCancel={cancelGeneration}
|
|
|
|
|
onRemoveQueuedMessage={handleRemoveQueuedMessage}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-04 15:12:03 +00:00
|
|
|
<ChatPipelinePanel
|
|
|
|
|
isNarrowScreen={isNarrowScreen}
|
|
|
|
|
pipeline={pipeline}
|
|
|
|
|
pipelineVersion={pipelineVersion}
|
|
|
|
|
agentConfigVersion={agentConfigVersion}
|
|
|
|
|
agentStateVersion={agentStateVersion}
|
|
|
|
|
storyTokenCosts={storyTokenCosts}
|
|
|
|
|
agentRoster={agentRoster}
|
|
|
|
|
busyAgentNames={busyAgentNames}
|
|
|
|
|
selectedWorkItemId={selectedWorkItemId}
|
|
|
|
|
serverLogs={serverLogs}
|
|
|
|
|
onSelectWorkItem={setSelectedWorkItemId}
|
|
|
|
|
onCloseWorkItem={() => setSelectedWorkItemId(null)}
|
|
|
|
|
onStartAgent={handleStartAgent}
|
|
|
|
|
onStopAgent={handleStopAgent}
|
|
|
|
|
onDeleteItem={handleDeleteItem}
|
|
|
|
|
/>
|
2026-03-22 19:07:07 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showApiKeyDialog && (
|
2026-04-04 15:12:03 +00:00
|
|
|
<ApiKeyDialog
|
|
|
|
|
apiKeyInput={apiKeyInput}
|
|
|
|
|
onApiKeyChange={setApiKeyInput}
|
|
|
|
|
onSave={handleSaveApiKey}
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
setShowApiKeyDialog(false);
|
|
|
|
|
setApiKeyInput("");
|
2026-03-22 19:07:07 +00:00
|
|
|
}}
|
2026-04-04 15:12:03 +00:00
|
|
|
/>
|
2026-03-22 19:07:07 +00:00
|
|
|
)}
|
2026-04-04 15:12:03 +00:00
|
|
|
|
2026-03-22 19:07:07 +00:00
|
|
|
{permissionQueue.length > 0 && (
|
2026-04-04 15:12:03 +00:00
|
|
|
<PermissionDialog
|
|
|
|
|
permissionQueue={permissionQueue}
|
|
|
|
|
onResponse={handlePermissionResponse}
|
|
|
|
|
/>
|
2026-03-22 19:07:07 +00:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showHelp && <HelpOverlay onDismiss={() => setShowHelp(false)} />}
|
|
|
|
|
|
|
|
|
|
{sideQuestion && (
|
|
|
|
|
<SideQuestionOverlay
|
|
|
|
|
question={sideQuestion.question}
|
|
|
|
|
response={sideQuestion.response}
|
|
|
|
|
loading={sideQuestion.loading}
|
|
|
|
|
onDismiss={() => setSideQuestion(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|