Skip to content

Commit d4f304e

Browse files
committed
Add per-chat close and clear-history actions to chat sidebar
1 parent 8b90303 commit d4f304e

4 files changed

Lines changed: 138 additions & 22 deletions

File tree

‎src/bridge/outbound-handler.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ export async function handleOutbound(
7272
break;
7373

7474
case 'chat/delete':
75-
// No-op in web: closing a tab should not delete the chat from the
76-
// server cache. The webview already removes it locally via resetChat.
75+
// Mirror eca-vscode: forward to the server to delete the chat
76+
// entirely. The SSE `chat:deleted` event keeps the sidebar and
77+
// message cache in sync.
78+
await api.deleteChat(data.chatId);
7779
break;
7880

7981
case 'chat/rollback':

‎src/bridge/transport.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,61 @@ export class WebBridge {
530530
this.dispatch('chat/createNewChat', undefined);
531531
}
532532

533+
/**
534+
* Clear a chat's message history while keeping the chat itself.
535+
*
536+
* Calls the REST endpoint and mirrors the `chat:cleared` SSE handler
537+
* locally for an immediate UI update — the server's SSE echo (if any)
538+
* is idempotent.
539+
*/
540+
async clearChatHistory(chatId: string): Promise<void> {
541+
try {
542+
await this.api.clearChat(chatId);
543+
} catch (err) {
544+
console.error('[Bridge] Failed to clear chat history:', err);
545+
return;
546+
}
547+
this.dispatch('chat/cleared', { chatId, messages: true });
548+
this.loadedChatIds.delete(chatId);
549+
messageCache.invalidate(this.host, chatId);
550+
}
551+
552+
/**
553+
* Close (delete) a chat on the server. Mirrors eca-vscode: the chat is
554+
* removed entirely via `DELETE /chats/:id` with no confirmation prompt.
555+
*
556+
* When the closed chat is the active one, selection advances to the most
557+
* recent remaining chat (or a fresh chat if none remain) so the webview
558+
* doesn't keep showing the deleted chat. Local cleanup mirrors the
559+
* `chat:deleted` SSE handler; the server's SSE echo (if any) is idempotent.
560+
*/
561+
async closeChat(chatId: string): Promise<void> {
562+
try {
563+
await this.api.deleteChat(chatId);
564+
} catch (err) {
565+
console.error('[Bridge] Failed to close chat:', err);
566+
return;
567+
}
568+
569+
// If the active chat is being closed, switch away first so the webview
570+
// doesn't keep rendering a chat that no longer exists.
571+
if (this.currentChatId === chatId) {
572+
const next = [...this.chatEntries].reverse().find((e) => e.id !== chatId);
573+
if (next) {
574+
await this.selectChat(next.id);
575+
} else {
576+
this.currentChatId = null;
577+
this.newChat();
578+
}
579+
}
580+
581+
this.dispatch('chat/deleted', chatId);
582+
this.removeChatEntry(chatId);
583+
this.loadedChatIds.delete(chatId);
584+
messageCache.invalidate(this.host, chatId);
585+
this.notifyChatListChange();
586+
}
587+
533588

534589

535590
// ---------------------------------------------------------------------------

‎src/components/ChatSidebar.css‎

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,27 @@
125125
.chat-sidebar-item {
126126
display: flex;
127127
align-items: center;
128-
gap: 0.5rem;
129128
width: 100%;
130-
padding: 0.5rem 0.75rem;
129+
color: #8a9ab0;
130+
transition: background 0.12s, color 0.12s;
131+
position: relative;
132+
}
133+
134+
/* Main clickable area (select chat) */
135+
.chat-sidebar-item-main {
136+
display: flex;
137+
align-items: center;
138+
gap: 0.5rem;
139+
flex: 1;
140+
min-width: 0;
141+
padding: 0.5rem 0.25rem 0.5rem 0.75rem;
131142
border: none;
132143
background: transparent;
133-
color: #8a9ab0;
144+
color: inherit;
134145
font-size: 0.8rem;
135146
font-family: inherit;
136147
text-align: left;
137148
cursor: pointer;
138-
transition: background 0.12s, color 0.12s;
139-
position: relative;
140149
}
141150

142151
.chat-sidebar-item:hover {
@@ -199,8 +208,16 @@
199208
50% { opacity: 0.3; }
200209
}
201210

202-
/* Delete button (on hover) */
203-
.chat-sidebar-delete {
211+
/* Per-chat actions (revealed on hover / active) */
212+
.chat-sidebar-item-actions {
213+
display: flex;
214+
align-items: center;
215+
gap: 0.1rem;
216+
padding-right: 0.4rem;
217+
flex-shrink: 0;
218+
}
219+
220+
.chat-sidebar-action {
204221
display: flex;
205222
align-items: center;
206223
justify-content: center;
@@ -210,19 +227,25 @@
210227
border-radius: 0.2rem;
211228
background: transparent;
212229
color: #5a6a7a;
213-
font-size: 11px;
230+
font-size: 12px;
214231
cursor: pointer;
215232
opacity: 0;
216233
flex-shrink: 0;
217234
transition: opacity 0.12s, background 0.12s, color 0.12s;
218235
}
219236

220-
.chat-sidebar-item:hover .chat-sidebar-delete {
221-
opacity: 0.7;
237+
.chat-sidebar-item:hover .chat-sidebar-action,
238+
.chat-sidebar-item.active .chat-sidebar-action {
239+
opacity: 0.65;
222240
}
223241

224-
.chat-sidebar-delete:hover {
242+
.chat-sidebar-action:hover {
225243
opacity: 1 !important;
244+
background: rgba(255, 255, 255, 0.08);
245+
color: #e0e4ea;
246+
}
247+
248+
.chat-sidebar-action-close:hover {
226249
background: rgba(241, 76, 76, 0.15);
227250
color: #f14c4c;
228251
}

‎src/components/ChatSidebar.tsx‎

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ export function ChatSidebar({ bridge, chats = [], selectedId, workspaceFolders =
6969
onMobileClose();
7070
}, [bridge, onMobileClose]);
7171

72+
const handleClearHistory = useCallback(
73+
(chatId: string) => {
74+
bridge?.clearChatHistory(chatId);
75+
},
76+
[bridge],
77+
);
78+
79+
const handleCloseChat = useCallback(
80+
(chatId: string) => {
81+
bridge?.closeChat(chatId);
82+
},
83+
[bridge],
84+
);
85+
7286

7387

7488
return (
@@ -113,18 +127,40 @@ export function ChatSidebar({ bridge, chats = [], selectedId, workspaceFolders =
113127
</div>
114128
) : (
115129
chats.map((chat) => (
116-
<button
130+
<div
117131
key={chat.id}
118132
className={`chat-sidebar-item ${chat.id === selectedId ? 'active' : ''}`}
119-
onClick={() => handleSelect(chat.id)}
120-
title={chat.title}
121133
>
122-
<i className="codicon codicon-comment chat-sidebar-icon" />
123-
<span className="chat-sidebar-label">{chat.title}</span>
124-
{chat.status === 'running' && (
125-
<span className="chat-sidebar-status running" />
126-
)}
127-
</button>
134+
<button
135+
className="chat-sidebar-item-main"
136+
onClick={() => handleSelect(chat.id)}
137+
title={chat.title}
138+
>
139+
<i className="codicon codicon-comment chat-sidebar-icon" />
140+
<span className="chat-sidebar-label">{chat.title}</span>
141+
{chat.status === 'running' && (
142+
<span className="chat-sidebar-status running" />
143+
)}
144+
</button>
145+
<div className="chat-sidebar-item-actions">
146+
<button
147+
className="chat-sidebar-action"
148+
onClick={(e) => { e.stopPropagation(); handleClearHistory(chat.id); }}
149+
title="Clear messages"
150+
aria-label="Clear chat messages"
151+
>
152+
<i className="codicon codicon-trash" />
153+
</button>
154+
<button
155+
className="chat-sidebar-action chat-sidebar-action-close"
156+
onClick={(e) => { e.stopPropagation(); handleCloseChat(chat.id); }}
157+
title="Close chat"
158+
aria-label="Close chat"
159+
>
160+
<i className="codicon codicon-close" />
161+
</button>
162+
</div>
163+
</div>
128164
))
129165
)}
130166
</div>

0 commit comments

Comments
 (0)