Skip to content

refactor: decompose AutomationChatView into focused components (FE-18)#920

Merged
Chris0Jeky merged 2 commits intomainfrom
refactor/fe-18-decompose-automation-chat-view
Apr 22, 2026
Merged

refactor: decompose AutomationChatView into focused components (FE-18)#920
Chris0Jeky merged 2 commits intomainfrom
refactor/fe-18-decompose-automation-chat-view

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

  • Decomposes the 1,523-line AutomationChatView.vue monolith into 7 focused components and 1 composable, all under 400 lines each
  • Extracts orchestration logic into useAutomationChat composable for clean separation of state management from template rendering
  • No behavioral changes -- all existing tests (2,552 tests, 210 files) pass without modification

Extracted components

Component Lines Responsibility
ChatHeroHeader 119 Page title, subtitle, action buttons
LlmHealthStatusBar 188 Provider health status banner with state-based styling
ChatSessionSidebar 254 Session list, create-session form, board context selector
ChatMessageList 361 Message rendering with markdown, degraded/clarification states
ChatParseHintCard 169 Parse-hint instruction format cards
ChatToolCallDetails 135 Tool call metadata expander
ChatComposeBar 128 Message textarea, proposal checkbox, send button, clarification skip
useAutomationChat (composable) 394 All orchestration state, API calls, board resolution, routing
AutomationChatView (parent) 235 Template shell wiring components together

Closes #859

Test plan

  • All 2,552 frontend unit tests pass (210 files)
  • TypeScript typecheck passes (npm run typecheck)
  • Production build succeeds (npm run build)
  • ESLint passes with 0 errors (npm run lint)
  • No component exceeds 400 lines
  • Visual regression check in browser (chat workflow renders identically)

Extract the 1,523-line monolith into 7 focused components + 1 composable,
each under 400 lines:

- ChatHeroHeader (119 lines) - page title, subtitle, action buttons
- LlmHealthStatusBar (188 lines) - provider health status banner
- ChatSessionSidebar (254 lines) - session list, create form, board selector
- ChatMessageList (361 lines) - message rendering with markdown support
- ChatParseHintCard (169 lines) - parse-hint instruction cards
- ChatToolCallDetails (135 lines) - tool call metadata expander
- ChatComposeBar (128 lines) - message input, proposal checkbox, send
- useAutomationChat composable (394 lines) - orchestration state/logic

Parent AutomationChatView reduced to 235 lines (template shell only).
All existing tests pass without modification.

Closes #859
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Adversarial self-review

Checked areas

Props/events wiring: Verified all parent-to-child prop bindings and child-to-parent event emissions match. Vue's automatic camelCase-to-kebab-case normalization handles the update:newSessionTitle -> @update:new-session-title mapping correctly. The InputAssistField in ChatSessionSidebar correctly chains its update:model-value through to the parent's updateNewSessionBoardValue.

Lost functionality check: All template sections from the original 1,523-line file are accounted for in the extracted components:

  • Hero header with all 4 action buttons
  • LLM health status bar with all 7 health states
  • Session sidebar with create form, board context, session list, empty state
  • Message list with all 6 message type renderings (text, degraded, clarification, parse-hint, proposal-reference, tool-status)
  • Parse hint card with toggle patterns and apply suggestion
  • Tool call metadata expander with round/tool/badge rendering
  • Compose bar with clarification skip, textarea (ctrl+enter), proposal checkbox, send button
  • Empty states ("no session selected", "no messages yet", "no sessions yet")

Component boundary issues: None found. The composable useAutomationChat cleanly owns all mutable state and exposes it as refs/computed. Child components receive read-only props and emit events upward. No prop drilling beyond 1 level.

Test coverage: All 2,552 tests pass without modification. The tests mount AutomationChatView which internally renders all child components, so the integration is fully validated. Both the main spec and coverage spec files exercise the full workflow.

Accessibility: All aria-label, aria-expanded, role, and data-* attributes are preserved in their respective extracted components.

CSS scoping: Each component has its own <style scoped> block. There is some CSS duplication (.td-btn base styles appear in multiple components) which is a trade-off of scoped styles. This is acceptable since the duplication is minimal and the styles are lightweight utility classes. A future improvement could extract shared button styles into a global stylesheet.

No issues found

The decomposition is functionally equivalent to the original. No fixes needed.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the AutomationChatView by extracting its business logic into a dedicated useAutomationChat composable and decomposing the UI into several modular components, such as ChatMessageList, ChatComposeBar, and ChatSessionSidebar. The review feedback focuses on performance optimizations, specifically identifying inefficient object instantiation and redundant JSON parsing within v-for loops and sort comparators. Additionally, there is a recommendation to refactor duplicated message-sending logic within the new composable to improve maintainability.

Comment on lines +63 to +65
return [...current.recentMessages].sort((a, b) => (
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Creating and parsing Date objects inside a sort comparator is a performance anti-pattern. The sort method calls the comparator $O(N \log N)$ times, leading to excessive object allocation and CPU overhead. It is more efficient to map the messages to timestamps once before sorting, or ensure the backend provides them in the correct order.

Suggested change
return [...current.recentMessages].sort((a, b) => (
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
))
return [...current.recentMessages].sort((a, b) => (
Date.parse(a.createdAt) - Date.parse(b.createdAt)
))

Comment on lines +238 to +279
async function handleSendMessage() {
if (!selectedSession.value) {
toast.error('Select a session first')
return
}
if (!messageContent.value.trim()) {
return
}

try {
sendingMessage.value = true
await chatApi.sendMessage(selectedSession.value.id, {
content: messageContent.value.trim(),
requestProposal: requestProposal.value,
})
messageContent.value = ''
requestProposal.value = false
await loadSession(selectedSession.value.id)
} catch (e: unknown) {
toast.error(getErrorDisplay(e, 'Failed to send message').message)
} finally {
sendingMessage.value = false
}
}

async function handleSkipClarification() {
if (!selectedSession.value) return
try {
sendingMessage.value = true
await chatApi.sendMessage(selectedSession.value.id, {
content: 'Just do your best',
requestProposal: requestProposal.value,
})
messageContent.value = ''
requestProposal.value = false
await loadSession(selectedSession.value.id)
} catch (e: unknown) {
toast.error(getErrorDisplay(e, 'Failed to send message').message)
} finally {
sendingMessage.value = false
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The logic for sending a message in handleSendMessage and handleSkipClarification is almost identical. This duplication makes the code harder to maintain and increases the risk of bugs if the message-sending flow changes in the future. Consider refactoring the core sending logic into a private helper function.

Comment on lines +38 to +48
function isTruncatedJson(content: string): boolean {
if (!content) return false
const trimmed = content.trim()
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return false
try {
JSON.parse(trimmed)
return false
} catch {
return true
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The isTruncatedJson function is called within the template loop for every assistant or system message. Performing JSON.parse inside a render cycle is expensive and can lead to UI jank as the message list grows. Consider pre-calculating this state when messages are loaded or using a more efficient heuristic.

>
<div class="td-message-header">
<span class="td-message-role">{{ normalizeChatRole(message.role) }}</span>
<span class="td-message-time">{{ new Date(message.createdAt).toLocaleTimeString() }}</span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Instantiating a new Date object inside a v-for loop for every message on every render cycle is inefficient. This can be optimized by formatting the timestamp once when the message is received or by using a computed property that maps messages to formatted strings.

Comment on lines +163 to +164
v-if="parseToolCallMetadata(message)"
:metadata="parseToolCallMetadata(message)!"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The parseToolCallMetadata function is called twice for every message that contains tool call metadata (once for v-if and once for the :metadata prop). Since this function performs JSON.parse, it should be optimized to avoid redundant parsing. Consider using a computed property to pre-parse metadata for all messages.

@click="emit('select-session', session.id)"
>
<div class="td-session-title">{{ session.title }}</div>
<div class="td-session-meta">{{ new Date(session.updatedAt).toLocaleString() }}</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Creating a new Date object and calling toLocaleString() inside a v-for loop can impact performance as the number of sessions increases. It is better to format these dates once when the sessions are fetched.

- Use Date.parse() instead of new Date().getTime() in sort comparator
  to avoid unnecessary object allocation (Gemini comment)
- Extract duplicated send logic into sendMessageToSession helper
  shared by handleSendMessage and handleSkipClarification (Gemini comment)
- Pre-compute toolCallMetadata and truncatedJson via computed maps to
  eliminate redundant JSON.parse calls during render (Gemini comment)
@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Adversarial Review of PR #920

Summary

The decomposition is structurally sound. Props/events wiring is correct across all parent-child boundaries, no functionality was lost during extraction, and all frontend checks pass (typecheck, build, lint, 2552 tests). I pushed a follow-up commit addressing the legitimate Gemini findings plus one additional concern.

Gemini Bot Comments Assessment

All 6 Gemini comments were reviewed. Findings grouped by merit:

Legitimate and fixed (3):

  1. parseToolCallMetadata called twice per message (ChatMessageList.vue lines 162-164) -- v-if="parseToolCallMetadata(message)" + :metadata="parseToolCallMetadata(message)!" caused redundant JSON.parse each render cycle. Fixed: replaced with a computed map (toolMetaByMessageId) that parses once per messages change, and a getToolMeta() lookup.

  2. handleSendMessage / handleSkipClarification duplication (useAutomationChat.ts) -- near-identical try/catch/finally blocks. Fixed: extracted sendMessageToSession(content) private helper; both handlers now delegate to it.

  3. new Date() in sort comparator (useAutomationChat.ts line 63-64) -- Date.parse() avoids allocating Date objects O(N log N) times. Fixed: switched to Date.parse(a.createdAt) - Date.parse(b.createdAt).

Valid but low-impact (3, noted but partly addressed):

  1. isTruncatedJson with JSON.parse in render loop -- same concern as Claude/create feature f 01 qr r as j4 s14advm nn qhp2 la #1. Fixed: pre-computed into a truncatedJsonIds Set via computed().

  2. new Date() in v-for for message timestamps (ChatMessageList.vue line 118) -- creating Date objects per message per render. This is idiomatic Vue and the message count is typically small (< 50 in a session). Not fixed -- optimizing this would add complexity disproportionate to the benefit.

  3. new Date() in v-for for session timestamps (ChatSessionSidebar.vue line 91) -- same as above, even lower impact since session counts are small. Not fixed.

My Own Findings

Issue: Massive CSS duplication across child components

The .td-btn, .td-btn--primary, .td-btn--secondary, .td-btn--sm, .td-btn:disabled styles are duplicated across 6 files: ChatComposeBar, ChatHeroHeader, ChatSessionSidebar, ChatParseHintCard, ChatMessageList, and the parent AutomationChatView. This is a significant maintainability regression -- if button styles change, all 6 copies must be updated. Consider:

  • Extracting shared button styles into a global stylesheet or a shared CSS module
  • Or using a shared TdButton component

This is not a blocker for this PR since the original file also had all these styles (just in one place), but the decomposition turned a single-location issue into a multi-file one. Recommend tracking as a follow-up.

No functional regressions found:

  • All props/events wiring is correct
  • v-model to :value+@input conversion is behaviorally equivalent
  • Computed property reactivity chains are preserved
  • onMounted and watch lifecycle hooks are correctly placed in the composable
  • Component boundary violations: none found -- children communicate exclusively via props/events
  • Accessibility: aria labels and ARIA expanded states are preserved in the extracted components

No new tests for new component boundaries:
The Definition of Done states "Behavior changes ship with tests." While this is a pure refactor, the new component boundaries (ChatMessageList, ChatComposeBar, ChatSessionSidebar, etc.) create testable surfaces that don't have coverage. This is acceptable for a refactor PR but worth noting for future work.

Verification

All checks pass after the fix commit:

  • npm run typecheck -- clean
  • npm run build -- clean
  • npm run lint -- 0 errors, 6 pre-existing warnings (unrelated)
  • npx vitest --run -- 210 test files, 2552 tests passed

@Chris0Jeky Chris0Jeky merged commit 6114cde into main Apr 22, 2026
14 checks passed
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Apr 22, 2026
@Chris0Jeky Chris0Jeky deleted the refactor/fe-18-decompose-automation-chat-view branch April 23, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

FE-18: Decompose AutomationChatView (1,523 lines → <400 each)

1 participant