diff --git a/frontend/src/layout/today/TodayPaneSection.tsx b/frontend/src/layout/today/TodayPaneSection.tsx
new file mode 100644
index 000000000000..4b76cc1031bc
--- /dev/null
+++ b/frontend/src/layout/today/TodayPaneSection.tsx
@@ -0,0 +1,48 @@
+import { ReactNode } from 'react'
+
+import { IconChevronDown, IconChevronRight } from '@posthog/icons'
+
+import { cn } from 'lib/utils/css-classes'
+
+interface TodayPaneSectionProps {
+ label: string
+ open: boolean
+ count: number
+ onToggle: () => void
+ actions?: JSX.Element | null
+ divider?: boolean
+ dataAttr: string
+ children: ReactNode
+}
+
+export function TodayPaneSection({
+ label,
+ open,
+ count,
+ onToggle,
+ actions,
+ divider = false,
+ dataAttr,
+ children,
+}: TodayPaneSectionProps): JSX.Element {
+ const Caret = open ? IconChevronDown : IconChevronRight
+ return (
+
+
+
+ {open && actions &&
{actions}
}
+
+ {open && {children}
}
+
+ )
+}
diff --git a/frontend/src/layout/today/TodayShell.scss b/frontend/src/layout/today/TodayShell.scss
index 70e418f1604f..d15f088accd4 100644
--- a/frontend/src/layout/today/TodayShell.scss
+++ b/frontend/src/layout/today/TodayShell.scss
@@ -51,6 +51,107 @@
letter-spacing: 0.08em;
}
+.TodayPaneSection {
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 8px;
+
+ &:first-child {
+ margin-top: 12px;
+ }
+
+ &--divider {
+ border-top: 1px solid var(--today-line);
+ }
+}
+
+.TodayPaneSection__header {
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ height: 32px;
+ margin-top: 4px;
+}
+
+.TodayPaneSection__toggle {
+ display: flex;
+ flex: 1 1 auto;
+ gap: 4px;
+ align-items: center;
+ min-width: 0;
+ padding: 4px 8px;
+ text-align: left;
+ border-radius: 6px;
+ transition: color 0.16s;
+
+ @media (hover: hover) and (pointer: fine) {
+ &:hover {
+ color: var(--today-ink);
+ }
+ }
+}
+
+.TodayPaneSection__caret {
+ flex: 0 0 11px;
+ width: 11px;
+ height: 11px;
+ opacity: 0.7;
+}
+
+.TodayPaneSection__count {
+ margin-left: 2px;
+ font-variant-numeric: tabular-nums;
+ color: var(--today-muted);
+}
+
+.TodayPaneSection__actions {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+}
+
+.TodayPaneSection__body {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+
+.TodayPane__group {
+ padding: 12px 8px 4px;
+ font-size: 11px;
+ font-weight: 500;
+ color: var(--today-muted);
+
+ &--first {
+ padding-top: 4px;
+ }
+}
+
+.TodayPane__dot {
+ width: 6px;
+ height: 6px;
+ background: var(--today-faint);
+ border-radius: 50%;
+
+ &[data-status='queued'],
+ &[data-status='in_progress'] {
+ background: var(--warning);
+ }
+
+ &[data-status='completed'] {
+ background: var(--success);
+ }
+
+ &[data-status='failed'] {
+ background: var(--danger);
+ }
+
+ &[data-kind='chat'] {
+ background: transparent;
+ border: 1px solid var(--today-faint);
+ }
+}
+
.TodayPane__state {
display: flex;
flex-direction: column;
diff --git a/frontend/src/layout/today/TodaySpacesSidebar.tsx b/frontend/src/layout/today/TodaySpacesSidebar.tsx
index e099c0c39f9b..3ccc236344a6 100644
--- a/frontend/src/layout/today/TodaySpacesSidebar.tsx
+++ b/frontend/src/layout/today/TodaySpacesSidebar.tsx
@@ -1,7 +1,8 @@
import { useActions, useValues } from 'kea'
import { router } from 'kea-router'
+import { Fragment } from 'react'
-import { IconChevronRight, IconPlus } from '@posthog/icons'
+import { IconChevronRight, IconList, IconPlus } from '@posthog/icons'
import { LemonButton } from '@posthog/lemon-ui'
import { Spinner } from 'lib/lemon-ui/Spinner'
@@ -9,24 +10,46 @@ import { cn } from 'lib/utils/css-classes'
import { urls } from 'scenes/urls'
import { TodayPaneRow } from './TodayPaneRow'
+import { TodayPaneSection } from './TodayPaneSection'
import { spaceLabel, todaySpacesLogic } from './todaySpacesLogic'
+import { TodayWorkItem, shortTimeAgo } from './todayWorkItems'
export function TodaySpacesSidebar(): JSX.Element {
const {
- sortedSpaces,
+ visibleSpaces,
+ browsingSpaces,
spacesLoading,
spacesUnavailable,
expandedSpaceIds,
spaceTasks,
loadingSpaceIds,
failedSpaceIds,
- recentChats,
- conversationHistoryLoading,
+ pinnedItems,
+ recentItems,
+ recentGroups,
+ recentLoading,
+ recentTasksUnavailable,
+ collapsedSections,
} = useValues(todaySpacesLogic)
- const { toggleSpace, loadSpaces, loadSpaceTasks } = useActions(todaySpacesLogic)
+ const { toggleSpace, loadSpaces, loadSpaceTasks, loadRecentTasks, toggleSection, setBrowsingSpaces } =
+ useActions(todaySpacesLogic)
const { location, searchParams } = useValues(router)
const onAi = location.pathname.endsWith('/ai')
+ const renderItem = (item: TodayWorkItem, dataAttr: string): JSX.Element => (
+ }
+ meta={shortTimeAgo(item.timestamp)}
+ to={item.kind === 'chat' ? urls.ai(item.id) : urls.aiTask(item.id)}
+ active={onAi && (item.kind === 'chat' ? searchParams.chat : searchParams.task) === item.id}
+ dataAttr={dataAttr}
+ />
+ )
+
+ const hasPinned = pinnedItems.length > 0
+
return (
)
diff --git a/frontend/src/layout/today/todaySpacesLogic.ts b/frontend/src/layout/today/todaySpacesLogic.ts
index 83d84215b26e..96e29e3a3208 100644
--- a/frontend/src/layout/today/todaySpacesLogic.ts
+++ b/frontend/src/layout/today/todaySpacesLogic.ts
@@ -3,14 +3,21 @@ import { loaders } from 'kea-loaders'
import { maxGlobalLogic } from 'scenes/max/maxGlobalLogic'
import { teamLogic } from 'scenes/teamLogic'
+import { userLogic } from 'scenes/userLogic'
-import { ConversationDetail } from '~/types'
+import { ConversationDetail, UserType } from '~/types'
import { taskChannelsList, tasksList } from 'products/tasks/frontend/generated/api'
import { ChannelDTOApi, TaskListItemApi } from 'products/tasks/frontend/generated/api.schemas'
+import { TodayWorkGroup, TodayWorkItem, buildRecentItems, groupByDay, sessionItem } from './todayWorkItems'
+
const SPACE_TASK_LIMIT = 8
-const RECENT_CHAT_LIMIT = 12
+const PINNED_SESSION_LIMIT = 20
+const RECENT_SESSION_LIMIT = 30
+const RECENT_ITEM_LIMIT = 30
+
+export type TodayWorkSectionId = 'pinned' | 'recent' | 'spaces'
/** The personal space first, then the team's general space, then starred spaces, then the rest by name. */
export function sortSpaces(spaces: ChannelDTOApi[]): ChannelDTOApi[] {
@@ -23,24 +30,75 @@ export function spaceLabel(space: Pick):
return space.system_role === 'personal' ? 'Me' : space.name
}
+export function starredSpaces(spaces: ChannelDTOApi[]): ChannelDTOApi[] {
+ return [
+ ...spaces.filter((space) => space.system_role === 'personal'),
+ ...spaces
+ .filter((space) => space.system_role !== 'personal' && space.starred)
+ .sort((first, second) => first.name.localeCompare(second.name)),
+ ]
+}
+
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface todaySpacesLogicValues {
conversationHistory: ConversationDetail[] // maxGlobalLogic
conversationHistoryLoading: boolean // maxGlobalLogic
currentTeamId: number | null // teamLogic
+ user: UserType | null // userLogic
+ browsingSpaces: boolean
+ collapsedSections: TodayWorkSectionId[]
expandedSpaceIds: string[]
failedSpaceIds: string[]
loadingSpaceIds: string[]
- recentChats: ConversationDetail[]
+ pinnedItems: TodayWorkItem[]
+ pinnedTasks: TaskListItemApi[]
+ pinnedTasksLoading: boolean
+ recentGroups: TodayWorkGroup[]
+ recentItems: TodayWorkItem[]
+ recentLoading: boolean
+ recentTasks: TaskListItemApi[]
+ recentTasksLoading: boolean
+ recentTasksUnavailable: boolean
sortedSpaces: ChannelDTOApi[]
spaceTasks: Record
spaces: ChannelDTOApi[]
spacesLoading: boolean
spacesUnavailable: boolean
+ visibleSpaces: ChannelDTOApi[]
}
// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface todaySpacesLogicActions {
+ loadPinnedTasks: () => any
+ loadPinnedTasksFailure: (
+ error: string,
+ errorObject?: any
+ ) => {
+ error: string
+ errorObject?: any
+ }
+ loadPinnedTasksSuccess: (
+ pinnedTasks: TaskListItemApi[],
+ payload?: any
+ ) => {
+ pinnedTasks: TaskListItemApi[]
+ payload?: any
+ }
+ loadRecentTasks: () => any
+ loadRecentTasksFailure: (
+ error: string,
+ errorObject?: any
+ ) => {
+ error: string
+ errorObject?: any
+ }
+ loadRecentTasksSuccess: (
+ recentTasks: TaskListItemApi[],
+ payload?: any
+ ) => {
+ recentTasks: TaskListItemApi[]
+ payload?: any
+ }
loadSpaceTasks: (spaceId: string) => {
spaceId: string
}
@@ -62,6 +120,9 @@ export interface todaySpacesLogicActions {
spaces: ChannelDTOApi[]
payload?: any
}
+ setBrowsingSpaces: (browsingSpaces: boolean) => {
+ browsingSpaces: boolean
+ }
setSpaceTasks: (
spaceId: string,
tasks: TaskListItemApi[]
@@ -69,6 +130,9 @@ export interface todaySpacesLogicActions {
spaceId: string
tasks: TaskListItemApi[]
}
+ toggleSection: (sectionId: TodayWorkSectionId) => {
+ sectionId: TodayWorkSectionId
+ }
toggleSpace: (spaceId: string) => {
spaceId: string
}
@@ -78,7 +142,15 @@ export interface todaySpacesLogicActions {
export interface todaySpacesLogicMeta {
__keaTypeGenInternalSelectorTypes: {
sortedSpaces: (spaces: ChannelDTOApi[]) => ChannelDTOApi[]
- recentChats: (conversationHistory: ConversationDetail[]) => ConversationDetail[]
+ visibleSpaces: (browsingSpaces: boolean, sortedSpaces: ChannelDTOApi[]) => ChannelDTOApi[]
+ pinnedItems: (pinnedTasks: TaskListItemApi[]) => TodayWorkItem[]
+ recentItems: (
+ recentTasks: TaskListItemApi[],
+ conversationHistory: ConversationDetail[],
+ pinnedTasks: TaskListItemApi[]
+ ) => TodayWorkItem[]
+ recentGroups: (recentItems: TodayWorkItem[]) => TodayWorkGroup[]
+ recentLoading: (recentTasksLoading: boolean, conversationHistoryLoading: boolean) => boolean
}
}
@@ -92,13 +164,22 @@ export type todaySpacesLogicType = MakeLogicType<
export const todaySpacesLogic = kea([
path(['layout', 'today', 'todaySpacesLogic']),
connect(() => ({
- values: [teamLogic, ['currentTeamId'], maxGlobalLogic, ['conversationHistory', 'conversationHistoryLoading']],
+ values: [
+ teamLogic,
+ ['currentTeamId'],
+ userLogic,
+ ['user'],
+ maxGlobalLogic,
+ ['conversationHistory', 'conversationHistoryLoading'],
+ ],
})),
actions({
toggleSpace: (spaceId: string) => ({ spaceId }),
loadSpaceTasks: (spaceId: string) => ({ spaceId }),
setSpaceTasks: (spaceId: string, tasks: TaskListItemApi[]) => ({ spaceId, tasks }),
loadSpaceTasksFailure: (spaceId: string) => ({ spaceId }),
+ toggleSection: (sectionId: TodayWorkSectionId) => ({ sectionId }),
+ setBrowsingSpaces: (browsingSpaces: boolean) => ({ browsingSpaces }),
}),
loaders(({ values }) => ({
spaces: [
@@ -114,6 +195,39 @@ export const todaySpacesLogic = kea([
},
},
],
+ pinnedTasks: [
+ [] as TaskListItemApi[],
+ {
+ loadPinnedTasks: async () => {
+ if (!values.currentTeamId) {
+ return []
+ }
+ const response = await tasksList(String(values.currentTeamId), {
+ pinned: true,
+ basic: true,
+ limit: PINNED_SESSION_LIMIT,
+ })
+ return response.results
+ },
+ },
+ ],
+ recentTasks: [
+ [] as TaskListItemApi[],
+ {
+ loadRecentTasks: async () => {
+ if (!values.currentTeamId || !values.user) {
+ return []
+ }
+ const response = await tasksList(String(values.currentTeamId), {
+ created_by: values.user.id,
+ ordering: '-last_activity_at',
+ basic: true,
+ limit: RECENT_SESSION_LIMIT,
+ })
+ return response.results
+ },
+ },
+ ],
})),
reducers({
expandedSpaceIds: [
@@ -124,7 +238,23 @@ export const todaySpacesLogic = kea([
state.includes(spaceId) ? state.filter((id) => id !== spaceId) : [...state, spaceId],
},
],
+ collapsedSections: [
+ [] as TodayWorkSectionId[],
+ { persist: true },
+ {
+ toggleSection: (state, { sectionId }) =>
+ state.includes(sectionId) ? state.filter((id) => id !== sectionId) : [...state, sectionId],
+ },
+ ],
+ browsingSpaces: [
+ false,
+ { persist: true },
+ {
+ setBrowsingSpaces: (_, { browsingSpaces }) => browsingSpaces,
+ },
+ ],
spacesUnavailable: [false, { loadSpaces: () => false, loadSpacesFailure: () => true }],
+ recentTasksUnavailable: [false, { loadRecentTasks: () => false, loadRecentTasksFailure: () => true }],
// Each space loads on its own, so expanding several at once never drops one space's sessions.
spaceTasks: [
{} as Record,
@@ -148,10 +278,38 @@ export const todaySpacesLogic = kea([
}),
selectors({
sortedSpaces: [(s) => [s.spaces], (spaces: ChannelDTOApi[]): ChannelDTOApi[] => sortSpaces(spaces)],
- recentChats: [
- (s) => [s.conversationHistory],
- (conversationHistory: ConversationDetail[]): ConversationDetail[] =>
- conversationHistory.slice(0, RECENT_CHAT_LIMIT),
+ visibleSpaces: [
+ (s) => [s.browsingSpaces, s.sortedSpaces],
+ (browsingSpaces: boolean, sortedSpaces: ChannelDTOApi[]): ChannelDTOApi[] =>
+ browsingSpaces ? sortedSpaces : starredSpaces(sortedSpaces),
+ ],
+ pinnedItems: [
+ (s) => [s.pinnedTasks],
+ (pinnedTasks: TaskListItemApi[]): TodayWorkItem[] =>
+ pinnedTasks.filter((task) => !task.archived).map(sessionItem),
+ ],
+ recentItems: [
+ (s) => [s.recentTasks, s.conversationHistory, s.pinnedTasks],
+ (
+ recentTasks: TaskListItemApi[],
+ conversationHistory: ConversationDetail[],
+ pinnedTasks: TaskListItemApi[]
+ ): TodayWorkItem[] =>
+ buildRecentItems(
+ recentTasks,
+ conversationHistory,
+ pinnedTasks.map((task) => task.id),
+ RECENT_ITEM_LIMIT
+ ),
+ ],
+ recentGroups: [
+ (s) => [s.recentItems],
+ (recentItems: TodayWorkItem[]): TodayWorkGroup[] => groupByDay(recentItems),
+ ],
+ recentLoading: [
+ (s) => [s.recentTasksLoading, s.conversationHistoryLoading],
+ (recentTasksLoading: boolean, conversationHistoryLoading: boolean): boolean =>
+ recentTasksLoading || conversationHistoryLoading,
],
}),
listeners(({ actions, values }) => ({
@@ -186,5 +344,7 @@ export const todaySpacesLogic = kea([
})),
afterMount(({ actions }) => {
actions.loadSpaces()
+ actions.loadPinnedTasks()
+ actions.loadRecentTasks()
}),
])
diff --git a/frontend/src/layout/today/todayWorkItems.test.ts b/frontend/src/layout/today/todayWorkItems.test.ts
new file mode 100644
index 000000000000..7b3ca3db569b
--- /dev/null
+++ b/frontend/src/layout/today/todayWorkItems.test.ts
@@ -0,0 +1,44 @@
+import { dayjs } from 'lib/dayjs'
+
+import { ConversationDetail } from '~/types'
+
+import { TaskListItemApi } from 'products/tasks/frontend/generated/api.schemas'
+
+import { buildRecentItems, groupByDay } from './todayWorkItems'
+
+const session = (id: string, lastActivityAt: string, archived = false): TaskListItemApi =>
+ ({ id, title: `Session ${id}`, last_activity_at: lastActivityAt, archived }) as TaskListItemApi
+
+const chat = (id: string, updatedAt: string): ConversationDetail =>
+ ({ id, title: `Chat ${id}`, updated_at: updatedAt, created_at: updatedAt }) as ConversationDetail
+
+describe('todayWorkItems', () => {
+ it('merges sessions and chats by latest activity, without pinned or archived sessions', () => {
+ const items = buildRecentItems(
+ [
+ session('s-pinned', '2026-03-10T12:00:00'),
+ session('s-old', '2026-03-08T09:00:00'),
+ session('s-archived', '2026-03-10T11:00:00', true),
+ ],
+ [chat('c-new', '2026-03-10T10:00:00')],
+ ['s-pinned'],
+ 10
+ )
+
+ expect(items.map((item) => `${item.kind}:${item.id}`)).toEqual(['chat:c-new', 'session:s-old'])
+ })
+
+ it.each([
+ ['2026-03-10T08:00:00', 'Today'],
+ ['2026-03-10T23:00:00', 'Today'],
+ ['2026-03-09T08:00:00', 'Yesterday'],
+ ['2026-03-06T08:00:00', 'Friday'],
+ ['2026-02-20T08:00:00', 'Feb 20'],
+ ['2025-12-20T08:00:00', 'Dec 20, 2025'],
+ ])('labels an item from %s as %s', (timestamp, label) => {
+ const now = dayjs('2026-03-10T12:00:00')
+ const groups = groupByDay(buildRecentItems([session('s', timestamp)], [], [], 10), now)
+
+ expect(groups.map((group) => group.label)).toEqual([label])
+ })
+})
diff --git a/frontend/src/layout/today/todayWorkItems.ts b/frontend/src/layout/today/todayWorkItems.ts
new file mode 100644
index 000000000000..330efaa1877f
--- /dev/null
+++ b/frontend/src/layout/today/todayWorkItems.ts
@@ -0,0 +1,113 @@
+import { Dayjs, dayjs } from 'lib/dayjs'
+
+import { ConversationDetail } from '~/types'
+
+import { TaskListItemApi } from 'products/tasks/frontend/generated/api.schemas'
+
+export type TodayWorkItemKind = 'session' | 'chat'
+
+export interface TodayWorkItem {
+ kind: TodayWorkItemKind
+ id: string
+ title: string
+ timestamp: string | null
+ status: string | null
+}
+
+export interface TodayWorkGroup {
+ key: string
+ label: string
+ items: TodayWorkItem[]
+}
+
+export function sessionItem(task: TaskListItemApi): TodayWorkItem {
+ return {
+ kind: 'session',
+ id: task.id,
+ title: task.title,
+ timestamp: task.last_activity_at ?? task.updated_at ?? task.created_at ?? null,
+ status: task.latest_run?.status ?? null,
+ }
+}
+
+export function chatItem(conversation: ConversationDetail): TodayWorkItem {
+ return {
+ kind: 'chat',
+ id: conversation.id,
+ title: conversation.title ?? '',
+ timestamp: conversation.updated_at ?? conversation.created_at,
+ status: null,
+ }
+}
+
+export function buildRecentItems(
+ sessions: TaskListItemApi[],
+ chats: ConversationDetail[],
+ pinnedSessionIds: string[],
+ limit: number
+): TodayWorkItem[] {
+ const pinned = new Set(pinnedSessionIds)
+ return [
+ ...sessions.filter((task) => !pinned.has(task.id) && !task.archived).map(sessionItem),
+ ...chats.map(chatItem),
+ ]
+ .sort((first, second) => timeOf(second) - timeOf(first))
+ .slice(0, limit)
+}
+
+export function groupByDay(items: TodayWorkItem[], now: Dayjs = dayjs()): TodayWorkGroup[] {
+ const groups: TodayWorkGroup[] = []
+ for (const item of items) {
+ const time = item.timestamp ? earlierOf(dayjs(item.timestamp), now) : null
+ const key = time ? time.format('YYYY-MM-DD') : 'undated'
+ const last = groups[groups.length - 1]
+ if (last?.key === key) {
+ last.items.push(item)
+ } else {
+ groups.push({ key, label: time ? dayLabel(time, now) : 'Earlier', items: [item] })
+ }
+ }
+ return groups
+}
+
+export function dayLabel(time: Dayjs, now: Dayjs = dayjs()): string {
+ const days = now.startOf('day').diff(time.startOf('day'), 'day')
+ if (days <= 0) {
+ return 'Today'
+ }
+ if (days === 1) {
+ return 'Yesterday'
+ }
+ if (days < 7) {
+ return time.format('dddd')
+ }
+ return time.year() === now.year() ? time.format('MMM D') : time.format('MMM D, YYYY')
+}
+
+export function shortTimeAgo(timestamp: string | null, now: Dayjs = dayjs()): string {
+ if (!timestamp) {
+ return ''
+ }
+ const minutes = Math.max(0, now.diff(dayjs(timestamp), 'minute'))
+ if (minutes < 1) {
+ return 'now'
+ }
+ if (minutes < 60) {
+ return `${minutes}m`
+ }
+ if (minutes < 60 * 24) {
+ return `${Math.floor(minutes / 60)}h`
+ }
+ if (minutes < 60 * 24 * 7) {
+ return `${Math.floor(minutes / (60 * 24))}d`
+ }
+ return `${Math.floor(minutes / (60 * 24 * 7))}w`
+}
+
+function earlierOf(first: Dayjs, second: Dayjs): Dayjs {
+ return first.isAfter(second) ? second : first
+}
+
+function timeOf(item: TodayWorkItem): number {
+ return item.timestamp ? dayjs(item.timestamp).valueOf() : 0
+}
diff --git a/frontend/src/scenes/project-homepage/today/Today.stories.tsx b/frontend/src/scenes/project-homepage/today/Today.stories.tsx
index 9130a2e329ed..2e3f83b60356 100644
--- a/frontend/src/scenes/project-homepage/today/Today.stories.tsx
+++ b/frontend/src/scenes/project-homepage/today/Today.stories.tsx
@@ -49,8 +49,48 @@ const SPACES = [
]
const CONVERSATIONS = [
- { id: 'chat-1', title: 'Why did signups spike on Monday?', status: 'idle', type: 'assistant' },
- { id: 'chat-2', title: 'Weekly retention by plan', status: 'idle', type: 'assistant' },
+ {
+ id: 'chat-1',
+ title: 'Why did signups spike on Monday?',
+ status: 'idle',
+ type: 'assistant',
+ updated_at: '2026-09-28T16:10:00Z',
+ },
+ {
+ id: 'chat-2',
+ title: 'Weekly retention by plan',
+ status: 'idle',
+ type: 'assistant',
+ updated_at: '2026-09-25T09:00:00Z',
+ },
+]
+
+const PINNED_SESSIONS = [
+ {
+ id: 'task-pinned',
+ title: 'Fix the flaky checkout test',
+ archived: false,
+ last_activity_at: '2026-09-28T17:40:00Z',
+ latest_run: { status: 'in_progress' },
+ },
+]
+
+const RECENT_SESSIONS = [
+ ...PINNED_SESSIONS,
+ {
+ id: 'task-1',
+ title: 'Add a retry to the billing webhook',
+ archived: false,
+ last_activity_at: '2026-09-28T18:05:00Z',
+ latest_run: { status: 'completed' },
+ },
+ {
+ id: 'task-2',
+ title: 'Investigate the drop in trial starts',
+ archived: false,
+ last_activity_at: '2026-09-27T11:20:00Z',
+ latest_run: { status: 'failed' },
+ },
]
const LIBRARY = [
@@ -136,7 +176,15 @@ const meta: Meta = {
},
],
'/api/projects/:team_id/task_channels/': SPACES,
- '/api/projects/:team_id/tasks/': EMPTY_PAGINATED_RESPONSE,
+ '/api/projects/:team_id/tasks/': ({ request }) => {
+ const params = new URL(request.url).searchParams
+ const results = params.get('pinned')
+ ? PINNED_SESSIONS
+ : params.get('created_by')
+ ? RECENT_SESSIONS
+ : []
+ return [200, { results, count: results.length, next: null, previous: null }]
+ },
'/api/environments/:team_id/conversations/': { results: CONVERSATIONS, next: null },
'/api/environments/:team_id/file_system/': ({ request }) => {
const type = new URL(request.url).searchParams.get('type')