diff --git a/apps/desktop/src/lib/trpc/routers/ui-state/index.ts b/apps/desktop/src/lib/trpc/routers/ui-state/index.ts index e9f19043286..17b97abbdd6 100644 --- a/apps/desktop/src/lib/trpc/routers/ui-state/index.ts +++ b/apps/desktop/src/lib/trpc/routers/ui-state/index.ts @@ -33,7 +33,7 @@ const fileViewerStateSchema = z.object({ const paneSchema = z.object({ id: z.string(), tabId: z.string(), - type: z.enum(["terminal", "webview", "file-viewer"]), + type: z.enum(["terminal", "webview", "file-viewer", "chat"]), name: z.string(), isNew: z.boolean().optional(), status: z.enum(["idle", "working", "permission", "review"]).optional(), @@ -43,6 +43,7 @@ const paneSchema = z.object({ cwd: z.string().nullable().optional(), cwdConfirmed: z.boolean().optional(), fileViewer: fileViewerStateSchema.optional(), + chat: z.object({ sessionId: z.string() }).optional(), }); /** diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx index 6924e6c9571..3085fd2fbc0 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx @@ -1,3 +1,4 @@ +import { FEATURE_FLAGS } from "@superset/shared/constants"; import { Button } from "@superset/ui/button"; import { DropdownMenu, @@ -8,8 +9,10 @@ import { } from "@superset/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useNavigate, useParams } from "@tanstack/react-router"; +import { useFeatureFlagEnabled } from "posthog-js/react"; import { useCallback, useMemo, useState } from "react"; import { + HiMiniChatBubbleLeftRight, HiMiniChevronDown, HiMiniCog6Tooth, HiMiniCommandLine, @@ -41,6 +44,7 @@ export function GroupStrip() { const activeTabIds = useTabsStore((s) => s.activeTabIds); const tabHistoryStacks = useTabsStore((s) => s.tabHistoryStacks); const { addTab, openPreset } = useTabsWithPresets(); + const addChatTab = useTabsStore((s) => s.addChatTab); const renameTab = useTabsStore((s) => s.renameTab); const removeTab = useTabsStore((s) => s.removeTab); const setActiveTab = useTabsStore((s) => s.setActiveTab); @@ -48,6 +52,7 @@ export function GroupStrip() { const movePaneToNewTab = useTabsStore((s) => s.movePaneToNewTab); const reorderTabs = useTabsStore((s) => s.reorderTabs); + const hasAiChat = useFeatureFlagEnabled(FEATURE_FLAGS.AI_CHAT); const { presets } = usePresets(); const isDark = useIsDarkTheme(); const navigate = useNavigate(); @@ -89,6 +94,11 @@ export function GroupStrip() { addTab(activeWorkspaceId); }; + const handleAddChat = () => { + if (!activeWorkspaceId) return; + addChatTab(activeWorkspaceId); + }; + const handleSelectPreset = (preset: Parameters[1]) => { if (!activeWorkspaceId) return; openPreset(activeWorkspaceId, preset); @@ -176,6 +186,23 @@ export function GroupStrip() { })} )} + {hasAiChat && ( + + + + + + New Chat + + + )} movePaneToNewTab(paneId)} isLastPaneInTab={checkIsLastPaneInTab} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx new file mode 100644 index 00000000000..663c4047e11 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/ChatInterface.tsx @@ -0,0 +1,159 @@ +import { + Checkpoint, + CheckpointIcon, + CheckpointTrigger, +} from "@superset/ui/ai-elements/checkpoint"; +import { + Conversation, + ConversationContent, + ConversationEmptyState, + ConversationScrollButton, +} from "@superset/ui/ai-elements/conversation"; +import { Message, MessageContent } from "@superset/ui/ai-elements/message"; +import { + PromptInput, + PromptInputButton, + PromptInputFooter, + PromptInputSubmit, + PromptInputTextarea, + PromptInputTools, +} from "@superset/ui/ai-elements/prompt-input"; +import { Shimmer } from "@superset/ui/ai-elements/shimmer"; +import { Suggestion, Suggestions } from "@superset/ui/ai-elements/suggestion"; +import { Fragment, useCallback, useState } from "react"; +import { + HiMiniAtSymbol, + HiMiniChatBubbleLeftRight, + HiMiniPaperClip, +} from "react-icons/hi2"; +import { ChatMessageItem } from "./components/ChatMessageItem"; +import { ContextIndicator } from "./components/ContextIndicator"; +import { ModelPicker } from "./components/ModelPicker"; +import { MOCK_MESSAGES, MODELS, SUGGESTIONS } from "./constants"; +import type { ChatMessage, ModelOption } from "./types"; + +export function ChatInterface() { + const [messages, setMessages] = useState(MOCK_MESSAGES); + const [isLoading, setIsLoading] = useState(false); + const [selectedModel, setSelectedModel] = useState(MODELS[1]); + const [modelSelectorOpen, setModelSelectorOpen] = useState(false); + + const handleSend = useCallback((message: { text: string }) => { + if (!message.text.trim()) return; + + const userMessage: ChatMessage = { + id: `msg-${Date.now()}`, + role: "user", + content: message.text, + }; + + setMessages((prev) => [...prev, userMessage]); + setIsLoading(true); + + setTimeout(() => { + const assistantMessage: ChatMessage = { + id: `msg-${Date.now()}-reply`, + role: "assistant", + content: + "This is a mock response. The chat backend is not connected yet.", + }; + setMessages((prev) => [...prev, assistantMessage]); + setIsLoading(false); + }, 1000); + }, []); + + const handleSuggestion = useCallback( + (suggestion: string) => { + handleSend({ text: suggestion }); + }, + [handleSend], + ); + + return ( +
+ + + {messages.length === 0 ? ( + <> + } + /> + + {SUGGESTIONS.map((s) => ( + + ))} + + + ) : ( + messages.map((msg) => ( + + {msg.checkpoint && ( + + + + {msg.checkpoint} + + + )} + + + )) + )} + {isLoading && ( + + + + Thinking... + + + + )} + + + + +
+
+ {messages.length > 0 && ( + + {SUGGESTIONS.map((s) => ( + + ))} + + )} + + + + + + + + + + + + +
+ + +
+
+
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ChatMessageItem/ChatMessageItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ChatMessageItem/ChatMessageItem.tsx new file mode 100644 index 00000000000..25a7016859a --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ChatMessageItem/ChatMessageItem.tsx @@ -0,0 +1,86 @@ +import { + CodeBlock, + CodeBlockCopyButton, +} from "@superset/ui/ai-elements/code-block"; +import { + Message, + MessageAction, + MessageActions, + MessageContent, + MessageResponse, +} from "@superset/ui/ai-elements/message"; +import { + Reasoning, + ReasoningContent, + ReasoningTrigger, +} from "@superset/ui/ai-elements/reasoning"; +import { + Task, + TaskContent, + TaskItem, + TaskItemFile, + TaskTrigger, +} from "@superset/ui/ai-elements/task"; +import { HiMiniArrowPath, HiMiniClipboard } from "react-icons/hi2"; +import type { ChatMessage } from "../../types"; +import { PlanBlock } from "../PlanBlock"; +import { ToolCallBlock } from "../ToolCallBlock"; + +export function ChatMessageItem({ message }: { message: ChatMessage }) { + return ( + + + {message.reasoning && ( + + + {message.reasoning} + + )} + + {message.plan && } + + {message.content && ( + {message.content} + )} + + {message.tasks?.map((task) => ( + + + + {task.files.map((file) => ( + + {file} + + ))} + + + ))} + + {message.codeBlocks?.map((block) => ( + + + + ))} + + {message.toolCalls?.map((tc) => ( + + ))} + + + {message.role === "assistant" && message.content && ( + + + + + + + + + )} + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ChatMessageItem/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ChatMessageItem/index.ts new file mode 100644 index 00000000000..9d4b48394b5 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ChatMessageItem/index.ts @@ -0,0 +1 @@ +export { ChatMessageItem } from "./ChatMessageItem"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ContextIndicator/ContextIndicator.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ContextIndicator/ContextIndicator.tsx new file mode 100644 index 00000000000..da893e09e3c --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ContextIndicator/ContextIndicator.tsx @@ -0,0 +1,50 @@ +import { + Context, + ContextCacheUsage, + ContextContent, + ContextContentBody, + ContextContentFooter, + ContextContentHeader, + ContextInputUsage, + ContextOutputUsage, + ContextReasoningUsage, + ContextTrigger, +} from "@superset/ui/ai-elements/context"; + +const MOCK_CONTEXT = { + usedTokens: 84_200, + maxTokens: 200_000, + modelId: "claude-sonnet-4-5-20250929", + usage: { + inputTokens: 42_100, + outputTokens: 18_300, + totalTokens: 84_200, + reasoningTokens: 12_800, + cachedInputTokens: 11_000, + }, +} as const; + +export function ContextIndicator() { + return ( + + + + + +
+ + + + +
+
+ +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ContextIndicator/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ContextIndicator/index.ts new file mode 100644 index 00000000000..6dc9d24f2fe --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ContextIndicator/index.ts @@ -0,0 +1 @@ +export { ContextIndicator } from "./ContextIndicator"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx new file mode 100644 index 00000000000..bf57f434872 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/ModelPicker.tsx @@ -0,0 +1,64 @@ +import { + ModelSelector, + ModelSelectorContent, + ModelSelectorEmpty, + ModelSelectorGroup, + ModelSelectorInput, + ModelSelectorItem, + ModelSelectorList, + ModelSelectorLogo, + ModelSelectorName, + ModelSelectorTrigger, +} from "@superset/ui/ai-elements/model-selector"; +import { PromptInputButton } from "@superset/ui/ai-elements/prompt-input"; +import { MODELS } from "../../constants"; +import type { ModelOption } from "../../types"; + +export function ModelPicker({ + selectedModel, + onSelectModel, + open, + onOpenChange, +}: { + selectedModel: ModelOption; + onSelectModel: (model: ModelOption) => void; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + return ( + + + + + {selectedModel.name} + + + + + + No models found. + + {MODELS.map((model) => ( + { + onSelectModel(model); + onOpenChange(false); + }} + > + +
+ {model.name} + + {model.description} + +
+
+ ))} +
+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/index.ts new file mode 100644 index 00000000000..cdb83810862 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ModelPicker/index.ts @@ -0,0 +1 @@ +export { ModelPicker } from "./ModelPicker"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/PlanBlock/PlanBlock.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/PlanBlock/PlanBlock.tsx new file mode 100644 index 00000000000..ccfad86a4c8 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/PlanBlock/PlanBlock.tsx @@ -0,0 +1,43 @@ +import { + Plan, + PlanContent, + PlanDescription, + PlanHeader, + PlanTitle, +} from "@superset/ui/ai-elements/plan"; +import type { PlanData } from "../../types"; + +export function PlanBlock({ plan }: { plan: PlanData }) { + return ( + + +
+ {plan.title} + {plan.description} +
+
+ +
    + {plan.steps.map((step) => ( +
  • + + {step.done ? "\u2713" : "\u25CB"} + + + {step.label} + +
  • + ))} +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/PlanBlock/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/PlanBlock/index.ts new file mode 100644 index 00000000000..6225627aebc --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/PlanBlock/index.ts @@ -0,0 +1 @@ +export { PlanBlock } from "./PlanBlock"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ToolCallBlock/ToolCallBlock.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ToolCallBlock/ToolCallBlock.tsx new file mode 100644 index 00000000000..8718215dbba --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ToolCallBlock/ToolCallBlock.tsx @@ -0,0 +1,56 @@ +import { + Confirmation, + ConfirmationAction, + ConfirmationActions, + ConfirmationRequest, + ConfirmationTitle, +} from "@superset/ui/ai-elements/confirmation"; +import { + Tool, + ToolContent, + ToolHeader, + ToolInput, + ToolOutput, +} from "@superset/ui/ai-elements/tool"; +import type { ToolCall } from "../../types"; + +export function ToolCallBlock({ toolCall }: { toolCall: ToolCall }) { + return ( +
+ + + + + {(toolCall.output || toolCall.errorText) && ( + + )} + + + + {toolCall.approval && ( + + + {"approved" in toolCall.approval + ? toolCall.approval.approved + ? `${toolCall.name} was approved` + : `${toolCall.name} was denied` + : `Allow ${toolCall.name}?`} + + + + Deny + Allow + + + + )} +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ToolCallBlock/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ToolCallBlock/index.ts new file mode 100644 index 00000000000..589339b3253 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ToolCallBlock/index.ts @@ -0,0 +1 @@ +export { ToolCallBlock } from "./ToolCallBlock"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/constants.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/constants.ts new file mode 100644 index 00000000000..f4ea3c09da5 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/constants.ts @@ -0,0 +1,209 @@ +import type { ChatMessage, ModelOption } from "./types"; + +export const MODELS: ModelOption[] = [ + { + id: "claude-opus-4-6", + name: "Claude Opus 4.6", + description: "Most capable — complex tasks, deep reasoning", + }, + { + id: "claude-sonnet-4-5-20250929", + name: "Claude Sonnet 4.5", + description: "Balanced — fast and capable", + }, + { + id: "claude-haiku-4-5-20251001", + name: "Claude Haiku 4.5", + description: "Fastest — quick tasks, low cost", + }, +]; + +export const MOCK_MESSAGES: ChatMessage[] = [ + { + id: "1", + role: "user", + content: "Can you help me fix the authentication bug in the login flow?", + }, + { + id: "2", + role: "assistant", + content: "", + reasoning: + "The user wants help with an authentication bug. I should first plan my approach, then investigate the codebase to understand the current implementation before suggesting fixes. Let me start by searching for auth-related files.", + plan: { + title: "Fix authentication bug", + description: + "I'll investigate the login flow and fix the token validation issue.", + steps: [ + { label: "Search for auth-related files", done: true }, + { label: "Read the login implementation", done: true }, + { label: "Identify the bug in validateCredentials", done: true }, + { label: "Apply the fix", done: false }, + { label: "Run tests to verify", done: false }, + ], + }, + }, + { + id: "3", + role: "assistant", + content: + "Let me search for the relevant files and understand the current implementation.", + tasks: [ + { + title: "Found auth-related files", + files: [ + "src/auth/login.ts", + "src/auth/session.ts", + "src/auth/validators.ts", + "src/auth/types.ts", + ], + }, + ], + toolCalls: [ + { + id: "tc-1", + name: "Glob", + state: "output-available", + input: { pattern: "src/auth/**/*.ts" }, + output: + "src/auth/login.ts\nsrc/auth/session.ts\nsrc/auth/validators.ts\nsrc/auth/types.ts", + }, + { + id: "tc-2", + name: "Read", + state: "output-available", + input: { file_path: "src/auth/login.ts" }, + output: + "export async function login(credentials: Credentials) {\n const token = await validateCredentials(credentials);\n return createSession(token);\n}\n\nexport async function validateCredentials(credentials: Credentials) {\n const { email, password } = credentials;\n const user = await findUserByEmail(email);\n if (!user || !await verifyPassword(password, user.passwordHash)) {\n return null;\n }\n return generateToken(user);\n}", + }, + { + id: "tc-3", + name: "Grep", + state: "output-available", + input: { + pattern: "isTokenExpired|tokenExpir", + path: "src/auth/", + }, + output: "No matches found.", + }, + ], + checkpoint: "Investigation complete", + }, + { + id: "4", + role: "assistant", + content: + "I found the issue. The `login` function doesn't check if the returned token is null or expired before creating a session. Here's the fix:", + codeBlocks: [ + { + language: "typescript", + code: `export async function login(credentials: Credentials) { + const token = await validateCredentials(credentials); + if (!token || isTokenExpired(token)) { + throw new AuthError('Invalid or expired credentials'); + } + return createSession(token); +}`, + }, + ], + toolCalls: [ + { + id: "tc-4", + name: "Edit", + state: "output-available", + input: { + file_path: "src/auth/login.ts", + old_string: + "const token = await validateCredentials(credentials);\n return createSession(token);", + new_string: + "const token = await validateCredentials(credentials);\n if (!token || isTokenExpired(token)) {\n throw new AuthError('Invalid or expired credentials');\n }\n return createSession(token);", + }, + output: "File updated successfully.", + approval: { + id: "approval-1", + approved: true, + }, + }, + ], + }, + { + id: "5", + role: "assistant", + content: "Let me run the tests to make sure the fix works correctly.", + toolCalls: [ + { + id: "tc-5", + name: "Bash", + state: "output-error", + input: { command: "bun test src/auth/" }, + output: undefined, + errorText: + "FAIL src/auth/login.test.ts\n ✓ login with valid credentials (3ms)\n ✓ login with invalid credentials (1ms)\n ✗ login with expired token (2ms)\n Expected: AuthError\n Received: undefined\n\n Missing import: isTokenExpired is not defined", + }, + ], + }, + { + id: "6", + role: "assistant", + content: + "The test revealed I forgot to import `isTokenExpired`. Let me fix that and also add the missing `AuthError` import.", + toolCalls: [ + { + id: "tc-6", + name: "Edit", + state: "output-available", + input: { + file_path: "src/auth/login.ts", + old_string: 'import { generateToken } from "./token";', + new_string: + 'import { generateToken, isTokenExpired } from "./token";\nimport { AuthError } from "./errors";', + }, + output: "File updated successfully.", + approval: { + id: "approval-2", + approved: true, + }, + }, + { + id: "tc-7", + name: "Bash", + state: "output-available", + input: { command: "bun test src/auth/" }, + output: + "PASS src/auth/login.test.ts\n ✓ login with valid credentials (3ms)\n ✓ login with invalid credentials (1ms)\n ✓ login with expired token (2ms)\n\nTest Suites: 1 passed, 1 total\nTests: 3 passed, 3 total", + approval: { + id: "approval-3", + approved: true, + }, + }, + ], + }, + { + id: "7", + role: "assistant", + content: + "All tests pass now. The fix adds proper null-check and expiration validation to the `login` function, along with the missing imports for `isTokenExpired` and `AuthError`.", + codeBlocks: [ + { + language: "typescript", + code: `import { generateToken, isTokenExpired } from "./token"; +import { AuthError } from "./errors"; + +export async function login(credentials: Credentials) { + const token = await validateCredentials(credentials); + if (!token || isTokenExpired(token)) { + throw new AuthError('Invalid or expired credentials'); + } + return createSession(token); +}`, + }, + ], + }, +]; + +export const SUGGESTIONS = [ + "Explain this codebase", + "Fix the failing tests", + "Write tests for auth", + "Refactor to async/await", +]; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/index.ts new file mode 100644 index 00000000000..b95d3f925aa --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/index.ts @@ -0,0 +1 @@ +export { ChatInterface } from "./ChatInterface"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/types.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/types.ts new file mode 100644 index 00000000000..69cc150311f --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/types.ts @@ -0,0 +1,43 @@ +export type MessageRole = "user" | "assistant" | "system"; + +export interface ToolCall { + id: string; + name: string; + state: "input-available" | "output-available" | "output-error"; + input: Record; + output?: unknown; + errorText?: string; + approval?: + | { id: string } + | { id: string; approved: true; reason?: string } + | { id: string; approved: false; reason?: string }; +} + +export interface PlanData { + title: string; + description: string; + steps: Array<{ label: string; done: boolean }>; +} + +export interface TaskData { + title: string; + files: string[]; +} + +export interface ChatMessage { + id: string; + role: MessageRole; + content: string; + reasoning?: string; + toolCalls?: ToolCall[]; + plan?: PlanData; + codeBlocks?: Array<{ code: string; language: string }>; + tasks?: TaskData[]; + checkpoint?: string; +} + +export interface ModelOption { + id: string; + name: string; + description: string; +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPane.tsx new file mode 100644 index 00000000000..cd8fe505887 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPane.tsx @@ -0,0 +1,59 @@ +import type { MosaicBranch } from "react-mosaic-component"; +import { BasePaneWindow, PaneToolbarActions } from "../components"; +import { ChatInterface } from "./ChatInterface"; + +interface ChatPaneProps { + paneId: string; + path: MosaicBranch[]; + isActive: boolean; + tabId: string; + workspaceId: string; + splitPaneAuto: ( + tabId: string, + sourcePaneId: string, + dimensions: { width: number; height: number }, + path?: MosaicBranch[], + ) => void; + removePane: (paneId: string) => void; + setFocusedPane: (tabId: string, paneId: string) => void; +} + +export function ChatPane({ + paneId, + path, + isActive, + tabId, + workspaceId: _workspaceId, + splitPaneAuto, + removePane, + setFocusedPane, +}: ChatPaneProps) { + return ( + ( +
+
+ + Chat + +
+ +
+ )} + > + +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/index.ts new file mode 100644 index 00000000000..50356fa07cf --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/index.ts @@ -0,0 +1 @@ +export { ChatPane } from "./ChatPane"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx index d4f6ed59fe3..7e0fb8ad393 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx @@ -1,6 +1,8 @@ import "react-mosaic-component/react-mosaic-component.css"; import "./mosaic-theme.css"; +import { FEATURE_FLAGS } from "@superset/shared/constants"; +import { useFeatureFlagEnabled } from "posthog-js/react"; import { useCallback, useEffect, useMemo } from "react"; import { Mosaic, @@ -16,6 +18,7 @@ import { cleanLayout, extractPaneIdsFromLayout, } from "renderer/stores/tabs/utils"; +import { ChatPane } from "./ChatPane"; import { FileViewerPane } from "./FileViewerPane"; import { TabPane } from "./TabPane"; @@ -33,6 +36,7 @@ export function TabView({ tab }: TabViewProps) { const focusedPaneId = useTabsStore((s) => s.focusedPaneIds[tab.id]); const movePaneToTab = useTabsStore((s) => s.movePaneToTab); const movePaneToNewTab = useTabsStore((s) => s.movePaneToNewTab); + const hasAiChat = useFeatureFlagEnabled(FEATURE_FLAGS.AI_CHAT); const allTabs = useTabsStore((s) => s.tabs); const allPanes = useTabsStore((s) => s.panes); @@ -157,6 +161,22 @@ export function TabView({ tab }: TabViewProps) { ); } + // Route chat panes to ChatPane component + if (paneInfo.type === "chat" && hasAiChat) { + return ( + + ); + } + // Default: terminal panes return ( ()( return { tabId: tab.id, paneId: pane.id }; }, + addChatTab: (workspaceId: string) => { + const state = get(); + + const { tab, pane } = createChatTabWithPane(workspaceId, state.tabs); + + const currentActiveId = state.activeTabIds[workspaceId]; + const historyStack = state.tabHistoryStacks[workspaceId] || []; + const newHistoryStack = currentActiveId + ? [ + currentActiveId, + ...historyStack.filter((id) => id !== currentActiveId), + ] + : historyStack; + + set({ + tabs: [...state.tabs, tab], + panes: { ...state.panes, [pane.id]: pane }, + activeTabIds: { + ...state.activeTabIds, + [workspaceId]: tab.id, + }, + focusedPaneIds: { + ...state.focusedPaneIds, + [tab.id]: pane.id, + }, + tabHistoryStacks: { + ...state.tabHistoryStacks, + [workspaceId]: newHistoryStack, + }, + }); + + return { tabId: tab.id, paneId: pane.id }; + }, + addTabWithMultiplePanes: ( workspaceId: string, options: AddTabWithMultiplePanesOptions, diff --git a/apps/desktop/src/renderer/stores/tabs/types.ts b/apps/desktop/src/renderer/stores/tabs/types.ts index 58a613e88d0..dd6b7acb523 100644 --- a/apps/desktop/src/renderer/stores/tabs/types.ts +++ b/apps/desktop/src/renderer/stores/tabs/types.ts @@ -68,6 +68,7 @@ export interface TabsStore extends TabsState { workspaceId: string, options?: AddTabOptions, ) => { tabId: string; paneId: string }; + addChatTab: (workspaceId: string) => { tabId: string; paneId: string }; addTabWithMultiplePanes: ( workspaceId: string, options: AddTabWithMultiplePanesOptions, diff --git a/apps/desktop/src/renderer/stores/tabs/utils.ts b/apps/desktop/src/renderer/stores/tabs/utils.ts index d638edb5aac..f32cb6c883e 100644 --- a/apps/desktop/src/renderer/stores/tabs/utils.ts +++ b/apps/desktop/src/renderer/stores/tabs/utils.ts @@ -212,6 +212,48 @@ export const createFileViewerPane = ( }; }; +/** + * Creates a new chat pane + */ +export const createChatPane = (tabId: string): Pane => { + const id = generateId("pane"); + + return { + id, + tabId, + type: "chat", + name: "Chat", + chat: { + sessionId: generateId("chat-session"), + }, + }; +}; + +/** + * Creates a new tab with a chat pane atomically + */ +export const createChatTabWithPane = ( + workspaceId: string, + existingTabs: Tab[] = [], +): { tab: Tab; pane: Pane } => { + const tabId = generateId("tab"); + const pane = createChatPane(tabId); + + const workspaceTabs = existingTabs.filter( + (t) => t.workspaceId === workspaceId, + ); + + const tab: Tab = { + id: tabId, + name: `Chat ${workspaceTabs.filter((t) => t.name.startsWith("Chat")).length + 1}`, + workspaceId, + layout: pane.id, + createdAt: Date.now(), + }; + + return { tab, pane }; +}; + /** * Generates a static tab name based on existing tabs * (e.g., "Terminal 1", "Terminal 2", finding the next available number) diff --git a/apps/desktop/src/shared/tabs-types.ts b/apps/desktop/src/shared/tabs-types.ts index cdc8c56923f..f725b2a4297 100644 --- a/apps/desktop/src/shared/tabs-types.ts +++ b/apps/desktop/src/shared/tabs-types.ts @@ -8,7 +8,7 @@ import type { ChangeCategory } from "./changes-types"; /** * Pane types that can be displayed within a tab */ -export type PaneType = "terminal" | "webview" | "file-viewer"; +export type PaneType = "terminal" | "webview" | "file-viewer" | "chat"; /** * Pane status for agent lifecycle indicators @@ -118,6 +118,15 @@ export interface Pane { cwd?: string | null; // Current working directory cwdConfirmed?: boolean; // True if cwd confirmed via OSC-7, false if seeded fileViewer?: FileViewerState; // For file-viewer panes + chat?: ChatPaneState; // For chat panes +} + +/** + * Chat pane-specific properties + */ +export interface ChatPaneState { + /** Session ID for the chat session */ + sessionId: string; } /** diff --git a/docs/ai-chat-plan.md b/docs/ai-chat-plan.md index 78e520060ad..a52809bca7c 100644 --- a/docs/ai-chat-plan.md +++ b/docs/ai-chat-plan.md @@ -970,11 +970,11 @@ All files below are created and typechecking. Compatibility fix: `DurableStream. 3. ~~**Phase A2** — Vendor proxy into `apps/streams` (copy 17 files, adjust 3 import paths)~~ ✅ DONE 4. ~~**Phase B** — Claude agent endpoint + SDK-to-AI chunk converter (2 new files)~~ ✅ DONE 5. ~~**Phase C2** — Simplify desktop session manager~~ ✅ DONE -6. **Phase C3** — Handle drafts -7. **Phase D** — Database schema + migration -8. **Phase E** — API tRPC router -9. **Phase F** — Desktop chat UI -10. **Phase G** — Web chat UI +6. **Phase F** — Desktop chat UI (works with existing proxy, no DB needed) +7. **Phase C3** — Handle drafts (local state + typing indicators) +8. **Phase G** — Web chat UI +9. **Phase D** — Database schema + migration (persistent storage) +10. **Phase E** — API tRPC router (web session management) ---