Skip to content
Closed
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
4 changes: 4 additions & 0 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ export default defineConfig({
process.env.NEXT_PUBLIC_DOCS_URL,
"https://docs.superset.sh",
),
"process.env.NEXT_PUBLIC_STREAMS_URL": defineEnv(
process.env.NEXT_PUBLIC_STREAMS_URL,
"http://localhost:8080",
),
Comment on lines +147 to +150
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

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the electron.vite.config.ts file around lines 147-150
cat -n apps/desktop/electron.vite.config.ts | sed -n '140,160p'

Repository: superset-sh/superset

Length of output: 917


🏁 Script executed:

# Let's see the full context of the defineEnv usage and the surrounding code
cat -n apps/desktop/electron.vite.config.ts | head -200

Repository: superset-sh/superset

Length of output: 7056


🏁 Script executed:

# Find where defineEnv is defined
rg "defineEnv" apps/desktop/ -A 3 -B 3

Repository: superset-sh/superset

Length of output: 6264


🏁 Script executed:

# Check if there are environment-specific branching or guards
rg "process.env.NODE_ENV|development|production" apps/desktop/electron.vite.config.ts -B 2 -A 2

Repository: superset-sh/superset

Length of output: 707


🏁 Script executed:

# Let's examine the defineEnv function implementation fully
cat -n apps/desktop/vite/helpers.ts | sed -n '1,150p' | grep -A 10 "export function defineEnv"

Repository: superset-sh/superset

Length of output: 421


🏁 Script executed:

# Let's see the complete defineEnv function with more context
sed -n '/export function defineEnv/,/^}/p' apps/desktop/vite/helpers.ts

Repository: superset-sh/superset

Length of output: 195


🏁 Script executed:

# Check if NEXT_PUBLIC_STREAMS_URL is used anywhere else in the codebase
rg "NEXT_PUBLIC_STREAMS_URL" --type ts --type tsx -B 2 -A 2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

# Check if there's any runtime validation for streams URL
rg "STREAMS_URL|streams.*url" apps/desktop/src/renderer -i -B 2 -A 2

Repository: superset-sh/superset

Length of output: 7748


🏁 Script executed:

# Let's check the env validation more carefully to understand the full picture
cat -n apps/desktop/src/renderer/env.renderer.ts

Repository: superset-sh/superset

Length of output: 2482


🏁 Script executed:

# Check if there's build-time environment setup or documentation about required env vars
find apps/desktop -name "*.md" -o -name ".env*" -o -name "*.env" | head -20

Repository: superset-sh/superset

Length of output: 929


🏁 Script executed:

# Let's verify what other NEXT_PUBLIC URLs default to
grep -n "NEXT_PUBLIC.*default\|NEXT_PUBLIC.*fallback" apps/desktop/electron.vite.config.ts apps/desktop/src/renderer/env.renderer.ts

Repository: superset-sh/superset

Length of output: 518


🏁 Script executed:

# Check for any build scripts or CI configuration that might set these env vars
find . -name "*.yml" -o -name "*.yaml" -o -name "Dockerfile*" -o -name "*.sh" | grep -E "build|deploy|ci|github" | head -20

Repository: superset-sh/superset

Length of output: 439


🏁 Script executed:

# Let's check if there's any documentation about building for production
cat -n apps/desktop/BUILDING.md 2>/dev/null | head -100

Repository: superset-sh/superset

Length of output: 534


🏁 Script executed:

# Check if there's any distinction in how env vars are set between dev and prod builds
rg "NEXT_PUBLIC_STREAMS_URL" -B 5 -A 5 | head -50

Repository: superset-sh/superset

Length of output: 4600


🏁 Script executed:

# Let's also verify the pattern: do other URLs also have production fallbacks, and what's the intended pattern?
grep -n "NEXT_PUBLIC.*:" apps/desktop/electron.vite.config.ts | grep "define"

Repository: superset-sh/superset

Length of output: 673


Fix inconsistent localhost default for NEXT_PUBLIC_STREAMS_URL.

The fallback to "http://localhost:8080" is inconsistent with other NEXT_PUBLIC URLs (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_WEB_URL, NEXT_PUBLIC_DOCS_URL), which all default to production endpoints. This will break chat functionality in production if the environment variable is not set at build time. Either require NEXT_PUBLIC_STREAMS_URL for production builds or provide a production-appropriate fallback.

🤖 Prompt for AI Agents
In `@apps/desktop/electron.vite.config.ts` around lines 147 - 150, The
NEXT_PUBLIC_STREAMS_URL fallback uses "http://localhost:8080" which is
inconsistent with other NEXT_PUBLIC_* defaults and will break production; update
the defineEnv usage for process.env.NEXT_PUBLIC_STREAMS_URL so that in
production it either throws or requires the env to be present or returns the
production endpoint used by the other NEXT_PUBLIC_* variables; specifically
modify the call to defineEnv(process.env.NEXT_PUBLIC_STREAMS_URL,
"http://localhost:8080") to instead use the production fallback URL (matching
the pattern of NEXT_PUBLIC_API_URL/NEXT_PUBLIC_WEB_URL/NEXT_PUBLIC_DOCS_URL) or
add a guard that throws during production builds when
process.env.NEXT_PUBLIC_STREAMS_URL is undefined, ensuring chat functionality
isn't left pointing to localhost.

"import.meta.env.DEV_SERVER_PORT": defineEnv(String(DEV_SERVER_PORT)),
"import.meta.env.NEXT_PUBLIC_POSTHOG_KEY": defineEnv(
process.env.NEXT_PUBLIC_POSTHOG_KEY,
Expand Down
8 changes: 5 additions & 3 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"dependencies": {
"@better-auth/stripe": "1.4.17",
"@durable-streams/client": "^0.2.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
Expand All @@ -45,16 +46,17 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@sentry/electron": "^7.7.0",
"@superset/ai-chat": "workspace:*",
"@superset/auth": "workspace:*",
"@superset/db": "workspace:*",
"@superset/local-db": "workspace:*",
"@superset/shared": "workspace:*",
"@superset/trpc": "workspace:*",
"@superset/ui": "workspace:*",
"@t3-oss/env-core": "^0.13.8",
"@tanstack/db": "0.5.22",
"@tanstack/electric-db-collection": "0.2.27",
"@tanstack/react-db": "0.1.66",
"@tanstack/db": "^0.5.24",
"@tanstack/electric-db-collection": "0.2.30",
"@tanstack/react-db": "^0.1.68",
"@tanstack/react-query": "^5.90.19",
"@tanstack/react-router": "^1.147.3",
"@tanstack/react-table": "^8.21.3",
Expand Down
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
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/env.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const envSchema = z.object({
.default("development"),
NEXT_PUBLIC_API_URL: z.url().default("https://api.superset.sh"),
NEXT_PUBLIC_WEB_URL: z.url().default("https://app.superset.sh"),
NEXT_PUBLIC_STREAMS_URL: z.url().default("http://localhost:8080"),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"),
SENTRY_DSN_DESKTOP: z.string().optional(),
Expand All @@ -33,6 +34,7 @@ const rawEnv = {
NODE_ENV: process.env.NODE_ENV,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_WEB_URL: process.env.NEXT_PUBLIC_WEB_URL,
NEXT_PUBLIC_STREAMS_URL: process.env.NEXT_PUBLIC_STREAMS_URL,
NEXT_PUBLIC_POSTHOG_KEY: import.meta.env.NEXT_PUBLIC_POSTHOG_KEY as
| string
| undefined,
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
- default-src 'self': Only allow resources from same origin
- script-src 'self' 'wasm-unsafe-eval' https://*.posthog.com: Allow scripts from same origin + WebAssembly (for xterm ImageAddon) + PostHog
- style-src 'self' 'unsafe-inline': Allow styles from same origin + inline (needed for CSS-in-JS)
- connect-src 'self' ws: wss: %NEXT_PUBLIC_API_URL% https://*.posthog.com https://*.sentry.io sentry-ipc:: Allow WebSocket + API (includes Electric proxy) + PostHog + Sentry
- connect-src 'self' ws: wss: http://localhost:* %NEXT_PUBLIC_API_URL% https://*.posthog.com https://*.sentry.io sentry-ipc:: Allow WebSocket + API (includes Electric proxy) + PostHog + Sentry + localhost for stream server
- img-src 'self' data: %NEXT_PUBLIC_API_URL% https://*.public.blob.vercel-storage.com https://github.com https://avatars.githubusercontent.com: Allow images from same origin + data URIs + API (Linear image proxy) + Vercel blob storage + GitHub avatars
- font-src 'self': Allow fonts from same origin
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://*.posthog.com; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss: %NEXT_PUBLIC_API_URL% https://*.posthog.com https://*.sentry.io sentry-ipc:; img-src 'self' data: %NEXT_PUBLIC_API_URL% https://*.public.blob.vercel-storage.com https://github.com https://avatars.githubusercontent.com; font-src 'self';" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://*.posthog.com; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss: http://localhost:* %NEXT_PUBLIC_API_URL% https://*.posthog.com https://*.sentry.io sentry-ipc:; img-src 'self' data: %NEXT_PUBLIC_API_URL% https://*.public.blob.vercel-storage.com https://github.com https://avatars.githubusercontent.com; font-src 'self';" />
</head>

<body>
Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/renderer/lib/workspace-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Get the most recently opened workspace path from grouped workspaces.
*/
export function getMostRecentWorkspacePath(
groups: Array<{
workspaces: Array<{
worktreePath: string;
lastOpenedAt: number;
}>;
}>,
): string | null {
const allWorkspaces = groups.flatMap((g) => g.workspaces);
if (allWorkspaces.length === 0) return null;

const sorted = [...allWorkspaces].sort(
(a, b) => b.lastOpenedAt - a.lastOpenedAt,
);
return sorted[0].worktreePath || null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { createStream } from "@superset/ai-chat/stream";
import { Button } from "@superset/ui/button";
import { ScrollArea } from "@superset/ui/scroll-area";
import { cn } from "@superset/ui/utils";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useCallback, useEffect } from "react";
import { env } from "renderer/env.renderer";
import { ChatView } from "../components/ChatView";
import { useChatStore } from "../stores/chatStore";

export const Route = createFileRoute(
"/_authenticated/_dashboard/chats/$chatId/",
)({
component: ChatDetailPage,
});

function ChatDetailPage() {
const { chatId } = Route.useParams();
const navigate = useNavigate();
const { sessions, createSession } = useChatStore();

// Ensure stream exists when page loads
useEffect(() => {
createStream(env.NEXT_PUBLIC_STREAMS_URL, chatId).catch((err) => {
console.error("[chats] Failed to ensure stream exists on load:", err);
});
}, [chatId]);

const handleCreateChat = useCallback(async () => {
const session = createSession();
try {
await createStream(env.NEXT_PUBLIC_STREAMS_URL, session.id);
} catch (err) {
console.error("[chats] Failed to create stream:", err);
}
navigate({ to: "/chats/$chatId", params: { chatId: session.id } });
}, [navigate, createSession]);

const handleSelectChat = useCallback(
async (id: string) => {
try {
await createStream(env.NEXT_PUBLIC_STREAMS_URL, id);
} catch (err) {
console.error("[chats] Failed to ensure stream exists:", err);
}
navigate({ to: "/chats/$chatId", params: { chatId: id } });
},
[navigate],
);

return (
<div className="flex h-full">
{/* Sidebar */}
<div className="w-64 border-r border-border flex flex-col bg-muted/30">
<div className="p-3 border-b border-border">
<Button onClick={handleCreateChat} className="w-full" size="sm">
+ New Chat
</Button>
</div>

<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
{sessions.length === 0 ? (
<div className="text-center text-muted-foreground text-sm py-8 px-4">
No chats yet.
</div>
) : (
sessions.map((session) => (
<button
key={session.id}
type="button"
onClick={() => handleSelectChat(session.id)}
className={cn(
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors",
"hover:bg-accent hover:text-accent-foreground",
chatId === session.id && "bg-accent text-accent-foreground",
)}
>
<span className="truncate block">{session.name}</span>
<span className="text-xs text-muted-foreground">
{new Date(session.createdAt).toLocaleDateString()}
</span>
</button>
))
)}
</div>
</ScrollArea>
</div>

{/* Chat View */}
<div className="flex-1 min-w-0">
<ChatView sessionId={chatId} className="h-full" />
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Individual chat message component
*/

import type { BetaContentBlock, ToolResult } from "@superset/ai-chat/stream";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@superset/ui/collapsible";
import { cn } from "@superset/ui/utils";
import { useState } from "react";
import { LuChevronRight } from "react-icons/lu";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import remarkGfm from "remark-gfm";
import { ToolCallPart } from "../ToolCallPart";

export interface ChatMessageProps {
role?: "user" | "assistant";
content: string;
contentBlocks?: BetaContentBlock[];
toolResults?: Map<string, ToolResult>;
timestamp?: Date;
isStreaming?: boolean;
}

function ThinkingBlock({ thinking }: { thinking: string }) {
const [isOpen, setIsOpen] = useState(false);

return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors">
<LuChevronRight
className={cn("h-3 w-3 transition-transform", isOpen && "rotate-90")}
/>
Thinking
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-1 rounded border border-border bg-muted/30 p-2 text-xs text-muted-foreground italic whitespace-pre-wrap">
{thinking}
</div>
</CollapsibleContent>
</Collapsible>
);
}

function AssistantContent({
content,
contentBlocks,
toolResults,
}: {
content: string;
contentBlocks?: BetaContentBlock[];
toolResults?: Map<string, ToolResult>;
}) {
if (!contentBlocks || contentBlocks.length === 0) {
return (
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
>
{content}
</ReactMarkdown>
</div>
);
}

return (
<div className="space-y-3">
{contentBlocks.map((block, index) => {
const key = `${block.type}-${index}`;
switch (block.type) {
case "text":
return (
<div
key={key}
className="prose prose-sm dark:prose-invert max-w-none"
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]}
>
{block.text}
</ReactMarkdown>
</div>
);
case "tool_use":
return (
<ToolCallPart
key={block.id}
block={block}
result={toolResults?.get(block.id)}
/>
);
case "thinking":
return <ThinkingBlock key={key} thinking={block.thinking} />;
default:
return (
<div
key={key}
className="rounded border border-border bg-muted/30 p-2 text-xs text-muted-foreground"
>
<span className="font-mono">{block.type}</span> block
</div>
);
}
})}
</div>
);
}

export function ChatMessage({
role = "assistant",
content,
contentBlocks,
toolResults,
timestamp,
isStreaming,
}: ChatMessageProps) {
const isUser = role === "user";

return (
<div
className={cn("flex w-full", isUser ? "justify-end" : "justify-start")}
>
<div
className={cn(
"max-w-[80%] rounded-lg px-4 py-3",
isUser
? "bg-primary text-primary-foreground"
: "bg-muted text-foreground",
)}
>
{isUser ? (
<p className="whitespace-pre-wrap">{content}</p>
) : (
<AssistantContent
content={content}
contentBlocks={contentBlocks}
toolResults={toolResults}
/>
)}
{timestamp && (
<span className="mt-1 block text-xs opacity-60">
{timestamp.toLocaleTimeString()}
</span>
)}
{isStreaming && (
<span className="mt-1 inline-block h-4 w-1 animate-pulse bg-current" />
)}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { ChatMessageProps } from "./ChatMessage";
export { ChatMessage } from "./ChatMessage";
Loading
Loading