Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
145 changes: 110 additions & 35 deletions frontend/src-tauri/src/agent.rs

Large diffs are not rendered by default.

224 changes: 136 additions & 88 deletions frontend/src/components/AgentMode.tsx

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions frontend/src/components/DeleteChatDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ interface DeleteChatDialogProps {
onConfirm: () => void;
chatTitle: string;
description?: React.ReactNode;
itemLabel?: "chat" | "task";
}

export function DeleteChatDialog({
open,
onOpenChange,
onConfirm,
chatTitle,
description
description,
itemLabel = "chat"
}: DeleteChatDialogProps) {
const handleConfirm = (e: React.MouseEvent) => {
e.preventDefault();
Expand All @@ -34,7 +36,7 @@ export function DeleteChatDialog({
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete this chat?</AlertDialogTitle>
<AlertDialogTitle>Are you sure you want to delete this {itemLabel}?</AlertDialogTitle>
<AlertDialogDescription>
{description ?? (
<>This will permanently delete "{chatTitle}". This action cannot be undone.</>
Expand Down
23 changes: 20 additions & 3 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,16 @@ export function Sidebar({
isOpen,
mode = "chat",
navigationContent,
newItemDisabled = false,
onNewItem,
onToggle
}: {
chatId?: string;
isOpen: boolean;
mode?: WorkspaceMode;
navigationContent?: ReactNode;
newItemDisabled?: boolean;
onNewItem?: () => void;
onToggle: () => void;
}) {
const router = useRouter();
Expand Down Expand Up @@ -226,6 +230,18 @@ export function Sidebar({
const showAgentMode = agentModeAvailable && billingStatus !== null && agentModeEnabled === true;
const isAgentMode = mode === "agent";

async function addItem() {
if (!isAgentMode) {
await addChat();
return;
}

if (isOpen && isCompactLayout) {
onToggle();
}
onNewItem?.();
}

useEffect(() => {
if (!agentModeAvailable || !userId) return;

Expand Down Expand Up @@ -349,11 +365,12 @@ export function Sidebar({
<div className="flex flex-col gap-2 px-4">
<button
type="button"
className="flex w-full items-center justify-start gap-2 py-1.5 pr-1 pl-0 text-sm text-[hsl(var(--maple-primary-strong))] transition-colors hover:text-[hsl(var(--maple-primary))] dark:text-[hsl(var(--maple-primary))] dark:hover:text-[hsl(var(--maple-primary-strong))]"
onClick={addChat}
className="flex w-full items-center justify-start gap-2 py-1.5 pr-1 pl-0 text-sm text-[hsl(var(--maple-primary-strong))] transition-colors hover:text-[hsl(var(--maple-primary))] disabled:cursor-not-allowed disabled:opacity-50 dark:text-[hsl(var(--maple-primary))] dark:hover:text-[hsl(var(--maple-primary-strong))]"
disabled={isAgentMode && (newItemDisabled || !onNewItem)}
onClick={() => void addItem()}
>
<SquarePenIcon className="h-4 w-4 shrink-0" />
New Chat
{isAgentMode ? "New Task" : "New Chat"}
</button>
{!isAgentMode && (
<button
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/UpgradePromptDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export function UpgradePromptDialog({
"Work through multi-step coding tasks",
"Read and edit files in projects you choose",
"Run commands and development tools",
"Keep separate agent sessions for each project",
"Keep separate tasks for each project",
"Use supported private AI models with API access"
]
};
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/agent/AgentMcpControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export function AgentMcpServersDialog({
<DialogTitle>MCP servers</DialogTitle>
<DialogDescription>
Add tools for Agent Mode over Standard IO or Streamable HTTP. Default servers are
used for future chats; existing chats keep their own selection.
used for future tasks; existing tasks keep their own selection.
</DialogDescription>
</DialogHeader>

Expand All @@ -323,7 +323,7 @@ export function AgentMcpServersDialog({

<McpServerGroup
title="Default MCP servers"
description="Enabled automatically for new Agent chats."
description="Enabled automatically for new Agent tasks."
servers={enabledServers}
allServers={servers}
disabled={disabled || isSaving}
Expand Down Expand Up @@ -382,8 +382,8 @@ export function AgentMcpServersDialog({
<AlertDialogTitle>Delete this MCP server?</AlertDialogTitle>
<AlertDialogDescription>
{pendingDeleteIndex === null
? "This server will be removed from future chats."
: `“${servers[pendingDeleteIndex]?.name}” will be removed from future chats. Existing chats keep their current selection.`}
? "This server will be removed from future tasks."
: `“${servers[pendingDeleteIndex]?.name}” will be removed from future tasks. Existing tasks keep their current selection.`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
Expand Down Expand Up @@ -534,7 +534,7 @@ function McpServerForm({
<DialogHeader className="shrink-0 px-6 pt-6">
<DialogTitle>{isEditing ? "Edit MCP server" : "Add MCP server"}</DialogTitle>
<DialogDescription>
Maple saves this configuration without testing the connection. It will connect when a chat
Maple saves this configuration without testing the connection. It will connect when a task
enables the server.
</DialogDescription>
</DialogHeader>
Expand Down Expand Up @@ -668,7 +668,7 @@ function McpServerForm({
) : null}

<p className="text-xs text-muted-foreground">
Values are masked here and saved in this account’s local Agent data, including each chat’s
Values are masked here and saved in this account’s local Agent data, including each task’s
selected server snapshot. OAuth and legacy SSE transports are not supported in this first
version.
</p>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/chat/ChatTurn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ export function ChatDesktopConversationHeader({
title,
isSidebarOpen,
onNewChat,
newItemLabel = "New chat",
titleClassName
}: {
title: string;
isSidebarOpen: boolean;
onNewChat: () => void;
newItemLabel?: string;
titleClassName?: string;
}) {
return (
Expand All @@ -111,7 +113,7 @@ export function ChatDesktopConversationHeader({
size="icon"
className="absolute right-0 h-9 w-9 border-0"
onClick={onNewChat}
aria-label="New chat"
aria-label={newItemLabel}
>
<SquarePen className="h-4 w-4" />
</Button>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/settings/DeleteAccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ export function DeleteAccountSettings() {
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
This action cannot be undone. You will lose access to paid features, chat history, and
settings.
This action cannot be undone. You will lose access to paid features, chat and task
history, and settings.
</AlertDescription>
</Alert>

Expand Down
20 changes: 11 additions & 9 deletions frontend/src/components/settings/HistorySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ export function HistorySettings() {
try {
await navigate({ to: "/", ignoreBlocker: true });
} catch (navigationError) {
console.error("Chat history was deleted, but navigation failed:", navigationError);
console.error("History was deleted, but navigation failed:", navigationError);
window.location.href = "/";
return;
}
window.dispatchEvent(new CustomEvent("newchat", { detail: { projectId: null } }));
} catch (deleteError) {
console.error("Error deleting chat history:", deleteError);
setError("Maple could not delete all chat history. Please try again.");
console.error("Error deleting chat and task history:", deleteError);
setError("Maple could not delete all chat and task history. Please try again.");
} finally {
operationBlock?.release();
setIsDeleting(false);
Expand All @@ -60,19 +60,21 @@ export function HistorySettings() {

return (
<SettingsPage
title="Chat history"
description="Manage the conversations stored in your private Maple workspace."
title="Chat and task history"
description="Manage the chats and tasks stored in your private Maple workspace."
>
<SettingsSection
title="Delete all chat history"
description="This permanently removes every conversation and project conversation from your account."
title="Delete all chat and task history"
description="This permanently removes every chat and task from your Maple workspace."
tone="danger"
>
<div className="space-y-4">
{error && <AlertDestructive title="Chat history was not deleted" description={error} />}
{error && (
<AlertDestructive title="Chat and task history was not deleted" description={error} />
)}
{isConfirming ? (
<div className="rounded-lg border border-destructive/40 bg-destructive/5 p-4">
<p className="text-sm font-medium">Delete your entire chat history?</p>
<p className="text-sm font-medium">Delete your entire chat and task history?</p>
<p className="mt-1 text-sm text-muted-foreground">This action cannot be undone.</p>
<div className="mt-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<Button
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/settings/SettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ function SettingsLayoutContent() {
},
{
label: "Data",
items: [{ label: "Chat history", to: "/settings/history", icon: Database }]
items: [{ label: "Chat and task history", to: "/settings/history", icon: Database }]
},
{
label: "Maple",
Expand Down
24 changes: 23 additions & 1 deletion frontend/src/services/agentMcpErrors.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import {
agentTaskErrorMessage,
conciseMcpError,
isMcpConnectionErrorEvent,
mcpConnectionErrorMessage,
Expand All @@ -20,7 +21,7 @@ describe("MCP connection errors", () => {
expect(message).not.toContain("WorkerTransport");
});

test("keeps the server name in structured session errors", () => {
test("keeps the server name in structured task errors", () => {
const message = mcpConnectionErrorMessage([{ name: "fixture_http", error: RAW_HTTP_ERROR }]);

expect(message).toContain("fixture_http");
Expand All @@ -38,13 +39,34 @@ describe("MCP connection errors", () => {
expect(message).not.toContain("WorkerTransport");
});

test("preserves session terminology returned by an external MCP server", () => {
expect(
userFacingAgentError("Some MCP servers could not connect: calendar: Session not found")
).toBe("Some MCP servers could not connect. calendar: Session not found");
expect(agentTaskErrorMessage("Failed to connect MCP server: Session not found")).toBe(
"Failed to connect MCP server: Session not found"
);
});

test("leaves unrelated Agent errors unchanged", () => {
expect(userFacingAgentError("The selected model is unavailable")).toBe(
"The selected model is unavailable"
);
expect(isMcpConnectionErrorEvent("The selected model is unavailable")).toBe(false);
});

test("translates known runtime session errors without rewriting unrelated text", () => {
expect(userFacingAgentError("Failed to load Agent task: Session not found")).toBe(
"Failed to load Agent task: Task not found"
);
expect(
agentTaskErrorMessage("Goose stream failed: Please try again or create a new session.")
).toBe("Goose stream failed: Please try again or create a new task.");
expect(agentTaskErrorMessage("MCP server session-cache failed")).toBe(
"MCP server session-cache failed"
);
});

test("classifies authentication, timeout, and STDIO startup failures", () => {
expect(conciseMcpError("HTTP 401 Unauthorized")).toBe(
"The server rejected the connection. Check its authentication headers."
Expand Down
33 changes: 32 additions & 1 deletion frontend/src/services/agentMcpErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,43 @@ export function mcpConnectionErrorMessage(

export function userFacingAgentError(message: string): string {
const singleLine = message.replace(/\s+/g, " ").trim();
if (!isMcpConnectionErrorEvent(singleLine)) return message;
if (!isMcpConnectionErrorEvent(singleLine)) return agentTaskErrorMessage(message);

const detail = singleLine.slice(MCP_EVENT_PREFIX.length).trim();
return `Some MCP servers could not connect. ${conciseMcpError(detail)}`;
}

export function agentTaskErrorMessage(message: string): string {
const isMapleTaskError = [
"Agent task",
"Failed to create Agent task",
"Failed to list Agent tasks",
"Failed to load Agent task",
"Failed to reload Agent task",
"Failed to find Agent task",
"Failed to delete Agent task",
"Failed to inspect cancelled Agent task",
"Failed to restore cancelled Agent task",
"Failed to name Agent task",
"Failed to load named Agent task",
"Failed to load updated Agent task",
"Failed to load Agent for task",
"Failed to save task MCP settings",
"Goose reply failed:",
"Goose stream failed:"
].some((prefix) => message.startsWith(prefix));

if (!isMapleTaskError) return message;

return message
.replace(/\bSession not found\b/g, "Task not found")
.replace(/\bsession not found\b/g, "task not found")
.replace(/\bCreate a new session\b/g, "Create a new task")
.replace(/\bcreate a new session\b/g, "create a new task")
.replace(/\bStart a new session\b/g, "Start a new task")
.replace(/\bstart a new session\b/g, "start a new task");
}

export function isMcpConnectionErrorEvent(message: string): boolean {
return message.replace(/\s+/g, " ").trim().startsWith(MCP_EVENT_PREFIX);
}
Expand Down
Loading