Skip to content

refactor(ui): refactor agent and task components into modules#229

Merged
jlia0 merged 4 commits intomainfrom
jlia0/theme-refactor-neutral
Mar 16, 2026
Merged

refactor(ui): refactor agent and task components into modules#229
jlia0 merged 4 commits intomainfrom
jlia0/theme-refactor-neutral

Conversation

@jlia0
Copy link
Copy Markdown
Collaborator

@jlia0 jlia0 commented Mar 16, 2026

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

  • Move Save button from agents/[id] top bar into SystemPromptTab and HeartbeatTab with individual save handlers
  • Refactor agent detail page: extract SystemPromptTab, HeartbeatTab, MemoryTab, ScheduleTab, SkillsTab into separate modules with barrel exports
  • Refactor tasks page: create task-card, task-form, create-task-modal, edit-task-modal components with DropdownMenu for actions
  • Convert chat-view and chatroom-view to use ChatContainer + PromptInput + Markdown for consistent UI
  • Add shadcn DatePicker + custom time picker to schedule-tab, improve styling and layout
  • Add click-to-popover on fullscreen-calendar day cells to display all events
  • Simplify memory-tab: remove conversation history section
  • Add @tailwindcss/typography plugin for markdown prose styling
  • Remove unused ui/message.tsx component

Testing

  • Next.js build passes with no errors
  • Agent detail page: system-prompt and heartbeat tabs have working Save buttons
  • Tasks page: create, edit, and delete operations functional with dropdown menu
  • Chat views display messages with proper formatting via Markdown component
  • Schedule and calendar UI updated and responsive

🤖 Generated with Claude Code

- 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-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR is a large structural refactor that breaks a monolithic agents/[id]/page.tsx and tasks/page.tsx into focused module components, adds modern chat UI primitives (ChatContainer, PromptInput, Markdown), and upgrades the schedule tab with a full date/time picker and calendar view. The overall direction is clean and the new component boundaries are well-chosen.

Key changes:

  • Agent detail page: tab-level components (SystemPromptTab, HeartbeatTab, MemoryTab, ScheduleTab, SkillsTab) extracted to src/components/agent/ with barrel exports; Save button moved per-tab.
  • Task page: TaskCard, TaskForm, CreateTaskModal, EditTaskModal extracted to src/components/task/ with DropdownMenu replacing inline action buttons.
  • Chat views: both AgentChatView and ChatRoomView migrated to ChatContainerRoot + PromptInput + Markdown; keyboard shortcut changed from Ctrl+Enter to Enter (Shift+Enter for newline).
  • Schedule tab: custom cron parser (cronNextOccurrences), shadcn Calendar date-picker, and FullScreenCalendar event popovers.
  • New UI primitives: chat-container.tsx, prompt-input.tsx, markdown.tsx, dropdown-menu.tsx, popover.tsx, calendar.tsx, code-block.tsx added.

Issues found:

  • heartbeatEnabled stale closure in handleSaveHeartbeat — the state variable is used inside the callback but omitted from the useCallback dependency array, so toggling heartbeat on/off may not be reflected when saving.
  • lastIdRef dead code in chatroom-view.tsxuseRef(0) was replaced with a plain object { current: 0 } that re-creates every render, making the assignment a no-op.
  • Un-memoized cron computation in schedule-tab.tsxcronNextOccurrences (up to ~130 K iterations per schedule) is called inline on every render; wrapping in useMemo is recommended.
  • Misleading stop icon in chat-view.tsx / chatroom-view.tsx — the Square icon shown while sending implies the action can be cancelled, but the button is disabled during that state.

Confidence Score: 3/5

  • Safe to merge after fixing the heartbeatEnabled stale-closure bug; other findings are non-critical style and performance issues.
  • The refactor is largely a mechanical extraction with no API surface changes. One confirmed logic bug exists: heartbeatEnabled is missing from the handleSaveHeartbeat useCallback dependency array, meaning the heartbeat enabled/disabled state can silently be saved incorrectly. The remaining issues (dead lastIdRef, un-memoized cron computation, misleading icon) are style/performance concerns that do not affect correctness. The build passes per the PR description.
  • tinyoffice/src/app/agents/[id]/page.tsx (stale closure in handleSaveHeartbeat) and tinyoffice/src/components/agent/schedule-tab.tsx (un-memoized render-time computation).

Important Files Changed

Filename Overview
tinyoffice/src/app/agents/[id]/page.tsx Significantly slimmed down to delegate to extracted tab modules; splits the single Save handler into handleSaveSystemPrompt / handleSaveHeartbeat — but heartbeatEnabled is missing from the useCallback dep array, causing a stale-closure bug.
tinyoffice/src/components/agent/schedule-tab.tsx New 686-line module implementing date-picker, time-picker, repeat modes, and a custom cron parser; calendar data is computed inline on every render (no useMemo), and the cronNextOccurrences loop can run up to ~130 K iterations per schedule on each re-render.
tinyoffice/src/components/chatroom-view.tsx Migrated to ChatContainer + PromptInput + Markdown; lastIdRef converted from useRef to a plain object making it dead code, and the old Ctrl+Enter keyboard shortcut was silently replaced by Enter-to-send behaviour.
tinyoffice/src/components/agent/chat-view.tsx New agent chat view using ChatContainer + PromptInput + Markdown with smart message deduplication; the Square stop-icon is shown during send but the button is disabled, giving a misleading cancel affordance.
tinyoffice/src/components/task/task-card.tsx Clean extraction with DropdownMenu actions (edit, delete with confirm step, send-to-agent); controlled open state for delete confirmation is implemented correctly.
tinyoffice/src/components/ui/markdown.tsx Block-memoized Markdown renderer using react-markdown + remark-gfm/breaks; MemoizedMarkdownBlock avoids unnecessary re-renders for unchanged blocks. Minor: inline-code detection via AST position is fragile but conventional.
tinyoffice/src/lib/api.ts Minor type-safety improvement to saveAgent — now accepts `Partial & Pick<AgentConfig, "name"

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
Loading

Last reviewed commit: aac1fe6

@@ -171,23 +164,14 @@ export default function AgentConfigPage({
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
},
}, [agent, agentId, heartbeatContent, heartbeatInterval, heartbeatEnabled, settings]);

const feedEndRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const lastIdRef = useRef(0);
const lastIdRef = { current: 0 };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Comment on lines +271 to +315
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" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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]);

Comment on lines +203 to +209
tooltip={sending ? "Sending..." : "Send message"}
>
<Button
variant="default"
size="icon"
className="h-8 w-8 rounded-full"
disabled={!input.trim() || sending}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Square with a Loader2 spinner to indicate non-interactive progress.

The same pattern exists in chatroom-view.tsx.

jlia0 and others added 3 commits March 17, 2026 04:54
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>
@jlia0 jlia0 merged commit 8c8059a into main Mar 16, 2026
@jlia0 jlia0 deleted the jlia0/theme-refactor-neutral branch March 16, 2026 21:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant