Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/desktop/src/lib/trpc/routers/ui-state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
});

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FEATURE_FLAGS } from "@superset/shared/constants";
import { Button } from "@superset/ui/button";
import {
DropdownMenu,
Expand All @@ -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,
Expand Down Expand Up @@ -41,13 +44,15 @@ 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);
const movePaneToTab = useTabsStore((s) => s.movePaneToTab);
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();
Expand Down Expand Up @@ -89,6 +94,11 @@ export function GroupStrip() {
addTab(activeWorkspaceId);
};

const handleAddChat = () => {
if (!activeWorkspaceId) return;
addChatTab(activeWorkspaceId);
};

const handleSelectPreset = (preset: Parameters<typeof openPreset>[1]) => {
if (!activeWorkspaceId) return;
openPreset(activeWorkspaceId, preset);
Expand Down Expand Up @@ -176,6 +186,23 @@ export function GroupStrip() {
})}
</div>
)}
{hasAiChat && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7 shrink-0"
onClick={handleAddChat}
>
<HiMiniChatBubbleLeftRight className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" sideOffset={4}>
New Chat
</TooltipContent>
</Tooltip>
)}
<NewTabDropZone
onDrop={(paneId) => movePaneToNewTab(paneId)}
isLastPaneInTab={checkIsLastPaneInTab}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ChatMessage[]>(MOCK_MESSAGES);
const [isLoading, setIsLoading] = useState(false);
const [selectedModel, setSelectedModel] = useState<ModelOption>(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 (
<div className="flex h-full flex-col bg-background">
<Conversation className="flex-1">
<ConversationContent className="mx-auto w-full max-w-3xl gap-6 px-4 py-6">
{messages.length === 0 ? (
<>
<ConversationEmptyState
title="Start a conversation"
description="Ask anything to get started"
icon={<HiMiniChatBubbleLeftRight className="size-8" />}
/>
<Suggestions className="justify-center">
{SUGGESTIONS.map((s) => (
<Suggestion
key={s}
suggestion={s}
onClick={handleSuggestion}
/>
))}
</Suggestions>
</>
) : (
messages.map((msg) => (
<Fragment key={msg.id}>
{msg.checkpoint && (
<Checkpoint>
<CheckpointIcon />
<CheckpointTrigger tooltip="Restore to this point">
{msg.checkpoint}
</CheckpointTrigger>
</Checkpoint>
)}
<ChatMessageItem message={msg} />
</Fragment>
))
)}
{isLoading && (
<Message from="assistant">
<MessageContent>
<Shimmer className="text-sm" duration={1.5}>
Thinking...
</Shimmer>
</MessageContent>
</Message>
)}
</ConversationContent>
<ConversationScrollButton />
</Conversation>

<div className="border-t bg-background px-4 py-3">
<div className="mx-auto w-full max-w-3xl">
{messages.length > 0 && (
<Suggestions className="mb-3">
{SUGGESTIONS.map((s) => (
<Suggestion key={s} suggestion={s} onClick={handleSuggestion} />
))}
</Suggestions>
)}
<PromptInput onSubmit={handleSend}>
<PromptInputTextarea placeholder="Ask anything..." />
<PromptInputFooter>
<PromptInputTools>
<PromptInputButton>
<HiMiniPaperClip className="size-4" />
</PromptInputButton>
<PromptInputButton>
<HiMiniAtSymbol className="size-4" />
</PromptInputButton>
<ModelPicker
selectedModel={selectedModel}
onSelectModel={setSelectedModel}
open={modelSelectorOpen}
onOpenChange={setModelSelectorOpen}
/>
</PromptInputTools>
<div className="flex items-center gap-1">
<ContextIndicator />
<PromptInputSubmit
status={isLoading ? "streaming" : undefined}
/>
</div>
</PromptInputFooter>
</PromptInput>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 from={message.role}>
<MessageContent>
{message.reasoning && (
<Reasoning>
<ReasoningTrigger />
<ReasoningContent>{message.reasoning}</ReasoningContent>
</Reasoning>
)}

{message.plan && <PlanBlock plan={message.plan} />}

{message.content && (
<MessageResponse>{message.content}</MessageResponse>
)}

{message.tasks?.map((task) => (
<Task key={task.title}>
<TaskTrigger title={task.title} />
<TaskContent>
{task.files.map((file) => (
<TaskItem key={file}>
<TaskItemFile>{file}</TaskItemFile>
</TaskItem>
))}
</TaskContent>
</Task>
))}
Comment on lines +46 to +57
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

key={task.title} and key={file} may collide if titles or filenames repeat.

task.title (Line 47) and file (Line 51) are user-supplied strings that aren't guaranteed unique. Duplicate keys cause React reconciliation bugs (silent dropped renders). Consider using the array index or a composite key if no stable id is available.

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ChatMessageItem/ChatMessageItem.tsx`
around lines 46 - 57, The keys for the Task and TaskItem elements use
user-provided strings (key={task.title} and key={file}) which can collide;
update the mapping over message.tasks and task.files to produce stable, unique
keys (e.g., use a composite key that includes the task index or a stable id and
the title, and for files include the parent task id/title plus the file name or
its index) so change the key generation for the Task component in the
message.tasks.map and the TaskItem in task.files.map to a composite key like
`${task.id ?? task.title}-${taskIndex}` and `${task.id ??
task.title}-${file}-${fileIndex}` (or similar) using the map indices or stable
ids to guarantee uniqueness.


{message.codeBlocks?.map((block) => (
<CodeBlock
key={block.code}
code={block.code}
language={block.language as "typescript"}
>
<CodeBlockCopyButton />
</CodeBlock>
))}

{message.toolCalls?.map((tc) => (
<ToolCallBlock key={tc.id} toolCall={tc} />
))}
</MessageContent>

{message.role === "assistant" && message.content && (
<MessageActions>
<MessageAction tooltip="Copy">
<HiMiniClipboard className="size-3.5" />
</MessageAction>
<MessageAction tooltip="Retry">
<HiMiniArrowPath className="size-3.5" />
</MessageAction>
</MessageActions>
)}
Comment on lines +74 to +83
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Copy and Retry actions have no onClick handlers — buttons are non-functional.

Both MessageAction elements render icons but bind no behavior. Users will see clickable-looking controls that do nothing. If these are placeholders for future work, consider either wiring them up now or hiding them until functional (or adding a // TODO with tracking).

🤖 Prompt for AI Agents
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatInterface/components/ChatMessageItem/ChatMessageItem.tsx`
around lines 74 - 83, The MessageAction buttons rendered inside ChatMessageItem
(the MessageActions wrapper with MessageAction children containing
HiMiniClipboard and HiMiniArrowPath) have no onClick handlers, so the Copy and
Retry controls are non-functional; add appropriate onClick handlers to
MessageAction for copy and retry behavior (or conditionally hide them) by wiring
the Copy button to a copy handler that copies message.content (e.g.,
handleCopyMessage) and wiring the Retry button to a retry handler that
re-submits the message or invokes the existing retry logic (e.g.,
handleRetryMessage or call the parent prop like onRetryMessage), and ensure
these handlers are passed into/defined in ChatMessageItem and bound to the
MessageAction components.

</Message>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ChatMessageItem } from "./ChatMessageItem";
Original file line number Diff line number Diff line change
@@ -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 (
<Context
maxTokens={MOCK_CONTEXT.maxTokens}
modelId={MOCK_CONTEXT.modelId}
usage={MOCK_CONTEXT.usage}
usedTokens={MOCK_CONTEXT.usedTokens}
>
<ContextTrigger />
<ContextContent>
<ContextContentHeader />
<ContextContentBody>
<div className="space-y-1">
<ContextInputUsage />
<ContextOutputUsage />
<ContextReasoningUsage />
<ContextCacheUsage />
</div>
</ContextContentBody>
<ContextContentFooter />
</ContextContent>
</Context>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ContextIndicator } from "./ContextIndicator";
Loading