@@ -17,6 +17,7 @@ import StatusBar from './StatusBar.vue'
1717import ChatArea from ' ./ChatArea.vue'
1818import PathPickerModal from ' ./PathPickerModal.vue'
1919import Explorer from ' ./Explorer.vue'
20+ import { useConfirm } from ' sbot-ui'
2021
2122const props = withDefaults (defineProps <{
2223 transport: IChatTransport
@@ -29,6 +30,7 @@ const props = withDefaults(defineProps<{
2930})
3031
3132const L = computed (() => resolveLabels (props .labels ))
33+ const { confirm } = useConfirm ()
3234
3335// ── Core state ──
3436
@@ -64,6 +66,9 @@ const explorerWidth = ref(420)
6466const explorerResizing = ref (false )
6567const sessionBarWidth = ref (180 )
6668const sessionBarResizing = ref (false )
69+ const sessionSearch = ref (' ' )
70+ const sessionHighlightIndex = ref (0 )
71+ const sessionSearchInputEl = ref <HTMLInputElement | null >(null )
6772
6873// ── Derived ──
6974
@@ -89,6 +94,23 @@ const contextWindow = computed<number | undefined>(() => {
8994
9095const fetchThinks = computed (() => props .transport .fetchThinks ?.bind (props .transport ))
9196
97+ const filteredSessions = computed <SessionItem []>(() => {
98+ const q = sessionSearch .value .trim ().toLowerCase ()
99+ if (! q ) return sessions .value
100+ return sessions .value .filter (s => {
101+ const name = (s .name || ' ' ).toLowerCase ()
102+ const path = (s .workPath || ' ' ).toLowerCase ()
103+ return name .includes (q ) || path .includes (q )
104+ })
105+ })
106+
107+ const highlightedSessionId = computed <string | null >(() => {
108+ const list = filteredSessions .value
109+ if (list .length === 0 ) return null
110+ const idx = Math .min (Math .max (sessionHighlightIndex .value , 0 ), list .length - 1 )
111+ return list [idx ]?.id ?? null
112+ })
113+
92114const archivedCount = computed (() => messages .value .filter (m => m .kind === MessageKind .Archive ).length )
93115const displayedMessages = computed <StoredMessage []>(() =>
94116 showArchived .value ? messages .value : messages .value .filter (m => m .kind !== MessageKind .Archive ),
@@ -202,7 +224,55 @@ function selectSession(id: string) {
202224
203225function toggleSidebar() {
204226 sidebarOpen .value = ! sidebarOpen .value
205- if (sidebarOpen .value ) settingsOpen .value = false
227+ if (sidebarOpen .value ) {
228+ settingsOpen .value = false
229+ resetSessionSearch ()
230+ nextTick (() => sessionSearchInputEl .value ?.focus ())
231+ }
232+ }
233+
234+ function resetSessionSearch() {
235+ sessionSearch .value = ' '
236+ const list = sessions .value
237+ const activeIdx = list .findIndex (s => s .id === activeSessionId .value )
238+ sessionHighlightIndex .value = activeIdx >= 0 ? activeIdx : 0
239+ }
240+
241+ function onSessionSearchInput() {
242+ sessionHighlightIndex .value = 0
243+ }
244+
245+ function moveSessionHighlight(delta : number ) {
246+ const len = filteredSessions .value .length
247+ if (len === 0 ) return
248+ const next = (sessionHighlightIndex .value + delta + len ) % len
249+ sessionHighlightIndex .value = next
250+ nextTick (() => {
251+ const id = filteredSessions .value [next ]?.id
252+ if (! id ) return
253+ const el = document .querySelector (` .chatui-session-popover [data-session-id="${id }"] ` )
254+ el ?.scrollIntoView ({ block: ' nearest' })
255+ })
256+ }
257+
258+ function onSessionSearchKeydown(e : KeyboardEvent ) {
259+ if (e .key === ' ArrowDown' ) {
260+ e .preventDefault ()
261+ moveSessionHighlight (1 )
262+ } else if (e .key === ' ArrowUp' ) {
263+ e .preventDefault ()
264+ moveSessionHighlight (- 1 )
265+ } else if (e .key === ' Enter' ) {
266+ e .preventDefault ()
267+ const id = highlightedSessionId .value
268+ if (id ) {
269+ selectSession (id )
270+ sidebarOpen .value = false
271+ }
272+ } else if (e .key === ' Escape' ) {
273+ e .preventDefault ()
274+ sidebarOpen .value = false
275+ }
206276}
207277
208278function toggleSettings() {
@@ -487,7 +557,10 @@ async function onRefresh() {
487557
488558async function onClearHistory() {
489559 const id = activeSessionId .value
490- if (! id || ! window .confirm (L .value .confirmClearHistory )) return
560+ if (! id || ! await confirm (L .value .confirmClearHistory , {
561+ danger: true ,
562+ cancelText: L .value .cancel ,
563+ })) return
491564 try {
492565 await props .transport .clearHistory (id )
493566 messages .value = []
@@ -564,11 +637,34 @@ onBeforeUnmount(() => {
564637 <Transition name="chatui-drawer">
565638 <div v-if =" sidebarOpen" class =" chatui-compact-popover-backdrop" @click =" sidebarOpen = false" >
566639 <div class =" chatui-compact-popover chatui-session-popover" @click.stop >
640+ <div class =" chatui-session-popover-search" >
641+ <svg class =" chatui-session-popover-search-icon" width =" 14" height =" 14" viewBox =" 0 0 16 16" fill =" none" stroke =" currentColor" stroke-width =" 1.5" stroke-linecap =" round" >
642+ <circle cx =" 7" cy =" 7" r =" 4.5" />
643+ <path d =" m10.5 10.5 3 3" />
644+ </svg >
645+ <input
646+ ref =" sessionSearchInputEl"
647+ v-model =" sessionSearch"
648+ type =" text"
649+ class =" chatui-session-popover-search-input"
650+ :placeholder =" L.sessionSearchPlaceholder"
651+ @input =" onSessionSearchInput"
652+ @keydown =" onSessionSearchKeydown"
653+ />
654+ <button
655+ v-if =" sessionSearch"
656+ class =" chatui-session-popover-search-clear"
657+ :title =" L.cancel"
658+ @click =" sessionSearch = ''; onSessionSearchInput(); sessionSearchInputEl?.focus()"
659+ >×</button >
660+ </div >
567661 <SessionBar
568- :sessions =" sessions "
662+ :sessions =" filteredSessions "
569663 :active-session-id =" activeSessionId "
664+ :highlighted-session-id =" highlightedSessionId "
570665 :labels =" labels "
571666 :show-header =" false "
667+ :empty-message =" sessionSearch ? L .sessionNoMatch : undefined "
572668 @select =" (id : string ) => { selectSession (id ); sidebarOpen = false } "
573669 @delete =" onDeleteSession "
574670 @rename =" onRenameSession "
@@ -953,10 +1049,61 @@ onBeforeUnmount(() => {
9531049 box-shadow : 0 12px 32px rgba (0 , 0 , 0 , 0.28 );
9541050 overflow : hidden ;
9551051}
1052+ .chatui-session-popover {
1053+ display : flex ;
1054+ flex-direction : column ;
1055+ }
9561056.chatui-session-popover :deep(.chatui-session-bar ) {
9571057 width : 100% ;
958- max-height : min (320px , calc (100vh - 80 px ));
1058+ max-height : min (320px , calc (100vh - 140 px ));
9591059 border-right : none ;
1060+ flex : 1 ;
1061+ min-height : 0 ;
1062+ }
1063+ .chatui-session-popover-search {
1064+ display : flex ;
1065+ align-items : center ;
1066+ gap : 6px ;
1067+ padding : 6px 8px ;
1068+ border-bottom : 1px solid var (--chatui-border-subtle , var (--chatui-border ));
1069+ background : var (--chatui-bg-surface );
1070+ flex-shrink : 0 ;
1071+ }
1072+ .chatui-session-popover-search-icon {
1073+ flex-shrink : 0 ;
1074+ color : var (--chatui-fg-secondary );
1075+ }
1076+ .chatui-session-popover-search-input {
1077+ flex : 1 ;
1078+ min-width : 0 ;
1079+ border : none ;
1080+ outline : none ;
1081+ background : transparent ;
1082+ font : inherit ;
1083+ color : var (--chatui-fg );
1084+ padding : 4px 0 ;
1085+ }
1086+ .chatui-session-popover-search-input ::placeholder {
1087+ color : var (--chatui-fg-secondary );
1088+ }
1089+ .chatui-session-popover-search-clear {
1090+ width : 20px ;
1091+ height : 20px ;
1092+ display : inline-flex ;
1093+ align-items : center ;
1094+ justify-content : center ;
1095+ border : none ;
1096+ border-radius : 4px ;
1097+ background : transparent ;
1098+ color : var (--chatui-fg-secondary );
1099+ font-size : 16px ;
1100+ line-height : 1 ;
1101+ cursor : pointer ;
1102+ flex-shrink : 0 ;
1103+ }
1104+ .chatui-session-popover-search-clear :hover {
1105+ background : var (--chatui-bg-hover );
1106+ color : var (--chatui-fg );
9601107}
9611108.chatui-settings-popover {
9621109 display : flex ;
0 commit comments