Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion packages/core/src/client/AgentTaskCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useRef, useCallback } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
IconLoader2,
IconCheck,
Expand Down Expand Up @@ -182,7 +183,7 @@ export function AgentTaskCard({
ref={previewRef}
className="rounded-md bg-muted/30 px-3 py-2 text-xs text-muted-foreground break-words max-h-48 overflow-y-auto agent-markdown prose prose-sm prose-invert max-w-none"
>
<ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{displayText.length > 800
? "..." + displayText.slice(-800)
: displayText}
Expand Down
34 changes: 29 additions & 5 deletions packages/core/src/client/AssistantChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "@assistant-ui/react";
import { MarkdownTextPrimitive } from "@assistant-ui/react-markdown";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { createAgentChatAdapter } from "./agent-chat-adapter.js";
import { type ContentPart, readSSEStreamRaw } from "./sse-event-processor.js";
import { cn } from "./utils.js";
Expand Down Expand Up @@ -98,7 +99,11 @@ function MarkdownText() {
injectMarkdownStyles();
}, []);
return (
<MarkdownTextPrimitive smooth className="agent-markdown break-words" />
<MarkdownTextPrimitive
smooth
className="agent-markdown break-words"
remarkPlugins={[remarkGfm]}
/>
);
}

Expand Down Expand Up @@ -342,7 +347,9 @@ function ToolCallFallback({
ref={streamRef}
className="mt-1 rounded-md bg-muted/50 px-3 py-2 text-xs text-muted-foreground break-words max-h-48 overflow-y-auto agent-markdown prose prose-sm prose-invert max-w-none"
>
<ReactMarkdown>{agentStreamText}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{agentStreamText}
</ReactMarkdown>
</div>
)}
{isExpanded && !isAgentCall && result !== undefined && (
Expand Down Expand Up @@ -472,7 +479,9 @@ function ReconnectStreamToolCall({
ref={streamRef}
className="mt-1 rounded-md bg-muted/50 px-3 py-2 text-xs text-muted-foreground break-words max-h-48 overflow-y-auto agent-markdown prose prose-sm prose-invert max-w-none"
>
<ReactMarkdown>{agentStreamText}</ReactMarkdown>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{agentStreamText}
</ReactMarkdown>
</div>
)}
{isExpanded && !isAgentCall && result !== undefined && (
Expand Down Expand Up @@ -763,10 +772,10 @@ function AssistantMessage() {
// ─── Thinking Indicator ─────────────────────────────────────────────────────

function ThinkingIndicator() {
const [dots, setDots] = useState(1);
const [dots, setDots] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setDots((d) => (d % 3) + 1);
setDots((d) => (d + 1) % 4);
}, 400);
return () => clearInterval(interval);
}, []);
Expand Down Expand Up @@ -995,6 +1004,7 @@ const AssistantChatInner = forwardRef<
const [showContinue, setShowContinue] = useState(false);
const [isReconnecting, setIsReconnecting] = useState(false);
const [reconnectContent, setReconnectContent] = useState<ContentPart[]>([]);
const reconnectRunIdRef = useRef<string | null>(null);
const wasRunningRef = useRef(false);
const tiptapRef = useRef<TiptapComposerHandle>(null);

Expand Down Expand Up @@ -1044,8 +1054,15 @@ const AssistantChatInner = forwardRef<
if (runRes.ok) {
const runInfo = await runRes.json();
// Agent is still running — subscribe to live SSE stream
reconnectRunIdRef.current = runInfo.runId;
setIsReconnecting(true);
setReconnectContent([]);
// Signal tab running indicator
window.dispatchEvent(
new CustomEvent("builder.chatRunning", {
detail: { isRunning: true, tabId: tabId || threadId },
}),
);

const streamReconnect = async () => {
try {
Expand Down Expand Up @@ -1106,6 +1123,13 @@ const AssistantChatInner = forwardRef<
} catch {}
setReconnectContent([]);
setIsReconnecting(false);
reconnectRunIdRef.current = null;
// Signal tab stopped
window.dispatchEvent(
new CustomEvent("builder.chatRunning", {
detail: { isRunning: false, tabId: tabId || threadId },
}),
);
};
streamReconnect();
}
Expand Down
8 changes: 8 additions & 0 deletions templates/analytics/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ pnpm action hubspot-deals --grep="enterprise" --fields=dealname,amount,stageLabe
3. **Update skills directly** when you discover new gotchas or patterns.
4. **Learn from corrections** — capture in the relevant skill or LEARNINGS.md resource.

## UI Components

**Always use shadcn/ui components** from `app/components/ui/` for all standard UI patterns (dialogs, popovers, dropdowns, tooltips, buttons, etc). Never build custom modals or dropdowns with absolute/fixed positioning — use the shadcn primitives instead.

**Always use Tabler Icons** (`@tabler/icons-react`) for all icons. Never use other icon libraries.

**Never use browser dialogs** (`window.confirm`, `window.alert`, `window.prompt`) — use shadcn AlertDialog instead.

## TypeScript Everywhere

All code must be TypeScript (`.ts`). Never create `.js`, `.cjs`, or `.mjs` files. Use ESM imports.
Expand Down
8 changes: 8 additions & 0 deletions templates/calendar/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,11 @@ The `--to` bound is exclusive, so use tomorrow's date for today's events.
2. **Actions for backend logic** — anything the agent needs to execute goes through `pnpm action`.
3. **Context-first** — always run `view-screen` before acting. Know what the user sees.
4. **Always query Google Calendar** — use `list-events` or `search-events` for schedule questions. Never return empty results without running a script first.

### UI Components

**Always use shadcn/ui components** from `app/components/ui/` for all standard UI patterns (dialogs, popovers, dropdowns, tooltips, buttons, etc). Never build custom modals or dropdowns with absolute/fixed positioning — use the shadcn primitives instead.

**Always use Tabler Icons** (`@tabler/icons-react`) for all icons. Never use other icon libraries.

**Never use browser dialogs** (`window.confirm`, `window.alert`, `window.prompt`) — use shadcn AlertDialog instead.
14 changes: 9 additions & 5 deletions templates/calendar/app/components/calendar/CreateEventDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,25 @@ interface CreateEventPopoverProps {
open: boolean;
onOpenChange: (open: boolean) => void;
defaultDate?: Date;
defaultStartTime?: string;
defaultEndTime?: string;
}

export function CreateEventPopover({
open,
onOpenChange,
defaultDate,
defaultStartTime: defaultStart,
defaultEndTime: defaultEnd,
}: CreateEventPopoverProps) {
const today = defaultDate || new Date();
const defaultDateStr = format(today, "yyyy-MM-dd");

const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [date, setDate] = useState(defaultDateStr);
const [startTime, setStartTime] = useState("09:00");
const [endTime, setEndTime] = useState("10:00");
const [startTime, setStartTime] = useState(defaultStart || "09:00");
const [endTime, setEndTime] = useState(defaultEnd || "10:00");
const [location, setLocation] = useState("");
const [allDay, setAllDay] = useState(false);

Expand All @@ -45,12 +49,12 @@ export function CreateEventPopover({
setTitle("");
setDescription("");
setDate(format(defaultDate || new Date(), "yyyy-MM-dd"));
setStartTime("09:00");
setEndTime("10:00");
setStartTime(defaultStart || "09:00");
setEndTime(defaultEnd || "10:00");
setLocation("");
setAllDay(false);
}
}, [open, defaultDate]);
}, [open, defaultDate, defaultStart, defaultEnd]);

// ⌘+Enter to submit
useEffect(() => {
Expand Down
92 changes: 88 additions & 4 deletions templates/calendar/app/components/calendar/DayView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,51 @@ interface DayViewProps {
onEditEvent: (event: CalendarEvent) => void;
onDeleteEvent: (eventId: string) => void;
onEventTimeChange?: (eventId: string, newStart: Date, newEnd: Date) => void;
onClickTimeSlot?: (date: Date, startTime: string, endTime: string) => void;
quickEditEventId?: string | null;
onQuickEditSave?: (eventId: string, title: string) => void;
onQuickEditCancel?: (eventId: string) => void;
isLoading?: boolean;
}

function QuickEditInput({
eventId,
onSave,
onCancel,
}: {
eventId: string;
onSave: (eventId: string, title: string) => void;
onCancel: (eventId: string) => void;
}) {
const [value, setValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
requestAnimationFrame(() => inputRef.current?.focus());
}, []);

return (
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onSave(eventId, value);
} else if (e.key === "Escape") {
e.preventDefault();
onCancel(eventId);
}
e.stopPropagation();
}}
onBlur={() => onSave(eventId, value)}
Comment thread
builder-io-integration[bot] marked this conversation as resolved.
Outdated
placeholder="(No title)"
className="w-full bg-transparent text-[11px] font-semibold text-foreground placeholder:text-foreground/40 outline-none leading-tight"
/>
);
}

// [startHour, startMin, durationMin, widthPct]
const DAY_SKELETONS: [number, number, number, number][] = [
[9, 0, 60, 82],
Expand Down Expand Up @@ -94,6 +136,10 @@ export function DayView({
onEditEvent,
onDeleteEvent,
onEventTimeChange,
onClickTimeSlot,
quickEditEventId,
onQuickEditSave,
onQuickEditCancel,
isLoading = false,
}: DayViewProps) {
const [now, setNow] = useState(new Date());
Expand Down Expand Up @@ -255,7 +301,28 @@ export function DayView({
</div>

{/* Positioned events overlay */}
<div className="absolute inset-0 ml-[56px] mr-4">
<div
className="absolute inset-0 ml-[56px] mr-4"
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) return;
if (!onClickTimeSlot || isDragging || shouldSuppressClick()) return;
const rect = e.currentTarget.getBoundingClientRect();
const y = e.clientY - rect.top;
const totalMinutes =
Math.floor(((y / HOUR_HEIGHT) * 60) / 15) * 15 + START_HOUR * 60;
const startH = Math.floor(totalMinutes / 60);
const startM = totalMinutes % 60;
const endMinutes = totalMinutes + 60;
const endH = Math.min(Math.floor(endMinutes / 60), 23);
const endM = endMinutes % 60;
const pad = (n: number) => String(n).padStart(2, "0");
onClickTimeSlot(
date,
`${pad(startH)}:${pad(startM)}`,
`${pad(endH)}:${pad(endM)}`,
);
}}
>
{/* Current time indicator */}
{showNowIndicator && (
<div
Expand Down Expand Up @@ -380,7 +447,21 @@ export function DayView({
opacity: isBeingDragged && isDragging ? 0.9 : undefined,
}}
>
{durationMin <= 30 ? (
{quickEditEventId === event.id &&
onQuickEditSave &&
onQuickEditCancel ? (
<div className="flex flex-col justify-center flex-1 min-w-0 mt-0.5">
<QuickEditInput
eventId={event.id}
onSave={onQuickEditSave}
onCancel={onQuickEditCancel}
/>
<div className="mt-0.5 truncate text-[10px] leading-tight text-foreground/60">
{format(displayStart, "h:mm a")} –{" "}
{format(displayEnd, "h:mm a")}
</div>
</div>
) : durationMin <= 30 ? (
<div className="flex items-baseline gap-1.5 truncate">
<span
className={cn(
Expand Down Expand Up @@ -471,8 +552,11 @@ export function DayView({
</button>
);

// Don't wrap in popover while dragging
if (isBeingDragged && isDragging) {
// Don't wrap in popover while dragging or quick-editing
if (
(isBeingDragged && isDragging) ||
quickEditEventId === event.id
) {
return (
<div key={event.id} className="contents">
{eventButton}
Expand Down
61 changes: 26 additions & 35 deletions templates/calendar/app/components/calendar/DeleteEventDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogCancel,
} from "@/components/ui/alert-dialog";
import type { CalendarEvent, DeleteEventScope } from "@shared/api";

interface DeleteEventDialogProps {
Expand All @@ -21,25 +29,6 @@ export function DeleteEventDialog({
onConfirm,
isPending,
}: DeleteEventDialogProps) {
// Close on Escape
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
onClose();
}
},
[onClose],
);

useEffect(() => {
if (open) {
window.addEventListener("keydown", handleKeyDown, true);
return () => window.removeEventListener("keydown", handleKeyDown, true);
}
}, [open, handleKeyDown]);

if (!event || !open) return null;

const isOrganizer = getIsOrganizer(event);
Expand All @@ -56,19 +45,17 @@ export function DeleteEventDialog({
}

return (
<>
{/* Transparent backdrop — click to dismiss */}
<div className="fixed inset-0 z-50" onClick={onClose} />

{/* Popover card */}
<div className="fixed left-1/2 top-1/2 z-50 w-[340px] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-popover p-5 shadow-xl animate-in fade-in-0 zoom-in-95">
<p className="mb-1 text-sm font-semibold text-foreground">
This is a recurring event
</p>
<p className="mb-4 text-sm text-muted-foreground">
Would you like to {isRemoveOnly ? "remove" : "delete"} just this
event, this and all following events, or all events in the series?
</p>
<AlertDialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<AlertDialogContent className="max-w-[340px]">
<AlertDialogHeader>
<AlertDialogTitle className="text-sm">
This is a recurring event
</AlertDialogTitle>
<AlertDialogDescription>
Would you like to {isRemoveOnly ? "remove" : "delete"} just this
event, this and all following events, or all events in the series?
</AlertDialogDescription>
</AlertDialogHeader>

<div className="space-y-1.5">
<Button
Expand Down Expand Up @@ -96,8 +83,12 @@ export function DeleteEventDialog({
All events
</Button>
</div>
</div>
</>

<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

Expand Down
Loading
Loading