Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/buddy/src/i18n/locales/en-US/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default {
'desktop.tasks.deleteTask': 'Delete',
'desktop.tasks.deleteTaskConfirmMessage': '“{title}” will be removed from the task list.',
'desktop.tasks.deleteTaskConfirmTitle': 'Delete task?',
'desktop.tasks.expandRemaining': 'Show {count} more',
'desktop.imagePreview.close': 'Close image preview',
'desktop.imagePreview.download': 'Download image',
'desktop.imagePreview.next': 'Next image',
Expand Down
1 change: 1 addition & 0 deletions apps/buddy/src/i18n/locales/zh-CN/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default {
'desktop.tasks.deleteTask': '删除',
'desktop.tasks.deleteTaskConfirmMessage': '“{title}”将从任务列表中移除。',
'desktop.tasks.deleteTaskConfirmTitle': '删除任务?',
'desktop.tasks.expandRemaining': '展开其余 {count} 个',
'desktop.imagePreview.close': '关闭大图',
'desktop.imagePreview.download': '下载图片',
'desktop.imagePreview.next': '下一张图片',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useBuddyI18n } from '@/i18n/buddyI18n'
import DesktopSpaceDialog from '@/modules/tasks/widgets/task-index/DesktopSpaceDialog.vue'
import DesktopTaskRow from '@/modules/tasks/widgets/task-index/DesktopTaskRow.vue'
import DesktopTaskSearchDialog from '@/modules/tasks/widgets/task-index/DesktopTaskSearchDialog.vue'
import DesktopTaskSidebarExpandRow from '@/modules/tasks/widgets/task-index/DesktopTaskSidebarExpandRow.vue'
import DesktopTaskSidebarSection from '@/modules/tasks/widgets/task-index/DesktopTaskSidebarSection.vue'
import DesktopTaskSpaceRow from '@/modules/tasks/widgets/task-index/DesktopTaskSpaceRow.vue'
import {
Expand Down Expand Up @@ -80,6 +81,7 @@ const {
dropPinnedItem,
endPinnedDrag,
enterPinnedDropTarget,
expandConversationGroup,
getTaskTitle,
getPinnedDropPosition,
isSectionExpanded,
Expand All @@ -101,7 +103,7 @@ const {
requestTaskRename,
saveSpace,
selectSpaceMenuAction,
globalTasks,
taskRows,
taskDeleteTarget,
taskRenameTarget,
taskTitleDraft,
Expand Down Expand Up @@ -207,6 +209,12 @@ function openSearchSpace(spaceId: string) {
@toggle="toggleSpace(item.space.id)"
/>
</div>
<DesktopTaskSidebarExpandRow
v-else-if="item.kind === 'expand'"
:label="t('desktop.tasks.expandRemaining', { count: item.remaining })"
space-task
@expand="expandConversationGroup(item.groupKey)"
/>
<DesktopTaskRow
v-else
:task-id="item.task.id"
Expand Down Expand Up @@ -260,6 +268,12 @@ function openSearchSpace(spaceId: string) {
@toggle="toggleSpace(item.space.id)"
/>
</div>
<DesktopTaskSidebarExpandRow
v-else-if="item.kind === 'expand'"
:label="t('desktop.tasks.expandRemaining', { count: item.remaining })"
space-task
@expand="expandConversationGroup(item.groupKey)"
/>
<DesktopTaskRow
v-else
:task-id="item.task.id"
Expand All @@ -280,30 +294,36 @@ function openSearchSpace(spaceId: string) {

<DesktopTaskSidebarSection
:expanded="isSectionExpanded('tasks')"
:items="globalTasks"
key-field="id"
:items="taskRows"
key-field="key"
:label="t('desktop.tasks.tasksSection')"
:priority="DESKTOP_TASK_SIDEBAR_SECTION_PRIORITIES.tasks"
:scroll-index="scrollAnchors.tasks"
section="tasks"
@scroll="index => recordScrollAnchor('tasks', index)"
@update:expanded="value => setSectionExpanded('tasks', value)"
>
<template #default="{ item: task }">
<template #default="{ item }">
<DesktopTaskSidebarExpandRow
v-if="item.kind === 'expand'"
:label="t('desktop.tasks.expandRemaining', { count: item.remaining })"
@expand="expandConversationGroup(item.groupKey)"
/>
<DesktopTaskRow
:task-id="task.id"
:active="task.id === activeConversationId"
:activity="task.activity"
v-bind="markBindings(task.id)"
v-else-if="item.kind === 'task'"
:task-id="item.task.id"
:active="item.task.id === activeConversationId"
:activity="item.task.activity"
v-bind="markBindings(item.task.id)"
:language="language"
:now="relativeTimeNow"
:occurred-at="task.automationOccurrence?.scheduledFor ?? task.updatedAt"
:occurred-at="item.task.automationOccurrence?.scheduledFor ?? item.task.updatedAt"
pin-mode="pin"
:title="getTaskTitle(task)"
@delete="requestTaskDelete(task)"
@open="emit('openTask', task.id)"
@pin="pinTask(task.id)"
@rename="requestTaskRename(task)"
:title="getTaskTitle(item.task)"
@delete="requestTaskDelete(item.task)"
@open="emit('openTask', item.task.id)"
@pin="pinTask(item.task.id)"
@rename="requestTaskRename(item.task)"
/>
</template>
</DesktopTaskSidebarSection>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
defineProps<{
label: string
spaceTask?: boolean
}>()
const emit = defineEmits<{
expand: []
}>()
</script>

<template>
<div
class="desktop-task-sidebar-expand-row"
:class="{ 'is-space-task': spaceTask }"
>
<button
class="desktop-task-sidebar-expand-row__button"
type="button"
@click="emit('expand')"
>
{{ label }}
</button>
</div>
</template>

<style scoped lang="scss">
.desktop-task-sidebar-expand-row {
height: var(--buddy-task-sidebar-row-size, 2.5rem);
min-width: 0;
padding-right: var(--buddy-task-sidebar-scrollbar-gutter, 0);
padding-bottom: calc(var(--buddy-task-sidebar-row-size, 2.5rem) - var(--buddy-task-sidebar-row-height, 2.25rem));
padding-left: var(--buddy-task-sidebar-scrollbar-gutter, 0);

&.is-space-task {
padding-left: 1.75rem;
}
}

.desktop-task-sidebar-expand-row__button {
display: flex;
width: 100%;
height: var(--buddy-task-sidebar-row-height, 2.25rem);
align-items: center;
border: 0;
border-radius: var(--buddy-task-sidebar-state-radius, 8px);
background: transparent;
color: var(--buddy-text-muted);
cursor: pointer;
font-size: var(--buddy-task-sidebar-item-font-size, var(--buddy-sidebar-item-font-size));
font-weight: var(--buddy-sidebar-item-font-weight);
line-height: 20px;
padding: 0 0.5rem 0 6px;
text-align: left;
transition:
background-color var(--buddy-motion-state-duration) var(--buddy-motion-state-easing),
color var(--buddy-motion-state-duration) var(--buddy-motion-state-easing);

&:hover {
background: var(--buddy-state-hover);
color: var(--buddy-text-strong);
}

&:focus-visible {
outline: 2px solid var(--buddy-focus-ring);
outline-offset: -2px;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type { LocalConversationSummary } from '@buddy-shared/conversation/conversationApi'
import type { LocalSpace } from '@buddy-shared/spaces/spaceApi'
import { describe, expect, it } from 'vitest'
import {
DESKTOP_TASK_SIDEBAR_CONVERSATION_LIMIT,
DESKTOP_TASK_SIDEBAR_TASKS_GROUP_KEY,
resolveTaskIndexProjection,
spaceConversationGroupKey,
} from '../taskPinnedItems'

function createTask(id: string, spaceId: string | null): LocalConversationSummary {
return {
id,
spaceId,
title: id,
updatedAt: '2026-09-29T00:00:00.000Z',
activity: 'idle',
} as LocalConversationSummary
}

function createSpace(id: string): LocalSpace {
return {
id,
name: id,
icon: null,
iconColor: null,
primaryDirectory: null,
revokedAt: null,
} as unknown as LocalSpace
}

describe('taskIndexProjection pinned conversations', () => {
it('pins a conversation that belongs to a Space', () => {
const projection = resolveTaskIndexProjection({
expandedSpaceIds: new Set(['space-a']),
pinnedItems: [{ kind: 'conversation', id: 'task-a1' }],
spaces: [createSpace('space-a')],
tasks: [createTask('task-a1', 'space-a'), createTask('task-a2', 'space-a')],
})

expect(projection.pinnedRows).toEqual([

Check failure on line 41 in apps/buddy/src/modules/tasks/widgets/task-index/__tests__/taskPinnedItems.spec.ts

View workflow job for this annotation

GitHub Actions / Workspace checks

[unit] src/modules/tasks/widgets/task-index/__tests__/taskPinnedItems.spec.ts > taskIndexProjection pinned conversations > pins a conversation that belongs to a Space

AssertionError: expected [] to deeply equal [ { task: ObjectContaining{…}, …(4) } ] - Expected + Received - [ - { - "key": "pinned:conversation:task-a1", - "kind": "task", - "pinKey": "conversation:task-a1", - "pinnedTopLevel": true, - "task": ObjectContaining { - "id": "task-a1", - }, - }, - ] + [] ❯ src/modules/tasks/widgets/task-index/__tests__/taskPinnedItems.spec.ts:41:35
{ task: expect.objectContaining({ id: 'task-a1' }), key: 'pinned:conversation:task-a1', kind: 'task', pinKey: 'conversation:task-a1', pinnedTopLevel: true },
])
expect(projection.spaceRows.flatMap(row => row.kind === 'task' ? [row.task.id] : [])).toEqual(['task-a2'])
})

it('keeps a pinned Space conversation out of the pinned Space expansion', () => {
const projection = resolveTaskIndexProjection({
expandedSpaceIds: new Set(['space-a']),
pinnedItems: [{ kind: 'space', id: 'space-a' }, { kind: 'conversation', id: 'task-a1' }],
spaces: [createSpace('space-a')],
tasks: [createTask('task-a1', 'space-a'), createTask('task-a2', 'space-a')],
})

expect(projection.pinnedRows.map(row => row.key)).toEqual([

Check failure on line 55 in apps/buddy/src/modules/tasks/widgets/task-index/__tests__/taskPinnedItems.spec.ts

View workflow job for this annotation

GitHub Actions / Workspace checks

[unit] src/modules/tasks/widgets/task-index/__tests__/taskPinnedItems.spec.ts > taskIndexProjection pinned conversations > keeps a pinned Space conversation out of the pinned Space expansion

AssertionError: expected [ 'pinned:space:space-a', …(2) ] to deeply equal [ 'pinned:space:space-a', …(2) ] - Expected + Received [ "pinned:space:space-a", + "pinned:space:space-a:conversation:task-a1", "pinned:space:space-a:conversation:task-a2", - "pinned:conversation:task-a1", ] ❯ src/modules/tasks/widgets/task-index/__tests__/taskPinnedItems.spec.ts:55:55
'pinned:space:space-a',
'pinned:space:space-a:conversation:task-a2',
'pinned:conversation:task-a1',
])
})

it('drops pinned conversations that no longer exist', () => {
const projection = resolveTaskIndexProjection({
expandedSpaceIds: new Set(['space-a']),
pinnedItems: [{ kind: 'conversation', id: 'task-gone' }],
spaces: [createSpace('space-a')],
tasks: [createTask('task-a1', 'space-a')],
})

expect(projection.pinnedRows).toEqual([])
})
})

describe('taskIndexProjection conversation limit', () => {
const tasks = Array.from({ length: 8 }, (_, index) => createTask(`task-${index}`, null))

it('caps the tasks group and appends an expand row with the remaining count', () => {
const projection = resolveTaskIndexProjection({
expandedSpaceIds: new Set(),
pinnedItems: [],
spaces: [],
tasks,
})

expect(projection.taskRows).toHaveLength(DESKTOP_TASK_SIDEBAR_CONVERSATION_LIMIT + 1)
expect(projection.taskRows.at(-1)).toEqual({
groupKey: DESKTOP_TASK_SIDEBAR_TASKS_GROUP_KEY,
key: `${DESKTOP_TASK_SIDEBAR_TASKS_GROUP_KEY}:expand`,
kind: 'expand',
remaining: 8 - DESKTOP_TASK_SIDEBAR_CONVERSATION_LIMIT,
})
})

it('shows every conversation once the group is expanded', () => {
const projection = resolveTaskIndexProjection({
expandedConversationGroups: new Set([DESKTOP_TASK_SIDEBAR_TASKS_GROUP_KEY]),
expandedSpaceIds: new Set(),
pinnedItems: [],
spaces: [],
tasks,
})

expect(projection.taskRows).toHaveLength(tasks.length)
expect(projection.taskRows.some(row => row.kind === 'expand')).toBe(false)
})

it('caps a Space conversation list independently', () => {
const projection = resolveTaskIndexProjection({
expandedSpaceIds: new Set(['space-a']),
pinnedItems: [],
spaces: [createSpace('space-a')],
tasks: Array.from({ length: 7 }, (_, index) => createTask(`task-a${index}`, 'space-a')),
})

const taskRows = projection.spaceRows.filter(row => row.kind !== 'space')
expect(taskRows).toHaveLength(DESKTOP_TASK_SIDEBAR_CONVERSATION_LIMIT + 1)
expect(taskRows.at(-1)).toMatchObject({
groupKey: spaceConversationGroupKey('space-a'),
kind: 'expand',
remaining: 2,
})
})

it('does not add an expand row when the group fits the limit', () => {
const projection = resolveTaskIndexProjection({
expandedSpaceIds: new Set(),
pinnedItems: [],
spaces: [],
tasks: tasks.slice(0, DESKTOP_TASK_SIDEBAR_CONVERSATION_LIMIT),
})

expect(projection.taskRows).toHaveLength(DESKTOP_TASK_SIDEBAR_CONVERSATION_LIMIT)
expect(projection.taskRows.some(row => row.kind === 'expand')).toBe(false)
})
})
Loading
Loading