Skip to content
Merged
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
118 changes: 97 additions & 21 deletions packages/client/src/api/hermes/kanban.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,35 @@ export interface KanbanAssignee {
counts: Record<string, number> | null
}

export interface KanbanBoard {
slug: string
name: string
description: string
icon: string
color: string
created_at: number | null
archived: boolean
db_path?: string
is_current?: boolean
counts: Record<string, number>
total: number
}

export interface KanbanBoardCreateRequest {
slug: string
name?: string
description?: string
icon?: string
color?: string
switchCurrent?: boolean
}

export interface KanbanCapabilities {
source: 'hermes-cli'
supports: Record<string, boolean>
missing: string[]
}

export interface KanbanCreateRequest {
title: string
body?: string
Expand All @@ -106,68 +135,115 @@ export interface KanbanCreateRequest {
tenant?: string
}

// ─── API functions ───────────────────────────────────────────────
export interface KanbanBoardOptions {
board?: string
}

export async function listTasks(opts?: {
export interface KanbanListOptions extends KanbanBoardOptions {
status?: string
assignee?: string
tenant?: string
}): Promise<KanbanTask[]> {
}

function normalizedBoard(board?: string): string {
const trimmed = board?.trim()
return trimmed || 'default'
}

function appendQuery(path: string, params: URLSearchParams): string {
const qs = params.toString()
return qs ? `${path}?${qs}` : path
}

function boardParams(board?: string): URLSearchParams {
const params = new URLSearchParams()
params.set('board', normalizedBoard(board))
return params
}

// ─── API functions ───────────────────────────────────────────────

export async function listBoards(opts?: { includeArchived?: boolean }): Promise<KanbanBoard[]> {
const params = new URLSearchParams()
if (opts?.includeArchived) params.set('includeArchived', 'true')
const res = await request<{ boards: KanbanBoard[] }>(appendQuery('/api/hermes/kanban/boards', params))
return res.boards
}

export async function createBoard(data: KanbanBoardCreateRequest): Promise<KanbanBoard> {
const res = await request<{ board: KanbanBoard }>('/api/hermes/kanban/boards', {
method: 'POST',
body: JSON.stringify(data),
})
return res.board
}

export async function archiveBoard(slug: string): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(`/api/hermes/kanban/boards/${encodeURIComponent(slug)}`, {
method: 'DELETE',
})
}

export async function getCapabilities(): Promise<KanbanCapabilities> {
const res = await request<{ capabilities: KanbanCapabilities }>('/api/hermes/kanban/capabilities')
return res.capabilities
}

export async function listTasks(opts?: KanbanListOptions): Promise<KanbanTask[]> {
const params = boardParams(opts?.board)
if (opts?.status) params.set('status', opts.status)
if (opts?.assignee) params.set('assignee', opts.assignee)
if (opts?.tenant) params.set('tenant', opts.tenant)
const qs = params.toString()
const res = await request<{ tasks: KanbanTask[] }>(`/api/hermes/kanban${qs ? `?${qs}` : ''}`)
const res = await request<{ tasks: KanbanTask[] }>(appendQuery('/api/hermes/kanban', params))
return res.tasks
}

export async function getTask(id: string): Promise<KanbanTaskDetail> {
return request<KanbanTaskDetail>(`/api/hermes/kanban/${id}`)
export async function getTask(id: string, opts?: KanbanBoardOptions): Promise<KanbanTaskDetail> {
return request<KanbanTaskDetail>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(id)}`, boardParams(opts?.board)))
}

export async function createTask(data: KanbanCreateRequest): Promise<KanbanTask> {
const res = await request<{ task: KanbanTask }>('/api/hermes/kanban', {
export async function createTask(data: KanbanCreateRequest, opts?: KanbanBoardOptions): Promise<KanbanTask> {
const res = await request<{ task: KanbanTask }>(appendQuery('/api/hermes/kanban', boardParams(opts?.board)), {
method: 'POST',
body: JSON.stringify(data),
})
return res.task
}

export async function completeTasks(taskIds: string[], summary?: string): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>('/api/hermes/kanban/complete', {
export async function completeTasks(taskIds: string[], summary?: string, opts?: KanbanBoardOptions): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(appendQuery('/api/hermes/kanban/complete', boardParams(opts?.board)), {
method: 'POST',
body: JSON.stringify({ task_ids: taskIds, summary }),
})
}

export async function blockTask(taskId: string, reason: string): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(`/api/hermes/kanban/${taskId}/block`, {
export async function blockTask(taskId: string, reason: string, opts?: KanbanBoardOptions): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/block`, boardParams(opts?.board)), {
method: 'POST',
body: JSON.stringify({ reason }),
})
}

export async function unblockTasks(taskIds: string[]): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>('/api/hermes/kanban/unblock', {
export async function unblockTasks(taskIds: string[], opts?: KanbanBoardOptions): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(appendQuery('/api/hermes/kanban/unblock', boardParams(opts?.board)), {
method: 'POST',
body: JSON.stringify({ task_ids: taskIds }),
})
}

export async function assignTask(taskId: string, profile: string): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(`/api/hermes/kanban/${taskId}/assign`, {
export async function assignTask(taskId: string, profile: string, opts?: KanbanBoardOptions): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/assign`, boardParams(opts?.board)), {
method: 'POST',
body: JSON.stringify({ profile }),
})
}

export async function getStats(): Promise<KanbanStats> {
const res = await request<{ stats: KanbanStats }>('/api/hermes/kanban/stats')
export async function getStats(opts?: KanbanBoardOptions): Promise<KanbanStats> {
const res = await request<{ stats: KanbanStats }>(appendQuery('/api/hermes/kanban/stats', boardParams(opts?.board)))
return res.stats
}

export async function getAssignees(): Promise<KanbanAssignee[]> {
const res = await request<{ assignees: KanbanAssignee[] }>('/api/hermes/kanban/assignees')
export async function getAssignees(opts?: KanbanBoardOptions): Promise<KanbanAssignee[]> {
const res = await request<{ assignees: KanbanAssignee[] }>(appendQuery('/api/hermes/kanban/assignees', boardParams(opts?.board)))
return res.assignees
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const priorityOptions = computed(() => [
const assigneeOptions = computed(() => {
return kanbanStore.assignees.map(a => {
const total = Object.values(a.counts || {}).reduce((s, c) => s + c, 0)
return { label: `${a.name} (${total})`, value: a.name }
return { label: `${a.name} · ${t('kanban.stats.tasks')}: ${total}`, value: a.name }
})
})

Expand Down
21 changes: 14 additions & 7 deletions packages/client/src/components/hermes/kanban/KanbanTaskDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async function searchTaskSessions() {
sessionLoading.value = true
try {
const res = await request<{ results: any[] }>(
`/api/hermes/kanban/search-sessions?task_id=${encodeURIComponent(detail.value.task.id)}&profile=${encodeURIComponent(profile)}`
`/api/hermes/kanban/search-sessions?task_id=${encodeURIComponent(detail.value.task.id)}&profile=${encodeURIComponent(profile)}&board=${encodeURIComponent(kanbanStore.selectedBoard)}`
)
sessionResults.value = res.results
} catch {
Expand Down Expand Up @@ -103,22 +103,29 @@ const historySession = computed<Session | null>(() => {
const assigneeOptions = computed(() => {
return kanbanStore.assignees.map(a => {
const total = Object.values(a.counts || {}).reduce((s, c) => s + c, 0)
return { label: `${a.name} (${total})`, value: a.name }
return { label: `${a.name} · ${t('kanban.stats.tasks')}: ${total}`, value: a.name }
})
})

watch(() => props.taskId, async (id) => {
watch(() => [props.taskId, kanbanStore.selectedBoard] as const, async ([id, board]) => {
if (!id) {
detail.value = null
return
}
loading.value = true
try {
detail.value = await getTask(id)
const nextDetail = await getTask(id, { board })
if (props.taskId === id && kanbanStore.selectedBoard === board) {
detail.value = nextDetail
}
} catch (err: any) {
message.error(t('kanban.message.loadFailed'))
if (props.taskId === id && kanbanStore.selectedBoard === board) {
message.error(t('kanban.message.loadFailed'))
}
} finally {
loading.value = false
if (props.taskId === id && kanbanStore.selectedBoard === board) {
loading.value = false
}
}
}, { immediate: true })

Expand Down Expand Up @@ -178,7 +185,7 @@ async function handleAssign() {
message.success(t('kanban.message.taskAssigned'))
assignProfile.value = null
if (detail.value) {
detail.value = await getTask(props.taskId)
detail.value = await getTask(props.taskId, { board: kanbanStore.selectedBoard })
}
emit('updated')
} catch (err: any) {
Expand Down
11 changes: 11 additions & 0 deletions packages/client/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ export default {
noTasks: 'No tasks',
allStatuses: 'All Statuses',
allAssignees: 'All Assignees',
board: {
create: 'New Board',
archive: 'Archive Board',
slugPlaceholder: 'Board slug, e.g. project-a',
namePlaceholder: 'Display name (optional)',
slugRequired: 'Board slug is required',
created: 'Board created',
archived: 'Board archived',
archiveConfirm: 'Archive the current board?',
},
columns: {
triage: 'Triage',
todo: 'To Do',
Expand Down Expand Up @@ -299,6 +309,7 @@ export default {
},
stats: {
total: 'Total',
tasks: 'Tasks',
},
},

Expand Down
11 changes: 11 additions & 0 deletions packages/client/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@ export default {
noTasks: '暂无任务',
allStatuses: '全部状态',
allAssignees: '全部负责人',
board: {
create: '新建看板',
archive: '归档看板',
slugPlaceholder: '看板标识,例如 project-a',
namePlaceholder: '显示名称(可选)',
slugRequired: '看板标识不能为空',
created: '看板已创建',
archived: '看板已归档',
archiveConfirm: '确定归档当前看板?',
},
columns: {
triage: '待分拣',
todo: '待办',
Expand Down Expand Up @@ -299,6 +309,7 @@ export default {
},
stats: {
total: '总计',
tasks: '任务数',
},
},

Expand Down
Loading
Loading