refactor(ui): refactor agent and task components into modules#229
refactor(ui): refactor agent and task components into modules#229
Conversation
- Move SaveButton from agents/[id] top bar into system-prompt and heartbeat tabs - Refactor agent detail page components: SystemPromptTab, HeartbeatTab, MemoryTab with Save buttons in tabs - Refactor tasks page into component library: task-card, task-form, create/edit-task-modal with DropdownMenu actions - Convert chat-view and chatroom-view to use ChatContainer + PromptInput + Markdown - Add shadcn DatePicker and custom time picker to schedule-tab - Add click-popover to fullscreen-calendar day cells to show all events - Remove unused ui/message.tsx component - Simplify memory-tab: remove "conversation history" section and manage memory skill reference - Apply @tailwindcss/typography plugin for prose markdown styling Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Greptile SummaryThis PR is a large structural refactor that breaks a monolithic Key changes:
Issues found:
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[agents/id/page.tsx] -->|renders| B[SystemPromptTab]
A -->|renders| C[HeartbeatTab]
A -->|renders| D[MemoryTab]
A -->|renders| E[ScheduleTab]
A -->|renders| F[SkillsTab]
A -->|renders| G[AgentChatView]
B -->|onSave → handleSaveSystemPrompt| API1[saveAgentSystemPrompt]
C -->|onSave → handleSaveHeartbeat| API2[saveAgentHeartbeat]
C -->|onSave → handleSaveHeartbeat| API3[updateSettings]
G --> CC[ChatContainerRoot]
G --> PI[PromptInput]
G --> MD[Markdown]
H[tasks/page.tsx] --> TC[TaskCard]
H --> CM[CreateTaskModal]
H --> EM[EditTaskModal]
CM --> TF[TaskForm]
EM --> TF
E --> SC[ScheduleTab cronNextOccurrences]
SC --> CAL[FullScreenCalendar]
SC --> POP[Popover DatePicker]
style API2 fill:#f96,stroke:#c33
style API3 fill:#f96,stroke:#c33
note1["⚠ heartbeatEnabled missing\nfrom useCallback deps"]
API2 -.->|stale closure risk| note1
Last reviewed commit: aac1fe6 |
| @@ -171,23 +164,14 @@ export default function AgentConfigPage({ | |||
| }, | |||
There was a problem hiding this comment.
heartbeatEnabled missing from useCallback dependency array
heartbeatEnabled is read inside handleSaveHeartbeat (used in the updateSettings call) but is not listed in the useCallback dependency array. This creates a stale closure: if the user toggles the heartbeat on/off and then clicks Save without any other triggering state change, the callback will fire with the old (stale) heartbeatEnabled value.
| }, | |
| }, [agent, agentId, heartbeatContent, heartbeatInterval, heartbeatEnabled, settings]); |
| const feedEndRef = useRef<HTMLDivElement>(null); | ||
| const containerRef = useRef<HTMLDivElement>(null); | ||
| const lastIdRef = useRef(0); | ||
| const lastIdRef = { current: 0 }; |
There was a problem hiding this comment.
lastIdRef replaced with plain object — dead code
useRef(0) has been replaced with an inline plain object { current: 0 }. A plain object is re-created on every render, so any assignment like lastIdRef.current = maxId is immediately discarded. Since the auto-scroll logic was also removed, lastIdRef is now entirely dead code and should be removed to avoid confusion.
| const lastIdRef = { current: 0 }; | |
| const lastIdRef = useRef(0); |
Or simply delete the declaration and the assignment entirely, since neither is read anywhere in the updated component.
| if (s.runAt) { | ||
| const occ = new Date(s.runAt); | ||
| const key = occ.toDateString(); | ||
| if (!dayMap.has(key)) { | ||
| dayMap.set(key, { day: new Date(occ.getFullYear(), occ.getMonth(), occ.getDate()), events: [] }); | ||
| } | ||
| const hours = occ.getHours(); | ||
| const mins = occ.getMinutes(); | ||
| const ampm = hours >= 12 ? "PM" : "AM"; | ||
| const h12 = hours % 12 || 12; | ||
| dayMap.get(key)!.events.push({ | ||
| id: s.id, | ||
| name: s.label || s.message.slice(0, 40), | ||
| time: `${h12}:${String(mins).padStart(2, "0")} ${ampm}`, | ||
| datetime: occ.toISOString(), | ||
| }); | ||
| continue; | ||
| } | ||
|
|
||
| const occurrences = cronNextOccurrences(s.cron, 60); | ||
| for (const occ of occurrences) { | ||
| const key = occ.toDateString(); | ||
| if (!dayMap.has(key)) { | ||
| dayMap.set(key, { day: new Date(occ.getFullYear(), occ.getMonth(), occ.getDate()), events: [] }); | ||
| } | ||
| const hours = occ.getHours(); | ||
| const mins = occ.getMinutes(); | ||
| const ampm = hours >= 12 ? "PM" : "AM"; | ||
| const h12 = hours % 12 || 12; | ||
| const timeStr = `${h12}:${String(mins).padStart(2, "0")} ${ampm}`; | ||
| dayMap.get(key)!.events.push({ | ||
| id: `${s.id}-${occ.getTime()}`, | ||
| name: s.label || s.message.slice(0, 40), | ||
| time: timeStr, | ||
| datetime: occ.toISOString(), | ||
| }); | ||
| } | ||
| } | ||
| calendarData.push(...dayMap.values()); | ||
|
|
||
| if (loading) { | ||
| return ( | ||
| <div className="flex items-center justify-center h-64"> | ||
| <div className="flex items-center gap-2 text-sm text-muted-foreground"> | ||
| <Loader2 className="h-4 w-4 animate-spin" /> |
There was a problem hiding this comment.
Calendar data not memoized — cronNextOccurrences runs on every render
The calendar data computation (including cronNextOccurrences calls) is performed inline in the component body, not inside a useMemo. cronNextOccurrences loops up to 60 × 24 × 90 = 129 600 iterations per schedule to generate occurrences. This means for every React re-render (e.g. form field keystrokes, hover states), all schedules are re-parsed. Wrap the block in useMemo keyed on schedules:
| if (s.runAt) { | |
| const occ = new Date(s.runAt); | |
| const key = occ.toDateString(); | |
| if (!dayMap.has(key)) { | |
| dayMap.set(key, { day: new Date(occ.getFullYear(), occ.getMonth(), occ.getDate()), events: [] }); | |
| } | |
| const hours = occ.getHours(); | |
| const mins = occ.getMinutes(); | |
| const ampm = hours >= 12 ? "PM" : "AM"; | |
| const h12 = hours % 12 || 12; | |
| dayMap.get(key)!.events.push({ | |
| id: s.id, | |
| name: s.label || s.message.slice(0, 40), | |
| time: `${h12}:${String(mins).padStart(2, "0")} ${ampm}`, | |
| datetime: occ.toISOString(), | |
| }); | |
| continue; | |
| } | |
| const occurrences = cronNextOccurrences(s.cron, 60); | |
| for (const occ of occurrences) { | |
| const key = occ.toDateString(); | |
| if (!dayMap.has(key)) { | |
| dayMap.set(key, { day: new Date(occ.getFullYear(), occ.getMonth(), occ.getDate()), events: [] }); | |
| } | |
| const hours = occ.getHours(); | |
| const mins = occ.getMinutes(); | |
| const ampm = hours >= 12 ? "PM" : "AM"; | |
| const h12 = hours % 12 || 12; | |
| const timeStr = `${h12}:${String(mins).padStart(2, "0")} ${ampm}`; | |
| dayMap.get(key)!.events.push({ | |
| id: `${s.id}-${occ.getTime()}`, | |
| name: s.label || s.message.slice(0, 40), | |
| time: timeStr, | |
| datetime: occ.toISOString(), | |
| }); | |
| } | |
| } | |
| calendarData.push(...dayMap.values()); | |
| if (loading) { | |
| return ( | |
| <div className="flex items-center justify-center h-64"> | |
| <div className="flex items-center gap-2 text-sm text-muted-foreground"> | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| const calendarData: CalendarData[] = useMemo(() => { | |
| const dayMap = new Map<string, CalendarData>(); | |
| for (const s of schedules) { | |
| if (!s.enabled) continue; | |
| // ... existing loop body ... | |
| } | |
| return [...dayMap.values()]; | |
| }, [schedules]); |
| tooltip={sending ? "Sending..." : "Send message"} | ||
| > | ||
| <Button | ||
| variant="default" | ||
| size="icon" | ||
| className="h-8 w-8 rounded-full" | ||
| disabled={!input.trim() || sending} |
There was a problem hiding this comment.
Square icon implies cancellable action but button is disabled while sending
During sending, the button renders a Square (stop) icon — a common pattern for cancelling in-flight requests. However, the button is disabled when sending is true, so clicking it does nothing. This is misleading UX; users may expect the square to abort the request. Either:
- Wire up actual cancellation (e.g.
AbortController), or - Replace the
Squarewith aLoader2spinner to indicate non-interactive progress.
The same pattern exists in chatroom-view.tsx.
Apply agentColor hash function to sidebar, agent detail header, and agent chat view so each agent gets a stable color based on its ID. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add heartbeatEnabled to handleSaveHeartbeat dependency array - Remove dead lastIdRef plain object in chatroom-view (was recreated every render, making the assignment a no-op) - Wrap cronNextOccurrences calendar data computation in useMemo to avoid expensive recalculation on every render Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The custom button-based toggle had hover styles that changed the background color. Use the shadcn Switch component instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Complete refactoring of agent and task component architecture. Moved save buttons from top-level actions into individual tabs (system-prompt and heartbeat). Refactored tasks page into reusable component modules with dropdown menu actions. Enhanced chat views with modern UI components (ChatContainer, PromptInput, Markdown) and improved schedule/calendar UX with shadcn DatePicker and event popovers.
Changes
Testing
🤖 Generated with Claude Code