Redesigning the home page from task lists to feed posts
Previously, users were managing their work through a standard checklist task interface. What we imagine next for Cobot is more a living feed of conversations, collaboration, and ongoing work. Tasks shouldn’t just be isolated to-dos, but starting points for conversations, collaboration, and ongoing work with Cobots.
Rethinking the architecture
The first big decision was moving away from thinking about this as a “task list” entirely.
This architectural change required using Next.js parallel routes.
app/[username]/home/
├── layout.tsx
├── @feed/ # Feed slot (left panel)
│ ├── default.tsx
│ └── page.tsx # Main feed content
└── @details/ # Details slot (right panel)
├── default.tsx
├── [taskId]/ # Dynamic task details
│ └── page.tsx # Task detail view
└── (.)[taskId]/
└── page.tsx
Not every post in the feed is created the same way. Some may be manually created by users, others were generated by automated workflows, and some would be system initiated by Cobots.
if (task.workflowId) {
author = {
id: user._id,
name: workflow?.name || "Workflow",
type: "user",
};
} else {
// Regular tasks: look at who actually started the conversation
const firstMessage = firstMessageMap.get(task._id);
// ... determine the real author
}
The frontend visual challenge
A big part this feature included visual changes from a checklist interface to a feed interface. This included showing conversations that were happening around a task, in a thread-like format, as well as a curved line to indicate the relationship between a post and its conversation thread.
const updateLineHeight = () => {
if (contentRef.current) {
const contentHeight = contentRef.current.offsetHeight;
const curveIconHeight = curveIconSize * (43 / 27);
if (contentHeight < minContentHeightForLine) {
setLineHeight(0);
return;
}
const calculatedHeight = Math.max(
contentHeight + 16 - curveIconHeight - curveIconSpacing + 5,
20
);
setLineHeight(calculatedHeight);
}
};
I used ResizeObserver
to watch for content changes and recalculate the line heights in real-time. If a post had longer content, the line would grow to match.
Task participants
Once I had the threads of each feed post, we needed to show who was participating in each conversation.
I ended up implementing this overlapping avatar design with CSS clip-path masking. The rightmost avatar shows fully, but each one to the left gets partially masked to create this stacked effect:
.avatar-masked {
clip-path: path("M12.375 0C19.0023 0.000178326 24.375 5.37269...");
}
Showing a workflow in progress
Workflow-generated posts needed to feel different from human-created tasks. Workflows in progress would generate long, detailed updates that would crowd the feed if displayed at full length.
I worked on creating a vertical marquee animation that showcases the latest message in a workflow, allowing users to quickly scan the feed and see what’s happening without having to read the entire message.
if (task.workflowId) {
const workflow = workflowMap.get(task.workflowId);
if (workflow && lastMessage) {
// show the most recent Cobot message
body = lastMessage.content || "";
}
}
The message preview needed to have a visual distinction from the rest of the content, so I wrapped the content in a rounded & slightly elevated container.
<div
className={cn(
"flex flex-col max-w-full transition-all duration-300 ease-in-out",
isWorkflowInProgress && "bg-fill-tertiary rounded-3xl p-3 px-4 mt-1",
)}
>
The different states of a feed post
The core of the system is this state-aware rendering logic:
<TaskBodyText
variant={task.inProgress ? `${task.type}-in-progress` : `${task.type}-finished`}
content={
task.type === "workflow" ? task.lastMessage?.content || task.body : task.body
}
/>
The thread component also is accounted for to show different icon combinations based on conversation state.
<ThreadStack
participants={task.conversation?.participants}
currentUserId={currentUserId}
isLoading={task.type === "task" && task.inProgress}
isWorkflowInProgress={task.type === "workflow" && task.inProgress}
unread={task.unread}
totalReplies={task.messageCount}
isCurrentlyViewing={isActiveTask}
/>
Try it out here.