11<script setup lang="ts">
2+ import type { DevframeMessageEntry } from ' @devframes/hub/types'
23import type { DevframeConnectionStatus , DevframeRpcClient } from ' devframe/client'
3- import type { DevframeMessageEntry } from ' ../types'
4+ import DisplayBadge from ' @antfu/design/components/Display/DisplayBadge.vue'
5+ import FormSearchField from ' @antfu/design/components/Form/FormSearchField.vue'
46import LayoutToolbar from ' @antfu/design/components/Layout/LayoutToolbar.vue'
57import { computed , onBeforeUnmount , ref } from ' vue'
68import {
@@ -13,6 +15,8 @@ import {
1315 connectionTitle ,
1416} from ' ../../../../design/design'
1517import MessagesView from ' ./components/MessagesView.vue'
18+ import MessageToolbarActions from ' ./components/MessageToolbarActions.vue'
19+ import { useMessageFilters } from ' ./composables/useMessageFilters'
1620import { useMessages } from ' ./state/messages'
1721
1822const props = defineProps <{
@@ -21,6 +25,10 @@ const props = defineProps<{
2125
2226const state = useMessages (props .rpc )
2327
28+ // Search / sort / filter state lives here (not in the view) so the nav bar's
29+ // search field + actions and the view's filter bar share one source of truth.
30+ const filters = useMessageFilters (() => state .entries )
31+
2432// The live feed rides on shared-state over the socket, so a dropped socket or
2533// refused auth is surfaced instead of silently freezing the list. The client
2634// doesn't auto-reconnect; a reload re-runs the whole handshake.
@@ -50,9 +58,9 @@ async function onDismiss(id: string): Promise<void> {
5058 await props .rpc .call (' devframes:plugin:messages:remove' , id )
5159}
5260
53- async function onDismissMany( ids : string [] ): Promise <void > {
54- for (const id of ids )
55- await props .rpc .call (' devframes:plugin:messages:remove' , id )
61+ async function onDismissFiltered( ): Promise <void > {
62+ for (const entry of filters . filteredEntries )
63+ await props .rpc .call (' devframes:plugin:messages:remove' , entry . id )
5664}
5765
5866async function onClear(): Promise <void > {
@@ -85,17 +93,36 @@ async function onOpenFile(entry: DevframeMessageEntry): Promise<void> {
8593 </div >
8694
8795 <template #search >
88- <div />
96+ <div class =" flex gap-2 items-center" >
97+ <FormSearchField
98+ v-if =" ! connState "
99+ v-model =" filters .search "
100+ size="sm"
101+ placeholder="Search messages…"
102+ class="max-w-64"
103+ />
104+ <DisplayBadge v-if =" filters .totalCount > 0 " :color =" false " class="text-xs font-mono">
105+ <template v-if =" filters .filteredCount !== filters .totalCount " >
106+ {{ filters.filteredCount }}/{{ filters.totalCount }}
107+ </template >
108+ <template v-else >
109+ {{ filters.totalCount }}
110+ </template >
111+ </DisplayBadge >
112+ </div >
89113 </template >
90114
91115 <template #end >
116+ <MessageToolbarActions
117+ v-if =" ! connState "
118+ :filters
119+ @dismiss-filtered =" onDismissFiltered "
120+ @clear =" onClear "
121+ />
92122 <span v-if =" conn" :class =" conn.class" >
93123 <span :class =" conn.dot" />
94124 {{ conn.label }}
95125 </span >
96- <span v-if =" state.entries.length > 0" class =" badge-muted font-mono" >
97- {{ state.entries.length }}
98- </span >
99126 </template >
100127 </LayoutToolbar >
101128
@@ -122,11 +149,9 @@ async function onOpenFile(entry: DevframeMessageEntry): Promise<void> {
122149 </div >
123150 <MessagesView
124151 v-else
125- :entries = " state . entries "
152+ :filters
126153 :can-open-file =" canOpenFile "
127154 @dismiss =" onDismiss "
128- @dismiss-many =" onDismissMany "
129- @clear =" onClear "
130155 @persist =" onPersist "
131156 @open-file =" onOpenFile "
132157 />
0 commit comments