Skip to content

Commit 765e2a7

Browse files
authored
feat: context menu (#1347)
1 parent f9adbcb commit 765e2a7

5 files changed

Lines changed: 528 additions & 52 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Sidebar Session Context Menu
2+
3+
## Overview
4+
5+
Add a session context menu to the left sidebar list so users can right-click any session item and access the same core session actions already available from the chat top bar.
6+
7+
## User Story
8+
9+
As a user browsing the left session list, I want to right-click a session and manage it in place, so I do not need to open the session first just to pin, rename, clear, or delete it.
10+
11+
## Acceptance Criteria
12+
13+
1. Right-clicking a pinned or unpinned session item in `WindowSideBar` opens a context menu.
14+
2. The context menu includes:
15+
- `Pin` or `Unpin` depending on current state
16+
- `Rename`
17+
- `Clear Messages`
18+
- `Delete`
19+
3. Left-clicking a session item still activates that session exactly as before.
20+
4. Selecting `Rename`, `Clear Messages`, or `Delete` opens the existing confirmation/input dialog flow from the sidebar.
21+
5. Selecting `Pin` or `Unpin` updates the session pinned state through the existing session store action.
22+
6. Pinned sessions remain rendered in the pinned section, and unpinned sessions remain rendered in grouped sections after actions complete.
23+
7. All user-facing labels continue to come from existing i18n keys; no hard-coded menu text is introduced.
24+
25+
## Non-Goals
26+
27+
- Replacing the existing top bar session action menu
28+
- Adding new session actions beyond the current top bar set
29+
- Changing session grouping, sorting, or persistence behavior

src/renderer/src/components/WindowSideBar.vue

Lines changed: 185 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -128,29 +128,17 @@
128128
<!-- Session list -->
129129
<div class="flex-1 overflow-y-auto px-1.5">
130130
<div v-if="pinnedSessions.length > 0" class="pt-2 space-y-0.5">
131-
<button
131+
<WindowSideBarSessionItem
132132
v-for="session in pinnedSessions"
133133
:key="`pinned-${session.id}`"
134-
class="flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-left transition-all duration-150"
135-
:class="
136-
sessionStore.activeSessionId === session.id
137-
? 'bg-accent text-accent-foreground'
138-
: 'text-foreground/80 hover:bg-accent/50'
139-
"
140-
@click="handleSessionClick(session)"
141-
>
142-
<Icon icon="lucide:pin" class="w-3.5 h-3.5 text-yellow-500 shrink-0" />
143-
<span class="flex-1 text-sm truncate">{{ session.title }}</span>
144-
<span v-if="session.status === 'working'" class="shrink-0">
145-
<Icon icon="lucide:loader-2" class="w-3.5 h-3.5 text-primary animate-spin" />
146-
</span>
147-
<span v-else-if="session.status === 'completed'" class="shrink-0">
148-
<Icon icon="lucide:check" class="w-3.5 h-3.5 text-green-500" />
149-
</span>
150-
<span v-else-if="session.status === 'error'" class="shrink-0">
151-
<Icon icon="lucide:alert-circle" class="w-3.5 h-3.5 text-destructive" />
152-
</span>
153-
</button>
134+
:session="session"
135+
:active="sessionStore.activeSessionId === session.id"
136+
@select="handleSessionClick"
137+
@toggle-pin="handleTogglePin"
138+
@rename="openRenameDialog"
139+
@clear="openClearDialog"
140+
@delete="openDeleteDialog"
141+
/>
154142
</div>
155143

156144
<!-- Empty state -->
@@ -171,50 +159,97 @@
171159
group.labelKey ? t(group.labelKey) : group.label
172160
}}</span>
173161
</div>
174-
<button
162+
<WindowSideBarSessionItem
175163
v-for="session in group.sessions"
176164
:key="session.id"
177-
class="flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-left transition-all duration-150"
178-
:class="
179-
sessionStore.activeSessionId === session.id
180-
? 'bg-accent text-accent-foreground'
181-
: 'text-foreground/80 hover:bg-accent/50'
182-
"
183-
@click="handleSessionClick(session)"
184-
>
185-
<span class="flex-1 text-sm truncate">{{ session.title }}</span>
186-
<span v-if="session.status === 'working'" class="shrink-0">
187-
<Icon icon="lucide:loader-2" class="w-3.5 h-3.5 text-primary animate-spin" />
188-
</span>
189-
<span v-else-if="session.status === 'completed'" class="shrink-0">
190-
<Icon icon="lucide:check" class="w-3.5 h-3.5 text-green-500" />
191-
</span>
192-
<span v-else-if="session.status === 'error'" class="shrink-0">
193-
<Icon icon="lucide:alert-circle" class="w-3.5 h-3.5 text-destructive" />
194-
</span>
195-
</button>
165+
:session="session"
166+
:active="sessionStore.activeSessionId === session.id"
167+
@select="handleSessionClick"
168+
@toggle-pin="handleTogglePin"
169+
@rename="openRenameDialog"
170+
@clear="openClearDialog"
171+
@delete="openDeleteDialog"
172+
/>
196173
</template>
197174
</div>
198175
</div>
199176
</div>
200177
</TooltipProvider>
178+
179+
<Dialog v-model:open="renameDialogOpen">
180+
<DialogContent>
181+
<DialogHeader>
182+
<DialogTitle>{{ t('dialog.rename.title') }}</DialogTitle>
183+
<DialogDescription>{{ t('dialog.rename.description') }}</DialogDescription>
184+
</DialogHeader>
185+
<Input v-model="renameValue" />
186+
<DialogFooter>
187+
<Button variant="outline" @click="renameDialogOpen = false">{{
188+
t('dialog.cancel')
189+
}}</Button>
190+
<Button variant="default" @click="handleRenameConfirm">{{ t('dialog.confirm') }}</Button>
191+
</DialogFooter>
192+
</DialogContent>
193+
</Dialog>
194+
195+
<Dialog v-model:open="clearDialogOpen">
196+
<DialogContent>
197+
<DialogHeader>
198+
<DialogTitle>{{ t('dialog.cleanMessages.title') }}</DialogTitle>
199+
<DialogDescription>{{ t('dialog.cleanMessages.description') }}</DialogDescription>
200+
</DialogHeader>
201+
<DialogFooter>
202+
<Button variant="outline" @click="clearDialogOpen = false">{{ t('dialog.cancel') }}</Button>
203+
<Button variant="destructive" @click="handleClearConfirm">{{
204+
t('dialog.cleanMessages.confirm')
205+
}}</Button>
206+
</DialogFooter>
207+
</DialogContent>
208+
</Dialog>
209+
210+
<Dialog v-model:open="deleteDialogOpen">
211+
<DialogContent>
212+
<DialogHeader>
213+
<DialogTitle>{{ t('dialog.delete.title') }}</DialogTitle>
214+
<DialogDescription>{{ t('dialog.delete.description') }}</DialogDescription>
215+
</DialogHeader>
216+
<DialogFooter>
217+
<Button variant="outline" @click="deleteDialogOpen = false">{{
218+
t('dialog.cancel')
219+
}}</Button>
220+
<Button variant="destructive" @click="handleDeleteConfirm">{{
221+
t('dialog.delete.confirm')
222+
}}</Button>
223+
</DialogFooter>
224+
</DialogContent>
225+
</Dialog>
201226
</template>
202227

203228
<script setup lang="ts">
204229
import { ref, computed } from 'vue'
205230
import { Icon } from '@iconify/vue'
231+
import { Input } from '@shadcn/components/ui/input'
206232
import {
207233
Tooltip,
208234
TooltipContent,
209235
TooltipProvider,
210236
TooltipTrigger
211237
} from '@shadcn/components/ui/tooltip'
212238
import { Button } from '@shadcn/components/ui/button'
239+
import {
240+
Dialog,
241+
DialogContent,
242+
DialogDescription,
243+
DialogFooter,
244+
DialogHeader,
245+
DialogTitle
246+
} from '@shadcn/components/ui/dialog'
213247
import { usePresenter } from '@/composables/usePresenter'
214248
import { useThemeStore } from '@/stores/theme'
215249
import { useAgentStore } from '@/stores/ui/agent'
216-
import { useSessionStore } from '@/stores/ui/session'
250+
import { useSessionStore, type UISession } from '@/stores/ui/session'
217251
import ModelIcon from './icons/ModelIcon.vue'
252+
import WindowSideBarSessionItem from './WindowSideBarSessionItem.vue'
218253
import { useI18n } from 'vue-i18n'
219254
220255
const windowPresenter = usePresenter('windowPresenter')
@@ -232,6 +267,37 @@ const selectedAgentName = computed(
232267
233268
const pinnedSessions = computed(() => sessionStore.getPinnedSessions(agentStore.selectedAgentId))
234269
const filteredGroups = computed(() => sessionStore.getFilteredGroups(agentStore.selectedAgentId))
270+
const renameTargetSession = ref<UISession | null>(null)
271+
const clearTargetSession = ref<UISession | null>(null)
272+
const deleteTargetSession = ref<UISession | null>(null)
273+
const renameValue = ref('')
274+
275+
const renameDialogOpen = computed({
276+
get: () => renameTargetSession.value !== null,
277+
set: (open: boolean) => {
278+
if (!open) {
279+
renameTargetSession.value = null
280+
}
281+
}
282+
})
283+
284+
const clearDialogOpen = computed({
285+
get: () => clearTargetSession.value !== null,
286+
set: (open: boolean) => {
287+
if (!open) {
288+
clearTargetSession.value = null
289+
}
290+
}
291+
})
292+
293+
const deleteDialogOpen = computed({
294+
get: () => deleteTargetSession.value !== null,
295+
set: (open: boolean) => {
296+
if (!open) {
297+
deleteTargetSession.value = null
298+
}
299+
}
300+
})
235301
236302
const openSettings = () => {
237303
const windowId = window.api.getWindowId()
@@ -241,7 +307,7 @@ const openSettings = () => {
241307
}
242308
243309
const handleNewChat = () => {
244-
sessionStore.closeSession()
310+
void sessionStore.closeSession()
245311
}
246312
247313
const handleAgentSelect = async (id: string | null) => {
@@ -281,7 +347,82 @@ const handleAgentSelect = async (id: string | null) => {
281347
}
282348
283349
const handleSessionClick = (session: { id: string }) => {
284-
sessionStore.selectSession(session.id)
350+
void sessionStore.selectSession(session.id)
351+
}
352+
353+
const closeAllSessionDialogs = () => {
354+
renameTargetSession.value = null
355+
clearTargetSession.value = null
356+
deleteTargetSession.value = null
357+
}
358+
359+
const openRenameDialog = (session: UISession) => {
360+
closeAllSessionDialogs()
361+
renameValue.value = session.title
362+
renameTargetSession.value = session
363+
}
364+
365+
const openClearDialog = (session: UISession) => {
366+
closeAllSessionDialogs()
367+
clearTargetSession.value = session
368+
}
369+
370+
const openDeleteDialog = (session: UISession) => {
371+
closeAllSessionDialogs()
372+
deleteTargetSession.value = session
373+
}
374+
375+
const handleTogglePin = async (session: UISession) => {
376+
try {
377+
await sessionStore.toggleSessionPinned(session.id, !session.isPinned)
378+
} catch (error) {
379+
console.error('Failed to toggle pin status:', error)
380+
}
381+
}
382+
383+
const handleRenameConfirm = async () => {
384+
const targetSession = renameTargetSession.value
385+
if (!targetSession) {
386+
return
387+
}
388+
389+
try {
390+
await sessionStore.renameSession(targetSession.id, renameValue.value)
391+
} catch (error) {
392+
console.error(t('common.error.renameChatFailed'), error)
393+
}
394+
395+
renameTargetSession.value = null
396+
}
397+
398+
const handleClearConfirm = async () => {
399+
const targetSession = clearTargetSession.value
400+
if (!targetSession) {
401+
return
402+
}
403+
404+
try {
405+
await sessionStore.clearSessionMessages(targetSession.id)
406+
} catch (error) {
407+
console.error(t('common.error.cleanMessagesFailed'), error)
408+
}
409+
410+
clearTargetSession.value = null
411+
}
412+
413+
const handleDeleteConfirm = async () => {
414+
const targetSession = deleteTargetSession.value
415+
if (!targetSession) {
416+
return
417+
}
418+
419+
try {
420+
await sessionStore.deleteSession(targetSession.id)
421+
} catch (error) {
422+
console.error(t('common.error.deleteChatFailed'), error)
423+
}
424+
425+
deleteTargetSession.value = null
285426
}
286427
</script>
287428

0 commit comments

Comments
 (0)