diff --git a/frontend/src/app/landing/page.tsx b/frontend/src/app/landing/page.tsx
new file mode 100644
index 000000000..e5d3187b7
--- /dev/null
+++ b/frontend/src/app/landing/page.tsx
@@ -0,0 +1,25 @@
+import { Footer } from "@/components/landing/footer";
+import { Header } from "@/components/landing/header";
+import { Hero } from "@/components/landing/hero";
+import { CaseStudySection } from "@/components/landing/sections/case-study-section";
+import { CommunitySection } from "@/components/landing/sections/community-section";
+import { SandboxSection } from "@/components/landing/sections/sandbox-section";
+import { SkillsSection } from "@/components/landing/sections/skills-section";
+import { WhatsNewSection } from "@/components/landing/sections/whats-new-section";
+
+export default function LandingPage() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index c36c68f11..78d4165ad 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -18,7 +18,7 @@ export default async function RootLayout({
const locale = await detectLocaleServer();
return (
-
+
{children}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index e5d3187b7..8483a6a36 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -1,25 +1,5 @@
-import { Footer } from "@/components/landing/footer";
-import { Header } from "@/components/landing/header";
-import { Hero } from "@/components/landing/hero";
-import { CaseStudySection } from "@/components/landing/sections/case-study-section";
-import { CommunitySection } from "@/components/landing/sections/community-section";
-import { SandboxSection } from "@/components/landing/sections/sandbox-section";
-import { SkillsSection } from "@/components/landing/sections/skills-section";
-import { WhatsNewSection } from "@/components/landing/sections/whats-new-section";
+import { redirect } from "next/navigation";
-export default function LandingPage() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- );
+export default function HomePage() {
+ redirect("/workspace");
}
diff --git a/frontend/src/components/ui/galaxy.jsx b/frontend/src/components/ui/galaxy.jsx
index c1d20ab84..37c0fe81d 100644
--- a/frontend/src/components/ui/galaxy.jsx
+++ b/frontend/src/components/ui/galaxy.jsx
@@ -335,7 +335,9 @@ export default function Galaxy({
ctn.removeEventListener("mousemove", handleMouseMove);
ctn.removeEventListener("mouseleave", handleMouseLeave);
}
- ctn.removeChild(gl.canvas);
+ if (gl.canvas.parentNode === ctn) {
+ ctn.removeChild(gl.canvas);
+ }
gl.getExtension("WEBGL_lose_context")?.loseContext();
};
}, [
diff --git a/frontend/src/components/ui/magic-bento.tsx b/frontend/src/components/ui/magic-bento.tsx
index 637d11b5c..29ea81cb2 100644
--- a/frontend/src/components/ui/magic-bento.tsx
+++ b/frontend/src/components/ui/magic-bento.tsx
@@ -130,7 +130,9 @@ const ParticleCard: React.FC<{
duration: 0.3,
ease: "back.in(1.7)",
onComplete: () => {
- particle.parentNode?.removeChild(particle);
+ if (particle.parentNode) {
+ particle.parentNode.removeChild(particle);
+ }
},
});
});
@@ -482,7 +484,9 @@ const GlobalSpotlight: React.FC<{
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseleave", handleMouseLeave);
- spotlightRef.current?.parentNode?.removeChild(spotlightRef.current);
+ if (spotlightRef.current?.parentNode) {
+ spotlightRef.current.parentNode.removeChild(spotlightRef.current);
+ }
};
}, [gridRef, disableAnimations, enabled, spotlightRadius, glowColor]);
diff --git a/frontend/src/core/messages/utils.ts b/frontend/src/core/messages/utils.ts
index 22f985009..666d6caca 100644
--- a/frontend/src/core/messages/utils.ts
+++ b/frontend/src/core/messages/utils.ts
@@ -40,17 +40,28 @@ export function getMessageGroups(messages: Message[]): MessageGroup[] {
const groups: MessageGroup[] = [];
- // Returns the last group if it can still accept tool messages
- // (i.e. it's an in-flight processing group, not a terminal human/assistant group).
+ // Returns the nearest prior group that can still accept tool messages.
+ // This intentionally scans backwards instead of only checking the last group,
+ // because an AI message can now render both:
+ // 1. an in-flight processing group for tool calls, and
+ // 2. a normal assistant bubble for user-visible text.
+ // In that case, the assistant bubble is appended after the processing group,
+ // and later tool messages still belong to that earlier processing group.
function lastOpenGroup() {
- const last = groups[groups.length - 1];
- if (
- last &&
- last.type !== "human" &&
- last.type !== "assistant" &&
- last.type !== "assistant:clarification"
- ) {
- return last;
+ for (let i = groups.length - 1; i >= 0; i -= 1) {
+ const group = groups[i];
+ if (!group) {
+ continue;
+ }
+ if (group.type === "human") {
+ return null;
+ }
+ if (
+ group.type !== "assistant" &&
+ group.type !== "assistant:clarification"
+ ) {
+ return group;
+ }
}
return null;
}
@@ -79,11 +90,6 @@ export function getMessageGroups(messages: Message[]): MessageGroup[] {
const open = lastOpenGroup();
if (open) {
open.messages.push(message);
- } else {
- console.error(
- "Unexpected tool message outside a processing group",
- message,
- );
}
}
continue;
@@ -116,9 +122,14 @@ export function getMessageGroups(messages: Message[]): MessageGroup[] {
}
}
- // Not an else-if: a message with reasoning + content (but no tool calls) goes
- // into the processing group above AND gets its own assistant bubble here.
- if (hasContent(message) && !hasToolCalls(message)) {
+ // Not an else-if: intermediate assistant messages can contain both
+ // user-facing text and tool calls. We still want to show that text as a
+ // normal assistant bubble instead of rendering only the tool timeline.
+ if (
+ hasContent(message) &&
+ !hasPresentFiles(message) &&
+ !hasSubagent(message)
+ ) {
groups.push({ id: message.id, type: "assistant", messages: [message] });
}
}
diff --git a/frontend/src/core/threads/export.ts b/frontend/src/core/threads/export.ts
index cf1f92e47..ffdec8f39 100644
--- a/frontend/src/core/threads/export.ts
+++ b/frontend/src/core/threads/export.ts
@@ -120,7 +120,9 @@ export function downloadAsFile(
a.download = filename;
document.body.appendChild(a);
a.click();
- document.body.removeChild(a);
+ if (a.parentNode === document.body) {
+ document.body.removeChild(a);
+ }
URL.revokeObjectURL(url);
}