Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f3f8493
Coalesce backend watcher emissions to reduce churn floods
Mar 5, 2026
e0c26bd
Fix Tauri dev startup stability across terminals
Mar 5, 2026
92a9cb6
fix: replace debounce+isProcessing with queue-based single-flight han…
Mar 5, 2026
267393a
feat: add poll backpressure scheduler to prevent UI freezes during hi…
Mar 5, 2026
de3ca3a
feat: add freeze diagnostics for watcher/poll pipeline
Mar 5, 2026
b595162
feat: add churn stress test and runbook (beads-task-issue-tracker-mtu.5)
Mar 5, 2026
54f07db
fix: add right padding for window controls on Linux/Windows
Mar 5, 2026
084564e
Attempt fix ui issue with top right buttons not showing
Mar 5, 2026
5fd6144
Fix stale blocked indicators from closed blockers
Mar 7, 2026
873d9fd
Add copy ID button to dashboard quick lists
Mar 7, 2026
8a9a111
Fix blocked issue mapping in filters and dashboard
Mar 7, 2026
6abce46
Reorder dashboard KPI cards in project panel
Mar 7, 2026
74f254a
Remove obsolete deleted status handling
Mar 7, 2026
f722a92
Fix total KPI status coverage and add workflow filters
Mar 7, 2026
aa1e55f
Make Workflow KPI first and default on startup
Mar 7, 2026
558d280
Keep Workflow KPI active when status filter is empty
Mar 7, 2026
431806b
Fix responsive wrapping for left sidebar KPI tiles
Mar 7, 2026
35c8cd1
fix: remove text truncation on KPI filter tiles to show full labels
Mar 7, 2026
9642785
fix: prevent KPI card selection ring from being clipped
Mar 7, 2026
e8eb6e3
fix: use flex layout for KPI tiles so each sizes to its content
Mar 7, 2026
d79fc8c
fix: add consistent horizontal padding to KPI tiles
Mar 7, 2026
52ff5b0
fix: prevent KPI tiles from flex-shrinking so padding stays consistent
Mar 7, 2026
f07bf67
fix: KPI filter tiles text truncation, border clipping, and min-width
Mar 7, 2026
afe57fb
refactor: rename Total KPI tile to All
Mar 7, 2026
a59a554
feat: change default filter to show all issues when no filters active
Mar 7, 2026
b3beb71
fix: increase KPI tile minimum width to 90px
Mar 7, 2026
15883c5
refactor: move blocked-by info into StatusBadge tooltip, remove separ…
Mar 7, 2026
c722400
fix: exclude dependency-blocked issues from Open KPI tile filter
Mar 7, 2026
6d8d053
feat: clicking All KPI tile adds all status filters to filter bar
Mar 7, 2026
dd7d12e
fix: compute blockedBy from dependencies in Nitro server transformer
Mar 7, 2026
7028d51
fix: align workflow KPI with default issue filtering
Mar 7, 2026
ff2598e
fix: align Open KPI count with Open filter results
Mar 7, 2026
906e5fc
fix: use :model-value instead of :checked on DropdownMenuCheckboxItem
Mar 7, 2026
8814a19
fix: show active filter chips when search is active
Mar 7, 2026
e4438c6
fix: text search now respects active filters instead of bypassing them
Mar 7, 2026
78db430
feat: add column reordering in column settings dropdown
Mar 7, 2026
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ The app uses a **native file watcher** on the `.beads` directory. When an AI age
- **Gallery Navigation**: Browse multiple attached files with arrow keys or buttons

### Filtering & Display
- **Extended Status Support**: All Beads statuses handled — `deferred`, `pinned`, `hooked`, and `tombstone` (deleted) issues filtered from default view
- **Extended Status Support**: All active Beads statuses handled — `deferred`, `pinned`, and `hooked`
- **Advanced Filters**: Multi-select filters by type, status, priority, labels, and assignee
- **Exclusion Filters**: Hide specific issues by criteria (inverse filtering)
- **Search**: Find issues by title, ID, or description
Expand Down
38 changes: 32 additions & 6 deletions app/components/dashboard/DashboardContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ import {
TooltipTrigger,
} from '~/components/ui/tooltip'

type KpiFilter = 'total' | 'open' | 'in_progress' | 'blocked'
type KpiFilter = 'total' | 'open' | 'in_progress' | 'blocked' | 'workflow'

const props = withDefaults(defineProps<{
stats: DashboardStats | null
readyIssues: Issue[]
inProgressIssues: Issue[]
blockedIssues: Issue[]
pinnedIssues: Issue[]
pinnedSortMode?: PinnedSortMode
kpiGridCols?: 2 | 4
kpiGridCols?: 2 | 5
activeKpiFilter: KpiFilter | null
statusFilters: string[]
showOnboarding?: boolean
hideKpis?: boolean
}>(), {
kpiGridCols: 4,
kpiGridCols: 5,
showOnboarding: false,
hideKpis: false,
})
Expand All @@ -46,18 +46,20 @@ const emit = defineEmits<{
// Collapsible state (per-project, singleton)
const isChartsCollapsed = useProjectStorage('chartsCollapsed', true)
const isInProgressCollapsed = useProjectStorage('inProgressCollapsed', true)
const isBlockedCollapsed = useProjectStorage('blockedCollapsed', true)
const isPinnedCollapsed = useProjectStorage('pinnedCollapsed', false)
const isReadyCollapsed = useProjectStorage('readyCollapsed', true)
</script>

<template>
<template v-if="stats">
<!-- KPI cards (hidden in desktop scrollable section where KPIs are in the fixed section) -->
<div v-if="!hideKpis" :class="['grid', kpiGridCols === 4 ? 'grid-cols-4 gap-1.5' : 'grid-cols-2 gap-3']">
<KpiCard title="Total" :value="stats.total" :active="activeKpiFilter === null && statusFilters.length === 0" @click="emit('kpi-click', 'total')" />
<div v-if="!hideKpis" :class="['flex flex-wrap p-0.5 -m-0.5', kpiGridCols === 5 ? 'gap-1.5' : 'gap-3']">
<KpiCard title="Workflow" :value="stats.workflow" color="var(--color-status-deferred)" :active="activeKpiFilter === 'workflow'" @click="emit('kpi-click', 'workflow')" />
<KpiCard title="Open" :value="stats.open" color="var(--color-status-open)" :active="activeKpiFilter === 'open'" @click="emit('kpi-click', 'open')" />
<KpiCard title="In Progress" :value="stats.inProgress" color="var(--color-status-in-progress)" :active="activeKpiFilter === 'in_progress'" @click="emit('kpi-click', 'in_progress')" />
<KpiCard title="Blocked" :value="stats.blocked" color="var(--color-status-blocked)" :active="activeKpiFilter === 'blocked'" @click="emit('kpi-click', 'blocked')" />
<KpiCard title="All" :value="stats.total" :active="activeKpiFilter === 'total'" @click="emit('kpi-click', 'total')" />
</div>

<!-- Collapsible Charts Section -->
Expand Down Expand Up @@ -108,6 +110,30 @@ const isReadyCollapsed = useProjectStorage('readyCollapsed', true)
</div>
</div>

<!-- Collapsible Blocked Section -->
<div v-if="blockedIssues.length > 0" class="space-y-2">
<button
class="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors w-full"
@click="isBlockedCollapsed = !isBlockedCollapsed"
>
<svg
class="w-3 h-3 transition-transform"
:class="{ '-rotate-90': isBlockedCollapsed }"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9" />
</svg>
<span class="uppercase tracking-wide">Blocked</span>
<span class="text-[10px] ml-auto">({{ blockedIssues.length }})</span>
</button>
<div v-show="!isBlockedCollapsed" class="pl-5">
<QuickList :issues="blockedIssues" @select="emit('select-issue', $event)" />
</div>
</div>

<!-- Collapsible Pinned Section -->
<div v-if="pinnedIssues.length > 0" class="space-y-2">
<div class="flex items-center gap-2 text-xs text-muted-foreground w-full">
Expand Down
6 changes: 3 additions & 3 deletions app/components/dashboard/KpiCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ const neonTitleStyle = computed(() => {

<template>
<button
class="p-1.5 rounded-md text-left w-full transition-colors"
class="px-2.5 py-1.5 rounded-md text-left transition-colors min-w-[90px]"
:class="[
active ? 'ring-2 ring-primary' : '',
active ? 'outline-2 outline-primary' : '',
isNeon
? 'border hover:brightness-125'
: 'bg-secondary/30 border border-border/50 hover:bg-secondary/50'
Expand All @@ -65,7 +65,7 @@ const neonTitleStyle = computed(() => {
@click="$emit('click')"
>
<div
class="text-[9px] uppercase tracking-wide mb-0.5 truncate"
class="text-[9px] uppercase tracking-wide mb-0.5 whitespace-nowrap"
:class="isNeon && color ? '' : 'text-muted-foreground'"
:style="neonTitleStyle"
>
Expand Down
82 changes: 72 additions & 10 deletions app/components/dashboard/QuickList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ const { focusedId, setFocused, handleKeydown, isFocused } = useKeyboardNavigatio
},
})

const copiedIssueId = ref<string | null>(null)
let copiedResetTimer: ReturnType<typeof setTimeout> | null = null

const copyIssueId = async (issueId: string, event: Event) => {
event.stopPropagation()

try {
await navigator.clipboard.writeText(issueId)
copiedIssueId.value = issueId

if (copiedResetTimer) {
clearTimeout(copiedResetTimer)
}

copiedResetTimer = setTimeout(() => {
copiedIssueId.value = null
}, 2000)
} catch (err) {
console.error('Failed to copy issue ID:', err)
}
}

onBeforeUnmount(() => {
if (copiedResetTimer) {
clearTimeout(copiedResetTimer)
}
})

const getShortId = (id: string) => {
const dotIndex = id.indexOf('.')
const baseId = dotIndex > 0 ? id.slice(0, dotIndex) : id
Expand All @@ -45,20 +73,54 @@ const getShortId = (id: string) => {
</div>

<div v-else class="space-y-1 pr-4 outline-none" tabindex="0" @keydown="handleKeydown">
<button
<div
v-for="issue in issues"
:key="issue.id"
class="w-full text-left p-1.5 rounded hover:bg-secondary/50 transition-colors"
:data-issue-id="issue.id"
class="w-full p-1.5 rounded hover:bg-secondary/50 transition-colors flex items-start gap-1.5"
:class="isFocused(issue.id) ? 'bg-primary/10 ring-1 ring-inset ring-primary/40' : ''"
@click="setFocused(issue.id); $emit('select', issue)"
>
<div class="flex items-center gap-1.5 mb-0.5">
<TypeBadge :type="issue.type" size="sm" />
<PriorityBadge :priority="issue.priority" size="sm" />
<span class="text-[10px] text-muted-foreground font-mono">{{ getShortId(issue.id) }}</span>
</div>
<p class="text-[11px] leading-tight line-clamp-2">{{ issue.title }}</p>
</button>
<button
class="flex-1 min-w-0 text-left"
@click="setFocused(issue.id); $emit('select', issue)"
>
<div class="flex items-center gap-1.5 mb-0.5">
<TypeBadge :type="issue.type" size="sm" />
<PriorityBadge :priority="issue.priority" size="sm" />
<span class="text-[10px] text-muted-foreground font-mono">{{ getShortId(issue.id) }}</span>
</div>
<p class="text-[11px] leading-tight line-clamp-2">{{ issue.title }}</p>
</button>

<button
class="shrink-0 p-0.5 mt-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"
:title="`Copy issue ID ${issue.id}`"
:aria-label="`Copy issue ID ${issue.id}`"
@click="copyIssueId(issue.id, $event)"
>
<svg
v-if="copiedIssueId !== issue.id"
class="w-3 h-3"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
<svg
v-else
class="w-3 h-3 text-green-500"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</button>
</div>
</div>
</ScrollArea>
</div>
Expand Down
3 changes: 2 additions & 1 deletion app/components/details/IssueDetailHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TypeBadge from '~/components/issues/TypeBadge.vue'
import StatusBadge from '~/components/issues/StatusBadge.vue'
import PriorityBadge from '~/components/issues/PriorityBadge.vue'
import { Button } from '~/components/ui/button'
import { isIssueBlocked } from '~/utils/issue-helpers'

defineProps<{
selectedIssue: Issue
Expand All @@ -25,7 +26,7 @@ defineEmits<{
<div class="flex items-center gap-1.5 flex-wrap">
<CopyableId :value="selectedIssue.id" :display-value="selectedIssue.id.includes('-') ? selectedIssue.id.slice(selectedIssue.id.lastIndexOf('-') + 1) : selectedIssue.id" />
<TypeBadge :type="selectedIssue.type" size="sm" />
<StatusBadge :status="selectedIssue.status" size="sm" />
<StatusBadge :status="isIssueBlocked(selectedIssue) ? 'blocked' : selectedIssue.status" :blocked-by="selectedIssue.blockedBy" size="sm" />
<PriorityBadge :priority="selectedIssue.priority" size="sm" />
</div>

Expand Down
5 changes: 3 additions & 2 deletions app/components/details/IssuePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import StatusBadge from '~/components/issues/StatusBadge.vue'
import PriorityBadge from '~/components/issues/PriorityBadge.vue'
import ImageThumbnail from '~/components/ui/image-preview/ImageThumbnail.vue'
import { extractNonImageRefs, isUrl } from '~/utils/markdown'
import { isIssueBlocked } from '~/utils/issue-helpers'
import type { AttachmentFile } from '~/composables/useAttachments'

const { currentTheme } = useTheme()
Expand Down Expand Up @@ -490,7 +491,7 @@ const formatEstimate = (minutes: number) => {
<span class="text-xs truncate">{{ issue.parent.title }}</span>
</div>
<div class="flex items-center gap-1 shrink-0">
<StatusBadge :status="issue.parent.status" size="sm" />
<StatusBadge :status="isIssueBlocked(issue.parent) ? 'blocked' : issue.parent.status" :blocked-by="issue.parent.blockedBy" size="sm" />
<PriorityBadge :priority="issue.parent.priority" size="sm" />
</div>
</div>
Expand Down Expand Up @@ -542,7 +543,7 @@ const formatEstimate = (minutes: number) => {
<span class="text-xs truncate">{{ child.title }}</span>
</div>
<div class="flex items-center gap-1 shrink-0">
<StatusBadge :status="child.status" size="sm" />
<StatusBadge :status="isIssueBlocked(child) ? 'blocked' : child.status" :blocked-by="child.blockedBy" size="sm" />
<PriorityBadge :priority="child.priority" size="sm" />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/components/issues/AssigneeFilterDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const isSelected = (assignee: string) => props.selectedAssignees.includes(assign
<DropdownMenuCheckboxItem
v-for="assignee in availableAssignees"
:key="assignee"
:checked="isSelected(assignee)"
:model-value="isSelected(assignee)"
class="text-xs cursor-pointer"
@select.prevent="$emit('toggle', assignee)"
>
Expand Down
Loading