diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts index 1f68dd6800e..ba10e28bcc3 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts @@ -5,7 +5,7 @@ import { localDb } from "main/lib/local-db"; import { workspaceInitManager } from "main/lib/workspace-init-manager"; import { z } from "zod"; import { publicProcedure, router } from "../../.."; -import { generateWorkspaceNameFromPrompt } from "../utils/ai-name"; +import { attemptWorkspaceAutoRenameFromPrompt } from "../utils/ai-name"; import { resolveWorkspaceBaseBranch } from "../utils/base-branch"; import { setBranchBaseConfig } from "../utils/base-branch-config"; import { @@ -290,6 +290,7 @@ export const createCreateProcedures = () => { z.object({ projectId: z.string(), name: z.string().optional(), + prompt: z.string().optional(), branchName: z.string().optional(), baseBranch: z.string().optional(), useExistingBranch: z.boolean().optional(), @@ -411,6 +412,24 @@ export const createCreateProcedures = () => { branch, name: input.name ?? branch, }); + let autoRenameWarning: string | undefined; + try { + const autoRenameResult = + await attemptWorkspaceAutoRenameFromPrompt({ + workspaceId: workspace.id, + prompt: input.prompt, + }); + autoRenameWarning = + autoRenameResult.status === "skipped" + ? autoRenameResult.warning + : undefined; + } catch (error) { + console.warn("[workspaces/create] Auto naming failed", { + workspaceId: workspace.id, + error: error instanceof Error ? error.message : String(error), + }); + autoRenameWarning = "Couldn't auto-name this workspace."; + } activateProject(project); const setupConfig = loadSetupConfig({ mainRepoPath: project.mainRepoPath, @@ -423,6 +442,7 @@ export const createCreateProcedures = () => { worktreePath: orphanedWorktree.path, projectId: project.id, isInitializing: false, + autoRenameWarning, wasExisting: true, }; } @@ -491,6 +511,7 @@ export const createCreateProcedures = () => { worktreePath, branch, mainRepoPath: project.mainRepoPath, + namingPrompt: input.prompt, useExistingBranch: input.useExistingBranch, }); @@ -952,14 +973,6 @@ export const createCreateProcedures = () => { workspaceName, }); }), - - generateName: publicProcedure - .input(z.object({ prompt: z.string().min(1) })) - .mutation(async ({ input }) => { - const name = await generateWorkspaceNameFromPrompt(input.prompt); - return { name }; - }), - importAllWorktrees: publicProcedure .input(z.object({ projectId: z.string() })) .mutation(async ({ input }) => { diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/ai-name.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/ai-name.ts index 0cf49fa417b..242d91437f7 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/ai-name.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/ai-name.ts @@ -1,71 +1,86 @@ import { createAnthropic } from "@ai-sdk/anthropic"; import { createOpenAI } from "@ai-sdk/openai"; -import { Agent } from "@mastra/core/agent"; +import type { Agent } from "@mastra/core/agent"; import { + generateTitleFromMessage, getCredentialsFromAnySource as getAnthropicCredentialsFromAnySource, + getAnthropicProviderOptions, getOpenAICredentialsFromAnySource, } from "@superset/chat/host"; +import { workspaces } from "@superset/local-db"; +import { and, eq, isNull } from "drizzle-orm"; +import { localDb } from "main/lib/local-db"; +import { getWorkspaceAutoRenameDecision } from "./workspace-auto-rename"; + +export type WorkspaceAutoRenameResult = + | { status: "renamed"; name: string } + | { + status: "skipped"; + reason: + | "empty-prompt" + | "missing-credentials" + | "generation-failed" + | "missing-workspace" + | "empty-generated-name" + | "workspace-deleting" + | "workspace-named" + | "workspace-name-changed"; + warning?: string; + }; type AgentModel = ConstructorParameters[0]["model"]; +type AnthropicCredentials = NonNullable< + ReturnType +>; +type OpenAICredentials = NonNullable< + ReturnType +>; interface TitleProvider { name: "Anthropic" | "OpenAI"; agentId: string; - resolveApiKey: () => string | null; - createModel: (apiKey: string) => AgentModel; + resolveCredentials: () => AnthropicCredentials | OpenAICredentials | null; + createModel: ( + credentials: AnthropicCredentials | OpenAICredentials, + ) => AgentModel; } const TITLE_PROVIDERS: TitleProvider[] = [ { name: "Anthropic", agentId: "workspace-namer-anthropic", - resolveApiKey: () => getAnthropicCredentialsFromAnySource()?.apiKey ?? null, - createModel: (apiKey) => - createAnthropic({ apiKey })("claude-haiku-4-5-20251001"), + resolveCredentials: () => getAnthropicCredentialsFromAnySource(), + createModel: (credentials) => + createAnthropic(getAnthropicProviderOptions(credentials))( + "claude-haiku-4-5-20251001", + ), }, { name: "OpenAI", agentId: "workspace-namer-openai", - resolveApiKey: () => getOpenAICredentialsFromAnySource()?.apiKey ?? null, - createModel: (apiKey) => createOpenAI({ apiKey })("gpt-4o-mini"), + resolveCredentials: () => getOpenAICredentialsFromAnySource(), + createModel: (credentials) => + createOpenAI({ apiKey: credentials.apiKey })("gpt-4o-mini"), }, ]; -async function generateTitleWithModel( - prompt: string, - agentId: string, - model: AgentModel, -): Promise { - const agent = new Agent({ - id: agentId, - name: "Workspace Namer", - instructions: "You generate concise workspace titles.", - model, - }); - - const title = await agent.generateTitleFromUserMessage({ - message: prompt, - tracingContext: {}, - }); - - return title?.trim() || null; -} - export async function generateWorkspaceNameFromPrompt( prompt: string, ): Promise { for (const provider of TITLE_PROVIDERS) { - const apiKey = provider.resolveApiKey(); - if (!apiKey) { + const credentials = provider.resolveCredentials(); + if (!credentials) { continue; } try { - const title = await generateTitleWithModel( - prompt, - provider.agentId, - provider.createModel(apiKey), - ); + const title = await generateTitleFromMessage({ + message: prompt, + agentModel: provider.createModel(credentials), + agentId: provider.agentId, + agentName: "Workspace Namer", + instructions: "You generate concise workspace titles.", + }); if (title) { return title; } @@ -79,3 +94,97 @@ export async function generateWorkspaceNameFromPrompt( return null; } + +export async function attemptWorkspaceAutoRenameFromPrompt({ + workspaceId, + prompt, +}: { + workspaceId: string; + prompt?: string | null; +}): Promise { + const cleanedPrompt = prompt?.trim(); + if (!cleanedPrompt) { + return { status: "skipped", reason: "empty-prompt" }; + } + + const generatedName = await generateWorkspaceNameFromPrompt(cleanedPrompt); + if (!generatedName) { + const hasCredentials = + getAnthropicCredentialsFromAnySource() !== null || + getOpenAICredentialsFromAnySource() !== null; + return { + status: "skipped", + reason: hasCredentials ? "generation-failed" : "missing-credentials", + warning: hasCredentials + ? "Couldn't auto-name this workspace." + : "Couldn't auto-name this workspace because chat credentials aren't configured.", + }; + } + + const workspace = localDb + .select({ + id: workspaces.id, + branch: workspaces.branch, + name: workspaces.name, + isUnnamed: workspaces.isUnnamed, + deletingAt: workspaces.deletingAt, + }) + .from(workspaces) + .where(eq(workspaces.id, workspaceId)) + .get(); + + const decision = getWorkspaceAutoRenameDecision({ + workspace: workspace ?? null, + generatedName, + }); + if (decision.kind === "skip") { + return { status: "skipped", reason: decision.reason }; + } + if (!workspace) { + return { status: "skipped", reason: "missing-workspace" }; + } + + const renameResult = localDb + .update(workspaces) + .set({ + name: decision.name, + isUnnamed: false, + updatedAt: Date.now(), + }) + .where( + and( + eq(workspaces.id, workspace.id), + eq(workspaces.branch, workspace.branch), + eq(workspaces.name, workspace.branch), + eq(workspaces.isUnnamed, true), + isNull(workspaces.deletingAt), + ), + ) + .run(); + if (renameResult.changes > 0) { + return { status: "renamed", name: decision.name }; + } + + const latestWorkspace = localDb + .select({ + branch: workspaces.branch, + name: workspaces.name, + isUnnamed: workspaces.isUnnamed, + deletingAt: workspaces.deletingAt, + }) + .from(workspaces) + .where(eq(workspaces.id, workspace.id)) + .get(); + + const latestDecision = getWorkspaceAutoRenameDecision({ + workspace: latestWorkspace ?? null, + generatedName, + }); + return { + status: "skipped", + reason: + latestDecision.kind === "skip" + ? latestDecision.reason + : "workspace-name-changed", + }; +} diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-auto-rename.test.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-auto-rename.test.ts new file mode 100644 index 00000000000..fe31a5ab78e --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-auto-rename.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, test } from "bun:test"; +import { + getWorkspaceAutoRenameDecision, + resolveWorkspaceAutoRename, +} from "./workspace-auto-rename"; + +describe("resolveWorkspaceAutoRename", () => { + test("returns generated name for untouched unnamed workspace", () => { + expect( + resolveWorkspaceAutoRename({ + workspace: { + branch: "feat/test-branch", + name: "feat/test-branch", + isUnnamed: true, + deletingAt: null, + }, + generatedName: "Fix auth flow", + }), + ).toBe("Fix auth flow"); + }); + + test("does not overwrite an already named workspace", () => { + expect( + resolveWorkspaceAutoRename({ + workspace: { + branch: "feat/test-branch", + name: "Custom name", + isUnnamed: false, + deletingAt: null, + }, + generatedName: "Fix auth flow", + }), + ).toBeNull(); + }); + + test("does not overwrite placeholder once another name has been applied", () => { + expect( + resolveWorkspaceAutoRename({ + workspace: { + branch: "feat/test-branch", + name: "Running setup", + isUnnamed: true, + deletingAt: null, + }, + generatedName: "Fix auth flow", + }), + ).toBeNull(); + }); + + test("ignores empty generated names", () => { + expect( + resolveWorkspaceAutoRename({ + workspace: { + branch: "feat/test-branch", + name: "feat/test-branch", + isUnnamed: true, + deletingAt: null, + }, + generatedName: " ", + }), + ).toBeNull(); + }); + + test("reports skip reason for already named workspace", () => { + expect( + getWorkspaceAutoRenameDecision({ + workspace: { + branch: "feat/test-branch", + name: "Custom name", + isUnnamed: false, + deletingAt: null, + }, + generatedName: "Fix auth flow", + }), + ).toEqual({ + kind: "skip", + reason: "workspace-named", + }); + }); +}); diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-auto-rename.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-auto-rename.ts new file mode 100644 index 00000000000..95ac16bcaf7 --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-auto-rename.ts @@ -0,0 +1,64 @@ +interface WorkspaceAutoRenameState { + branch: string; + name: string; + isUnnamed: boolean | null; + deletingAt?: number | null; +} + +interface ResolveWorkspaceAutoRenameParams { + workspace: WorkspaceAutoRenameState | null; + generatedName: string | null; +} + +type WorkspaceAutoRenameDecision = + | { + kind: "rename"; + name: string; + } + | { + kind: "skip"; + reason: + | "missing-workspace" + | "empty-generated-name" + | "workspace-deleting" + | "workspace-named" + | "workspace-name-changed"; + }; + +export function getWorkspaceAutoRenameDecision({ + workspace, + generatedName, +}: ResolveWorkspaceAutoRenameParams): WorkspaceAutoRenameDecision { + const cleanedGeneratedName = generatedName?.trim() ?? ""; + if (!workspace) { + return { kind: "skip", reason: "missing-workspace" }; + } + + if (!cleanedGeneratedName) { + return { kind: "skip", reason: "empty-generated-name" }; + } + + if (workspace.deletingAt != null) { + return { kind: "skip", reason: "workspace-deleting" }; + } + + if (!workspace.isUnnamed) { + return { kind: "skip", reason: "workspace-named" }; + } + + // Only replace the original branch placeholder. If the name has already + // changed, avoid overwriting it with a late async rename. + if (workspace.name !== workspace.branch) { + return { kind: "skip", reason: "workspace-name-changed" }; + } + + return { kind: "rename", name: cleanedGeneratedName }; +} + +export function resolveWorkspaceAutoRename({ + workspace, + generatedName, +}: ResolveWorkspaceAutoRenameParams): string | null { + const decision = getWorkspaceAutoRenameDecision({ workspace, generatedName }); + return decision.kind === "rename" ? decision.name : null; +} diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts index 200b7756a79..eedb6714f37 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts @@ -4,6 +4,7 @@ import { track } from "main/lib/analytics"; import { localDb } from "main/lib/local-db"; import { workspaceInitManager } from "main/lib/workspace-init-manager"; import type { WorkspaceInitStep } from "shared/types/workspace-init"; +import { attemptWorkspaceAutoRenameFromPrompt } from "./ai-name"; import { resolveWorkspaceBaseBranch } from "./base-branch"; import { getBranchBaseConfig, setBranchBaseConfig } from "./base-branch-config"; import { @@ -26,6 +27,7 @@ export interface WorkspaceInitParams { worktreePath: string; branch: string; mainRepoPath: string; + namingPrompt?: string; /** If true, use an existing branch instead of creating a new one */ useExistingBranch?: boolean; /** If true, skip worktree creation (worktree already exists on disk) */ @@ -45,10 +47,36 @@ export async function initializeWorkspaceWorktree({ worktreePath, branch, mainRepoPath, + namingPrompt, useExistingBranch, skipWorktreeCreation, }: WorkspaceInitParams): Promise { const manager = workspaceInitManager; + const completeReadyState = async (): Promise => { + let warning: string | undefined; + try { + const autoRenameResult = await attemptWorkspaceAutoRenameFromPrompt({ + workspaceId, + prompt: namingPrompt, + }); + warning = + autoRenameResult.status === "skipped" + ? autoRenameResult.warning + : undefined; + } catch (error) { + console.warn("[workspace-init] Auto naming failed", { + workspaceId, + error: error instanceof Error ? error.message : String(error), + }); + warning = "Couldn't auto-name this workspace."; + } + + if (manager.isCancellationRequested(workspaceId)) { + return; + } + + manager.updateProgress(workspaceId, "ready", "Ready", undefined, warning); + }; try { await manager.acquireProjectLock(projectId); @@ -141,7 +169,7 @@ export async function initializeWorkspaceWorktree({ .where(eq(worktrees.id, worktreeId)) .run(); - manager.updateProgress(workspaceId, "ready", "Ready"); + await completeReadyState(); track("workspace_initialized", { workspace_id: workspaceId, @@ -444,7 +472,7 @@ export async function initializeWorkspaceWorktree({ .where(eq(worktrees.id, worktreeId)) .run(); - manager.updateProgress(workspaceId, "ready", "Ready"); + await completeReadyState(); track("workspace_initialized", { workspace_id: workspaceId, diff --git a/apps/desktop/src/main/lib/workspace-init-manager.ts b/apps/desktop/src/main/lib/workspace-init-manager.ts index f50921d2f18..5602e1732b1 100644 --- a/apps/desktop/src/main/lib/workspace-init-manager.ts +++ b/apps/desktop/src/main/lib/workspace-init-manager.ts @@ -115,6 +115,7 @@ class WorkspaceInitManager extends EventEmitter { step: WorkspaceInitStep, message: string, error?: string, + warning?: string, ): void { const job = this.jobs.get(workspaceId); if (!job) { @@ -127,6 +128,7 @@ class WorkspaceInitManager extends EventEmitter { step, message, error, + warning, }; this.emit("progress", job.progress); diff --git a/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx b/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx index 47369ebb21c..e6831844e83 100644 --- a/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx +++ b/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx @@ -15,10 +15,7 @@ import { launchAgentSession } from "renderer/lib/agent-session-orchestrator"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { resolveEffectiveWorkspaceBaseBranch } from "renderer/lib/workspaceBaseBranch"; import { useOpenProject } from "renderer/react-query/projects"; -import { - useCreateWorkspace, - useUpdateWorkspace, -} from "renderer/react-query/workspaces"; +import { useCreateWorkspace } from "renderer/react-query/workspaces"; import { useCloseNewWorkspaceModal, useNewWorkspaceModalOpen, @@ -102,8 +99,6 @@ export function NewWorkspaceModal() { resolveInitialCommands: (commands) => runSetupScriptRef.current ? commands : null, }); - const updateWorkspace = useUpdateWorkspace(); - const generateName = electronTrpc.workspaces.generateName.useMutation(); const { openNew } = useOpenProject(); const selectableAgents = STARTABLE_AGENT_TYPES as readonly StartableAgentType[]; @@ -329,6 +324,7 @@ export function NewWorkspaceModal() { { projectId: selectedProjectId, name: workspaceName, + prompt: prompt || undefined, branchName: branchSlug || undefined, baseBranch: baseBranch || undefined, applyPrefix, @@ -338,25 +334,6 @@ export function NewWorkspaceModal() { : undefined, ); - if (prompt && !result.wasExisting) { - void (async () => { - try { - const res = await generateName.mutateAsync({ prompt }); - if (!res.name) return; - - await updateWorkspace.mutateAsync({ - id: result.workspace.id, - patch: { name: res.name, isUnnamed: false }, - }); - } catch (error) { - console.error( - "[new-workspace/title] Failed to generate/apply workspace name", - error, - ); - } - })(); - } - const launchRequest = launchRequestTemplate ? { ...launchRequestTemplate, diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts index df4189e5e97..dc2bae68abd 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts @@ -1,3 +1,4 @@ +import { toast } from "@superset/ui/sonner"; import { useNavigate } from "@tanstack/react-router"; import { useCallback, useRef } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; @@ -64,6 +65,12 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { updateProgress(optimisticProgress); } + if (!data.isInitializing && data.autoRenameWarning) { + toast.warning("Workspace created without auto-name", { + description: data.autoRenameWarning, + }); + } + if (!data.wasExisting) { const normalizedLaunchRequest = pendingSetupOverrides?.agentLaunchRequest diff --git a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx index 429735c4cf9..516ecd9b3f6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx @@ -1,4 +1,5 @@ import { Button } from "@superset/ui/button"; +import { toast } from "@superset/ui/sonner"; import { Spinner } from "@superset/ui/spinner"; import { createFileRoute, @@ -6,6 +7,7 @@ import { Outlet, useNavigate, } from "@tanstack/react-router"; +import { useRef } from "react"; import { DndProvider } from "react-dnd"; import { HiOutlineWifi } from "react-icons/hi2"; import { NewWorkspaceModal } from "renderer/components/NewWorkspaceModal"; @@ -41,6 +43,7 @@ function AuthenticatedLayout() { const isOnline = useOnlineStatus(); const navigate = useNavigate(); const utils = electronTrpc.useUtils(); + const shownWorkspaceInitWarningsRef = useRef(new Set()); const isSignedIn = env.SKIP_ENV_VALIDATION || !!session?.user; const activeOrganizationId = env.SKIP_ENV_VALIDATION @@ -56,6 +59,15 @@ function AuthenticatedLayout() { electronTrpc.workspaces.onInitProgress.useSubscription(undefined, { onData: (progress) => { updateInitProgress(progress); + if ( + progress.warning && + !shownWorkspaceInitWarningsRef.current.has(progress.workspaceId) + ) { + shownWorkspaceInitWarningsRef.current.add(progress.workspaceId); + toast.warning("Workspace created without auto-name", { + description: progress.warning, + }); + } if (progress.step === "ready" || progress.step === "failed") { // Invalidate both the grouped list AND the specific workspace utils.workspaces.getAllGrouped.invalidate(); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraPane.tsx index 2a81c105cf0..5c83584fe5f 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraPane.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatMastraPane/ChatMastraPane.tsx @@ -111,7 +111,8 @@ export function ChatMastraPane({ const paneName = pane?.name?.trim() ?? ""; const tabName = tab?.name?.trim() ?? ""; const hasCustomTabTitle = Boolean(tab?.userTitle?.trim()); - const shouldSetPaneTitle = paneName.length === 0 || paneName === "New Chat"; + const shouldSetPaneTitle = + paneName.length === 0 || paneName === "New Chat"; const shouldSetTabTitle = !hasCustomTabTitle && (tabName.length === 0 || diff --git a/apps/desktop/src/shared/types/workspace-init.ts b/apps/desktop/src/shared/types/workspace-init.ts index b19d22a4214..797fe6634bb 100644 --- a/apps/desktop/src/shared/types/workspace-init.ts +++ b/apps/desktop/src/shared/types/workspace-init.ts @@ -20,6 +20,7 @@ export interface WorkspaceInitProgress { step: WorkspaceInitStep; message: string; error?: string; + warning?: string; } export const INIT_STEP_MESSAGES: Record = { diff --git a/bun.lock b/bun.lock index 6d00e26199b..1b004d4e358 100644 --- a/bun.lock +++ b/bun.lock @@ -593,6 +593,7 @@ "name": "@superset/chat", "version": "0.0.1", "dependencies": { + "@mastra/core": "^1.3.0", "@trpc/server": "^11.7.1", "fast-glob": "^3.3.3", "fuse.js": "^7.1.0", @@ -618,6 +619,7 @@ "version": "0.0.1", "dependencies": { "@mastra/mcp": "^1.0.2", + "@superset/chat": "workspace:*", "@superset/trpc": "workspace:*", "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", @@ -862,15 +864,15 @@ "@a2a-js/sdk": ["@a2a-js/sdk@0.2.5", "", { "dependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.23", "body-parser": "^2.2.0", "cors": "^2.8.5", "express": "^4.21.2", "uuid": "^11.1.0" } }, "sha512-VTDuRS5V0ATbJ/LkaQlisMnTAeYKXAK6scMguVBstf+KIBQ7HIuKhiXLv+G/hvejkV+THoXzoNifInAkU81P1g=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.49", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EU/odEtJeqAWY9gRgCBQEK3m1p9nXPdGuvy4XF4q5TFJqUD+x5ykGUpJUL7Eo+pzXddHP+VyP8yW12FpB9EsYA=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2e1hBCKsd+7m0hELwrakR1QDfZfFhz9PF2d4qb8TxQueEyApo7ydlEWRpXeKC+KdA2FRV21dMb1G6FxdeNDa2w=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], "@ai-sdk/openai": ["@ai-sdk/openai@3.0.36", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-foY3onGY8l3q9niMw0Cwe9xrYnm46keIWL57NRw6F3DKzSW9TYTfx0cQJs/j8lXJ8lPzqNxpMO/zXOkqCUt3IQ=="], "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@ai-sdk/provider-utils-v5": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="], @@ -2704,7 +2706,7 @@ "aggregate-error": ["aggregate-error@4.0.1", "", { "dependencies": { "clean-stack": "^4.0.0", "indent-string": "^5.0.0" } }, "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w=="], - "ai": ["ai@6.0.104", "", { "dependencies": { "@ai-sdk/gateway": "3.0.58", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-boYGxbtdsa1YX3uuN7BV0FvAL3sGq7p/RLAMonK94jyt5C7sKj6jfib3/wD12koqX53htLTI/l4tX0HqNFRMZQ=="], + "ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="], "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -5460,10 +5462,14 @@ "@a2a-js/sdk/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="], + "@ai-sdk/provider-utils-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], "@ai-sdk/provider-utils-v6/@ai-sdk/provider": ["@ai-sdk/provider@3.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ=="], + "@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8XiKWbemmCbvNN0CLR9u3PQiet4gtEVIrX4zzLxnCj06AwsEDJwJVBbKrEI4t6qE8XRSIvU2irka0dcpziKW6w=="], + "@ai-sdk/react/ai": ["ai@6.0.94", "", { "dependencies": { "@ai-sdk/gateway": "3.0.52", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.15", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/F9wh262HbK05b/5vILh38JvPiheonT+kBj1L97712E7VPchqmcx7aJuZN3QSk5Pj6knxUJLm2FFpYJI1pHXUA=="], "@ai-sdk/ui-utils-v5/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="], @@ -6072,12 +6078,8 @@ "make-fetch-happen/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], - "mastracode/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="], - "mastracode/@ai-sdk/openai": ["@ai-sdk/openai@3.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A=="], - "mastracode/ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="], - "matcher/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -7516,14 +7518,6 @@ "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "mastracode/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - - "mastracode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - - "mastracode/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], - - "mastracode/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="], "metro-file-map/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], diff --git a/packages/chat-mastra/package.json b/packages/chat-mastra/package.json index 95725289671..5d260bbddc5 100644 --- a/packages/chat-mastra/package.json +++ b/packages/chat-mastra/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@mastra/mcp": "^1.0.2", + "@superset/chat": "workspace:*", "@superset/trpc": "workspace:*", "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", diff --git a/packages/chat-mastra/src/server/trpc/utils/runtime/runtime.ts b/packages/chat-mastra/src/server/trpc/utils/runtime/runtime.ts index 74bccce84e2..ae76adfc61c 100644 --- a/packages/chat-mastra/src/server/trpc/utils/runtime/runtime.ts +++ b/packages/chat-mastra/src/server/trpc/utils/runtime/runtime.ts @@ -294,10 +294,10 @@ export async function generateAndSetTitle( const agent = typeof mode.agent === "function" ? mode.agent({}) : mode.agent; - const title = await agent.generateTitleFromUserMessage({ + const title = await generateTitleFromMessage({ + agent, message: text, - model: runtime.harness.getFullModelId(), - tracingContext: {}, + modelId: runtime.harness.getFullModelId(), }); if (!title?.trim()) return; @@ -309,3 +309,5 @@ export async function generateAndSetTitle( console.warn("[chat-mastra] Title generation failed:", error); } } + +import { generateTitleFromMessage } from "@superset/chat/host"; diff --git a/packages/chat/package.json b/packages/chat/package.json index b0864cee189..842b6884744 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -22,6 +22,7 @@ "test": "bun test" }, "dependencies": { + "@mastra/core": "^1.3.0", "@trpc/server": "^11.7.1", "fast-glob": "^3.3.3", "fuse.js": "^7.1.0", diff --git a/packages/chat/src/host/auth/anthropic/anthropic.ts b/packages/chat/src/host/auth/anthropic/anthropic.ts index 54a5577534d..c73cdb48305 100644 --- a/packages/chat/src/host/auth/anthropic/anthropic.ts +++ b/packages/chat/src/host/auth/anthropic/anthropic.ts @@ -13,12 +13,42 @@ import { join } from "node:path"; import { createAuthStorage } from "mastracode"; import { ANTHROPIC_AUTH_PROVIDER_ID } from "../provider-ids"; -interface ClaudeCredentials { +export interface ClaudeCredentials { apiKey: string; source: "config" | "keychain" | "auth-storage" | "runtime-env"; kind: "apiKey" | "oauth"; } +export type AnthropicProviderOptions = + | { apiKey: string } + | { + authToken: string; + headers: { + "anthropic-beta": string; + "user-agent": string; + "x-app": string; + }; + }; + +export function getAnthropicProviderOptions( + credentials: ClaudeCredentials, +): AnthropicProviderOptions { + if (credentials.kind === "oauth") { + return { + authToken: credentials.apiKey, + headers: { + "anthropic-beta": "claude-code-20250219,oauth-2025-04-20", + "user-agent": "claude-cli/2.1.2 (external, cli)", + "x-app": "cli", + }, + }; + } + + return { + apiKey: credentials.apiKey, + }; +} + interface ClaudeConfigFile { apiKey?: string; api_key?: string; diff --git a/packages/chat/src/host/auth/anthropic/index.ts b/packages/chat/src/host/auth/anthropic/index.ts index fbe191a8446..238ba0bf902 100644 --- a/packages/chat/src/host/auth/anthropic/index.ts +++ b/packages/chat/src/host/auth/anthropic/index.ts @@ -1,4 +1,6 @@ +export type { AnthropicProviderOptions, ClaudeCredentials } from "./anthropic"; export { + getAnthropicProviderOptions, getCredentialsFromAnySource, getCredentialsFromAuthStorage, getCredentialsFromConfig, diff --git a/packages/chat/src/host/index.ts b/packages/chat/src/host/index.ts index c68c63fb77e..064d47c3472 100644 --- a/packages/chat/src/host/index.ts +++ b/packages/chat/src/host/index.ts @@ -1,4 +1,9 @@ +export type { + AnthropicProviderOptions, + ClaudeCredentials, +} from "./auth/anthropic"; export { + getAnthropicProviderOptions, getCredentialsFromAnySource, getCredentialsFromAuthStorage, getCredentialsFromConfig, @@ -13,3 +18,4 @@ export { export { ChatService } from "./chat-service"; export type { ChatServiceRouter } from "./router"; export { createChatServiceRouter } from "./router"; +export { generateTitleFromMessage } from "./title-generation"; diff --git a/packages/chat/src/host/title-generation/index.ts b/packages/chat/src/host/title-generation/index.ts new file mode 100644 index 00000000000..5dcc0b3bf42 --- /dev/null +++ b/packages/chat/src/host/title-generation/index.ts @@ -0,0 +1 @@ +export { generateTitleFromMessage } from "./title-generation"; diff --git a/packages/chat/src/host/title-generation/title-generation.ts b/packages/chat/src/host/title-generation/title-generation.ts new file mode 100644 index 00000000000..91c390fb3cf --- /dev/null +++ b/packages/chat/src/host/title-generation/title-generation.ts @@ -0,0 +1,53 @@ +import { Agent } from "@mastra/core/agent"; + +type TitleAgent = Pick; +type TitleModel = ConstructorParameters[0]["model"]; + +type GenerateTitleFromMessageParams = + | { + message: string; + agent: TitleAgent; + modelId: string; + tracingContext?: Record; + } + | { + message: string; + agentModel: TitleModel; + agentId?: string; + agentName?: string; + instructions?: string; + tracingContext?: Record; + }; + +export async function generateTitleFromMessage( + params: GenerateTitleFromMessageParams, +): Promise { + const { message, tracingContext = {} } = params; + const cleanedMessage = message.trim(); + if (!cleanedMessage) { + return null; + } + + if ("agent" in params) { + const title = await params.agent.generateTitleFromUserMessage({ + message: cleanedMessage, + model: params.modelId, + tracingContext, + }); + return title?.trim() || null; + } + + const titleAgent = new Agent({ + id: params.agentId ?? "title-generator", + name: params.agentName ?? "Title Generator", + instructions: params.instructions ?? "You generate concise titles.", + model: params.agentModel, + }); + + const title = await titleAgent.generateTitleFromUserMessage({ + message: cleanedMessage, + tracingContext, + }); + + return title?.trim() || null; +}