Skip to content

Commit 4d570f8

Browse files
author
linyuan.yang
committed
up
1 parent 877ebc5 commit 4d570f8

11 files changed

Lines changed: 189 additions & 12 deletions

File tree

0 Bytes
Binary file not shown.

packages/chat-ui/src/components/ChatView.vue

Lines changed: 151 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import StatusBar from './StatusBar.vue'
1717
import ChatArea from './ChatArea.vue'
1818
import PathPickerModal from './PathPickerModal.vue'
1919
import Explorer from './Explorer.vue'
20+
import { useConfirm } from 'sbot-ui'
2021
2122
const props = withDefaults(defineProps<{
2223
transport: IChatTransport
@@ -29,6 +30,7 @@ const props = withDefaults(defineProps<{
2930
})
3031
3132
const L = computed(() => resolveLabels(props.labels))
33+
const { confirm } = useConfirm()
3234
3335
// ── Core state ──
3436
@@ -64,6 +66,9 @@ const explorerWidth = ref(420)
6466
const explorerResizing = ref(false)
6567
const sessionBarWidth = ref(180)
6668
const 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
9095
const 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+
92114
const archivedCount = computed(() => messages.value.filter(m => m.kind === MessageKind.Archive).length)
93115
const 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
203225
function 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
208278
function toggleSettings() {
@@ -487,7 +557,10 @@ async function onRefresh() {
487557
488558
async 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 - 80px));
1058+
max-height: min(320px, calc(100vh - 140px));
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;

packages/chat-ui/src/components/SessionBar.vue

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
<script setup lang="ts">
22
import { ref, computed, nextTick } from 'vue'
3-
import { SButton, SInput } from 'sbot-ui'
3+
import { SButton, SInput, useConfirm } from 'sbot-ui'
44
import type { SessionItem, ChatLabels } from '../types'
55
import { resolveLabels, tpl } from '../labels'
66
77
const props = withDefaults(defineProps<{
88
sessions: SessionItem[]
99
activeSessionId: string | null
10+
highlightedSessionId?: string | null
1011
labels?: ChatLabels
1112
showHeader?: boolean
1213
width?: number
14+
emptyMessage?: string
1315
}>(), {
1416
showHeader: true,
17+
highlightedSessionId: null,
1518
})
1619
1720
const barStyle = computed(() =>
@@ -26,6 +29,7 @@ const emit = defineEmits<{
2629
}>()
2730
2831
const L = computed(() => resolveLabels(props.labels))
32+
const { confirm } = useConfirm()
2933
3034
const editingId = ref<string | null>(null)
3135
const editingName = ref('')
@@ -48,10 +52,13 @@ function commitEdit() {
4852
if (val) emit('rename', id, val)
4953
}
5054
51-
function onDelete(id: string) {
55+
async function onDelete(id: string) {
5256
const s = props.sessions.find(s => s.id === id)
5357
const label = s?.name || L.value.untitledSession
54-
if (window.confirm(tpl(L.value.confirmDeleteSession, { name: label }))) {
58+
if (await confirm(tpl(L.value.confirmDeleteSession, { name: label }), {
59+
danger: true,
60+
cancelText: L.value.cancel,
61+
})) {
5562
emit('delete', id)
5663
}
5764
}
@@ -66,7 +73,8 @@ function onDelete(id: string) {
6673
<div
6774
v-for="s in sessions" :key="s.id"
6875
class="chatui-session-item"
69-
:class="{ active: activeSessionId === s.id }"
76+
:class="{ active: activeSessionId === s.id, highlighted: highlightedSessionId === s.id }"
77+
:data-session-id="s.id"
7078
@click="emit('select', s.id)"
7179
>
7280
<div style="display:flex;align-items:center;gap:4px">
@@ -94,7 +102,8 @@ function onDelete(id: string) {
94102
</div>
95103
</div>
96104
<div v-if="sessions.length === 0" class="chatui-session-empty">
97-
{{ L.emptySession }}<br>{{ L.createSessionHint }}
105+
<template v-if="emptyMessage">{{ emptyMessage }}</template>
106+
<template v-else>{{ L.emptySession }}<br>{{ L.createSessionHint }}</template>
98107
</div>
99108
</div>
100109
</div>
@@ -116,6 +125,11 @@ function onDelete(id: string) {
116125
}
117126
.chatui-session-item:hover { background: var(--chatui-bg-hover); }
118127
.chatui-session-item.active { background: var(--chatui-bg-active); }
128+
.chatui-session-item.highlighted {
129+
background: var(--chatui-bg-hover);
130+
box-shadow: inset 0 0 0 1px var(--chatui-border-focus, var(--chatui-accent));
131+
}
132+
.chatui-session-item.active.highlighted { background: var(--chatui-bg-active); }
119133
.chatui-session-item-name {
120134
font-size: 13px; font-weight: 500; color: var(--chatui-fg);
121135
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;

packages/chat-ui/src/labels.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export const defaultLabels: Required<ChatLabels> = {
4444
emptySession: '暂无会话',
4545
createSessionHint: '点击上方新建',
4646
editSessionNameHint: '双击编辑名称',
47+
sessionSearchPlaceholder: '搜索会话…',
48+
sessionNoMatch: '没有匹配的会话',
4749
agent: 'Agent',
4850
storage: '存储',
4951
workpath: '工作目录',

packages/chat-ui/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export interface ChatLabels {
144144
emptySession?: string
145145
createSessionHint?: string
146146
editSessionNameHint?: string
147+
sessionSearchPlaceholder?: string
148+
sessionNoMatch?: string
147149
agent?: string
148150
storage?: string
149151
workpath?: string

packages/pwa/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"dependencies": {
1212
"@sbot/chat-ui": "workspace:*",
13+
"sbot-ui": "workspace:*",
1314
"sbot.commons": "workspace:*"
1415
},
1516
"devDependencies": {

packages/pwa/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { ref, computed } from 'vue'
33
import { ChatView, ServerPicker, WebSocketTransport } from '@sbot/chat-ui'
44
import type { RemoteEntry } from '@sbot/chat-ui'
5+
import { SConfirm } from 'sbot-ui'
56
import '@sbot/chat-ui/themes/variables.css'
67
import '@sbot/chat-ui/themes/theme-dark.css'
78
import '@sbot/chat-ui/themes/theme-pwa.css'
@@ -89,6 +90,7 @@ function removeRemote(index: number) {
8990
<ChatView :transport="transport" :show-attachments="true" />
9091
</template>
9192
</div>
93+
<SConfirm default-confirm-text="确定" default-cancel-text="取消" />
9294
</template>
9395

9496
<style scoped>

packages/vscode-extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"dependencies": {
5555
"@sbot/chat-ui": "workspace:*",
5656
"axios": "catalog:",
57+
"sbot-ui": "workspace:*",
5758
"sbot.commons": "workspace:*",
5859
"ws": "catalog:"
5960
},

packages/vscode-extension/src/SbotClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ export class SbotClient {
8181
}
8282

8383
async deleteSession(sessionId: string): Promise<void> {
84-
await this.http.delete(`/api/settings/sessions/${encodeURIComponent(sessionId)}`);
8584
await this.http.delete(`/api/sessions/${encodeURIComponent(sessionId)}/history`).catch(() => {});
85+
await this.http.delete(`/api/settings/sessions/${encodeURIComponent(sessionId)}`);
8686
}
8787

8888
async updateSession(sessionId: string, patch: Record<string, any>): Promise<void> {

packages/vscode-extension/webview/App.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { ref, onMounted } from 'vue'
33
import { ChatView, ServerPicker } from '@sbot/chat-ui'
44
import type { RemoteEntry } from '@sbot/chat-ui'
5+
import { SConfirm } from 'sbot-ui'
56
import '@sbot/chat-ui/themes/variables.css'
67
import '@sbot/chat-ui/themes/theme-vscode.css'
78
import '@sbot/chat-ui/themes/sbot-ui-bridge.css'
@@ -15,8 +16,7 @@ const currentBaseUrl = ref('')
1516
1617
onMounted(async () => {
1718
remotes.value = await transport.getRemotes()
18-
const last = await transport.getLastServer()
19-
if (last?.url) selectServer(last.url, !!last.local)
19+
selectLocal()
2020
})
2121
2222
const connectError = ref('')
@@ -29,6 +29,7 @@ async function selectServer(baseUrl: string, local = false) {
2929
phase.value = 'chat'
3030
} catch (e: any) {
3131
connectError.value = `无法连接服务器 ${baseUrl}`
32+
phase.value = 'server-pick'
3233
}
3334
}
3435
@@ -86,6 +87,7 @@ async function removeRemote(index: number) {
8687
<ChatView :transport="transport" always-compact />
8788
</template>
8889
</div>
90+
<SConfirm default-confirm-text="确定" default-cancel-text="取消" />
8991
</template>
9092

9193
<style>

0 commit comments

Comments
 (0)