Skip to content
Merged
1 change: 0 additions & 1 deletion apps/mesh/src/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export interface Organization {
export interface SidebarItem {
title: string;
url: string;
connectionId: string;
icon: string;
}

Expand Down
1 change: 0 additions & 1 deletion apps/mesh/src/tools/organization/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { z } from "zod";
export const SidebarItemSchema = z.object({
title: z.string(),
url: z.string(),
connectionId: z.string(),
icon: z.string(),
});

Expand Down
6 changes: 2 additions & 4 deletions apps/mesh/src/web/components/chat/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useChat as useAiChat } from "@ai-sdk/react";
import type { UseChatHelpers } from "@ai-sdk/react";
import { DecoChatAside } from "@deco/ui/components/deco-chat-aside.tsx";
import { DecoChatInputV2 } from "@deco/ui/components/deco-chat-input-v2.tsx";
import { cn } from "@deco/ui/lib/utils.ts";
Expand Down Expand Up @@ -35,9 +35,7 @@ export type {

export type ChatMessage = UIMessage<Metadata>;

export type ChatStatus = ReturnType<
typeof useAiChat<UIMessage<Metadata>>
>["status"];
export type ChatStatus = UseChatHelpers<UIMessage<Metadata>>["status"];

function useChatAutoScroll({
messageCount,
Expand Down
75 changes: 35 additions & 40 deletions apps/mesh/src/web/components/chat/message-assistant.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cn } from "@deco/ui/lib/utils.ts";
import { Metadata } from "@deco/ui/types/chat-metadata.ts";
import type { DynamicToolUIPart, ToolUIPart } from "ai";
import type { ToolUIPart } from "ai";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { Target04, Stars01, Lightbulb01 } from "@untitledui/icons";
import { MessageProps } from "./message-user.tsx";
Expand Down Expand Up @@ -70,46 +70,39 @@ function ThoughtSummary({ duration }: { duration: number }) {
);
}

function renderPart(
part: MessageProps<Metadata>["message"]["parts"][number],
id: string,
index: number,
usageStats?: ReactNode,
) {
const isToolCall =
part.type.startsWith("tool-") || part.type === "dynamic-tool";
const shouldSkip =
part.type === "step-start" ||
part.type === "file" ||
part.type === "source-url" ||
part.type === "source-document";
if (shouldSkip) {
return null;
}
if (isToolCall) {
return (
<ToolCallPart
key={`${id}-${index}`}
part={part as ToolUIPart | DynamicToolUIPart}
id={id}
/>
);
}
type MessagePart = MessageProps<Metadata>["message"]["parts"][number];

interface MessagePartProps {
part: MessagePart;
id: string;
usageStats?: ReactNode;
}

function MessagePart({ part, id, usageStats }: MessagePartProps) {
switch (part.type) {
case "dynamic-tool":
return <ToolCallPart part={part} id={id} />;
case "text":
return (
<MessageTextPart
key={`${id}-${index}`}
id={id}
text={part.text}
copyable={true}
part={part}
extraActions={usageStats}
copyable
/>
);
case "reasoning":
return (
<MessageReasoningPart key={`${id}-${index}`} part={part} id={id} />
);
return <MessageReasoningPart part={part} id={id} />;
case "step-start":
case "file":
case "source-url":
case "source-document":
return null;
default: {
if (part.type.startsWith("tool-")) {
return <ToolCallPart part={part as ToolUIPart} id={id} />;
}
}
}

throw new Error(`Unknown part type: ${part.type}`);
Expand Down Expand Up @@ -173,14 +166,16 @@ export function MessageAssistant<T extends Metadata>({
{hasContent ? (
<>
{showThought && <ThoughtSummary duration={duration} />}
{parts.map((part, index) =>
renderPart(
part,
id,
index,
index === parts.length - 1 ? usageStats : undefined,
),
)}
{parts.map((part, index) => (
<MessagePart
key={`${id}-${index}`}
part={part}
id={id}
usageStats={
index === parts.length - 1 ? usageStats : undefined
}
/>
))}
</>
) : isLoading ? (
<TypingIndicator />
Expand Down
10 changes: 3 additions & 7 deletions apps/mesh/src/web/components/chat/message-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function MessageUser<T extends Metadata>({
{" "}
<div
onClick={handleClick}
className="w-full border min-w-0 shadow-[0_3px_6px_-1px_rgba(0,0,0,0.1)] rounded-lg text-[0.9375rem] break-words overflow-wrap-anywhere bg-muted px-4 py-2 cursor-pointer transition-colors"
className="w-full border min-w-0 shadow-[0_3px_6px_-1px_rgba(0,0,0,0.1)] rounded-lg text-[0.9375rem] wrap-break-word overflow-wrap-anywhere bg-muted px-4 py-2 cursor-pointer transition-colors"
>
<div
className={cn(
Expand All @@ -67,17 +67,13 @@ export function MessageUser<T extends Metadata>({
{parts.map((part, index) => {
if (part.type === "text") {
return (
<MessageTextPart
key={`${id}-${index}`}
id={id}
text={part.text}
/>
<MessageTextPart key={`${id}-${index}`} id={id} part={part} />
);
}
return null;
})}
{isLongMessage && !isExpanded && (
<div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-muted to-transparent pointer-events-none" />
<div className="absolute bottom-0 left-0 right-0 h-12 bg-linear-to-t from-muted to-transparent pointer-events-none" />
)}
</div>
{isLongMessage && (
Expand Down
9 changes: 5 additions & 4 deletions apps/mesh/src/web/components/chat/parts/text-part.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,26 @@ import { useCopy } from "@deco/ui/hooks/use-copy.ts";
import { Button } from "@deco/ui/components/button.tsx";
import { MemoizedMarkdown } from "@deco/ui/components/chat/chat-markdown.tsx";
import { Check, Copy01 } from "@untitledui/icons";
import type { TextUIPart } from "ai";

interface MessageTextPartProps {
id: string;
text: string;
part: TextUIPart;
copyable?: boolean;
extraActions?: ReactNode;
}

export function MessageTextPart({
id,
text,
part,
copyable = false,
extraActions,
}: MessageTextPartProps) {
const { handleCopy } = useCopy();
const [isCopied, setIsCopied] = useState(false);

const handleCopyMessage = async () => {
await handleCopy(text);
await handleCopy(part.text);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
Expand All @@ -30,7 +31,7 @@ export function MessageTextPart({

return (
<div className="group/part relative">
<MemoizedMarkdown id={id} text={text} />
<MemoizedMarkdown id={id} text={part.text} />
{showActions && (
<div className="flex w-full items-center text-xs text-muted-foreground opacity-0 pointer-events-none transition-all duration-200 group-hover/part:opacity-100 group-hover/part:pointer-events-auto mt-2">
<div className="flex items-center gap-1">
Expand Down
37 changes: 22 additions & 15 deletions apps/mesh/src/web/components/chat/side-panel-chat.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
import { generatePrefixedId } from "@/shared/utils/generate-id";
import { EmptyState } from "@/web/components/empty-state";
import { IntegrationIcon } from "@/web/components/integration-icon";
import { useDecoChatOpen } from "@/web/hooks/use-deco-chat-open";
import { authClient } from "@/web/lib/auth-client";
import { useProjectContext } from "@/web/providers/project-context-provider";
import { Button } from "@deco/ui/components/button.tsx";
import { DecoChatEmptyState } from "@deco/ui/components/deco-chat-empty-state.tsx";
import { CpuChip02, Plus, X, Loading01 } from "@untitledui/icons";
import type { Metadata } from "@deco/ui/types/chat-metadata.ts";
import { useNavigate } from "@tanstack/react-router";
import { Chat, useGateways, useModels, type ModelChangePayload } from "./chat";
import { CpuChip02, Loading01, Plus, X } from "@untitledui/icons";
import { Suspense, useState } from "react";
import { toast } from "sonner";
import { useThreads } from "../../hooks/use-chat-store";
import { useLocalStorage } from "../../hooks/use-local-storage";
import { LOCALSTORAGE_KEYS } from "../../lib/localstorage-keys";
import { useChat } from "../../providers/chat-provider";
import { ThreadHistoryPopover } from "./thread-history-popover";
import type { Metadata } from "@deco/ui/types/chat-metadata.ts";
import { usePersistedChat } from "../../hooks/use-persisted-chat";
import { useConnections } from "../../hooks/collections/use-connection";
import {
useConnectionActions,
useConnections,
} from "../../hooks/collections/use-connection";
import { useBindingConnections } from "../../hooks/use-binding";
import { useSystemPrompt } from "../../hooks/use-system-prompt";
import { useThreads } from "../../hooks/use-chat-store";
import {
useGatewayPrompts,
type GatewayPrompt,
} from "../../hooks/use-gateway-prompts";
import { IceBreakers } from "./ice-breakers";
import { Suspense, useState } from "react";
import { useInvalidateCollectionsOnToolCall } from "../../hooks/use-invalidate-collections-on-tool-call";
import { useLocalStorage } from "../../hooks/use-local-storage";
import { usePersistedChat } from "../../hooks/use-persisted-chat";
import { useSystemPrompt } from "../../hooks/use-system-prompt";
import { LOCALSTORAGE_KEYS } from "../../lib/localstorage-keys";
import { useChat } from "../../providers/chat-provider";
import { ErrorBoundary } from "../error-boundary";
import { useConnectionActions } from "../../hooks/collections/use-connection";
import { generatePrefixedId } from "@/shared/utils/generate-id";
import { Chat, useGateways, useModels, type ModelChangePayload } from "./chat";
import { IceBreakers } from "./ice-breakers";
import { ThreadHistoryPopover } from "./thread-history-popover";

// Capybara avatar URL from decopilotAgent
const CAPYBARA_AVATAR_URL =
Expand Down Expand Up @@ -130,10 +133,14 @@ export function ChatPanel() {
// Generate dynamic system prompt based on context
const systemPrompt = useSystemPrompt();

// Get the onToolCall handler for invalidating collection queries
const onToolCall = useInvalidateCollectionsOnToolCall();

// Use shared persisted chat hook - must be called unconditionally (Rules of Hooks)
const chat = usePersistedChat({
threadId: activeThreadId,
systemPrompt,
onToolCall,
onCreateThread: (thread) =>
createThread({ id: thread.id, title: thread.title }),
});
Expand Down
Loading