Three independently ugly bugs that all fed the same log flood:
* Electron's `WebFrameMain.send` writes every "Render frame was disposed"
to stderr via `console.error`. When the worker fans a second
agent-event past a reloading renderer, we produced thousands of
identical lines and stalled the main loop on stderr I/O. Added a
`mainFrame.detached` guard so the send is short-circuited before
Electron's own catch block fires, and installed a targeted filter in
`suppress-warnings.ts` (now actually imported from the entry point)
so the specific `console.error("Error sending from webFrameMain:"…)`
noise never reaches the terminal. Any other payload still surfaces.
* `dispatchAgentMessage` accepted the human-reader sentinel as a wake
target when an agent replied to `__board__` — `wakeAgentIfPossible`
then called `db.getAgent("__board__")`, threw, and the dispatcher
kept repeating on every send. Strip `HUMAN_SENDER_ID` at
`resolveMessageWakeTargets`, and keep a defense-in-depth guard in the
heartbeat so a stray caller (future automation fan-out) cannot
reintroduce the crash.
* Pi's skills loader rejects any SKILL.md whose frontmatter `name`
diverges from its parent directory. The four bundled `my-writing-style`
copies declared `name: writing-style-skill`, so every install emitted
a warning and dropped the skill. They also lived as orphan gitlinks
(mode 160000 without .gitmodules), and the global `*.md` gitignore
hid every other SKILL.md from version control. Fixed the frontmatter
to match the dir, converted the dangling submodules to plain trees,
and allowlisted `builtin-skills/**/*.md` so a fresh clone carries the
catalogue instead of silently losing it.
3.1 KiB
3.1 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use Cases Must Not Contain Presentation Logic | HIGH | enables multiple UI implementations from same use case | usecase, presentation, formatting, separation |
Use Cases Must Not Contain Presentation Logic
Use cases return domain data, not formatted strings, HTML, or UI-specific structures. Presentation logic belongs in the interface adapters layer.
Incorrect (presentation logic in use case):
class GetUserProfileUseCase {
execute(userId: string): UserProfileResponse {
const user = this.repo.findById(userId)
const orders = this.orderRepo.findByUser(userId)
return {
displayName: `${user.firstName} ${user.lastName}`, // Formatting
memberSince: user.createdAt.toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
}), // Locale-specific formatting
avatarHtml: `<img src="${user.avatarUrl}" alt="${user.firstName}"/>`, // HTML!
orderSummary: orders.length > 0
? `${orders.length} orders totaling $${this.sumOrders(orders).toFixed(2)}`
: 'No orders yet', // UI text
recentOrders: orders.slice(0, 5).map(o => ({
...o,
statusBadgeColor: this.getStatusColor(o.status) // UI styling
}))
}
}
private getStatusColor(status: string): string {
const colors = { pending: 'yellow', shipped: 'blue', delivered: 'green' }
return colors[status] || 'gray'
}
}
Correct (use case returns domain data):
// application/usecases/GetUserProfileUseCase.ts
class GetUserProfileUseCase {
execute(userId: string): UserProfileResult {
const user = this.repo.findById(userId)
const orders = this.orderRepo.findByUser(userId)
return {
user: {
id: user.id.value,
firstName: user.firstName,
lastName: user.lastName,
email: user.email.value,
avatarUrl: user.avatarUrl,
createdAt: user.createdAt
},
orders: orders.map(o => ({
id: o.id.value,
total: o.total.amount,
currency: o.total.currency,
status: o.status,
createdAt: o.createdAt
})),
orderCount: orders.length,
totalSpent: this.sumOrders(orders)
}
}
}
// interface_adapters/presenters/UserProfilePresenter.ts
class UserProfilePresenter {
present(result: UserProfileResult, locale: string): UserProfileViewModel {
const formatter = new Intl.DateTimeFormat(locale, {
month: 'long',
year: 'numeric'
})
return {
displayName: `${result.user.firstName} ${result.user.lastName}`,
memberSince: formatter.format(result.user.createdAt),
avatarUrl: result.user.avatarUrl,
orderSummary: this.formatOrderSummary(result, locale),
recentOrders: result.orders.slice(0, 5).map(o =>
this.formatOrder(o, locale)
)
}
}
}
Benefits:
- Same use case serves web, mobile, API consumers
- Locale/format changes don't touch business logic
- UI redesigns don't affect use case tests
Reference: Clean Architecture - Presenters