Skip to content

Commit 9af9537

Browse files
committed
Improve chat list
1 parent 7ffad6c commit 9af9537

12 files changed

Lines changed: 708 additions & 21 deletions

src/bridge/api.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,42 @@ export class EcaRemoteApi {
174174
});
175175
}
176176

177+
// ---------------------------------------------------------------------------
178+
// Trust
179+
// ---------------------------------------------------------------------------
180+
181+
/** Set the trust mode on the server. */
182+
async setTrust(trust: boolean): Promise<void> {
183+
return this.request('/trust', {
184+
method: 'POST',
185+
body: { trust },
186+
});
187+
}
188+
189+
// ---------------------------------------------------------------------------
190+
// MCP operations
191+
// ---------------------------------------------------------------------------
192+
193+
/** Start an MCP server by name. */
194+
async mcpStartServer(name: string): Promise<void> {
195+
return this.request(`/mcp/${encodeURIComponent(name)}/start`, { method: 'POST' });
196+
}
197+
198+
/** Stop an MCP server by name. */
199+
async mcpStopServer(name: string): Promise<void> {
200+
return this.request(`/mcp/${encodeURIComponent(name)}/stop`, { method: 'POST' });
201+
}
202+
203+
/** Connect (reconnect) an MCP server by name. */
204+
async mcpConnectServer(name: string): Promise<void> {
205+
return this.request(`/mcp/${encodeURIComponent(name)}/connect`, { method: 'POST' });
206+
}
207+
208+
/** Logout an MCP server by name. */
209+
async mcpLogoutServer(name: string): Promise<void> {
210+
return this.request(`/mcp/${encodeURIComponent(name)}/logout`, { method: 'POST' });
211+
}
212+
177213
// ---------------------------------------------------------------------------
178214
// SSE
179215
// ---------------------------------------------------------------------------

src/bridge/outbound-handler.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ export async function handleOutbound(
110110
dispatch('editor/saveClipboardImage', { requestId: data.requestId, path: null });
111111
break;
112112

113+
case 'editor/toggleSidebar':
114+
window.dispatchEvent(new CustomEvent('eca-toggle-sidebar'));
115+
break;
116+
113117
// --- Query operations (not available via REST — return empty) ---
114118
case 'chat/queryContext':
115119
dispatch('chat/queryContext', { chatId: data.chatId, contexts: [] });
@@ -123,16 +127,31 @@ export async function handleOutbound(
123127
dispatch('chat/queryFiles', { chatId: data.chatId, files: [] });
124128
break;
125129

126-
// --- MCP operations (not available in web) ---
130+
// --- Trust mode ---
131+
case 'server/setTrust':
132+
await api.setTrust(data);
133+
break;
134+
135+
// --- MCP operations ---
127136
case 'mcp/startServer':
137+
await api.mcpStartServer(data.name);
138+
break;
139+
128140
case 'mcp/stopServer':
141+
await api.mcpStopServer(data.name);
142+
break;
143+
129144
case 'mcp/connectServer':
145+
await api.mcpConnectServer(data.name);
146+
break;
147+
130148
case 'mcp/logoutServer':
131-
console.log(`[outbound] MCP operation not available in web: ${type}`);
149+
await api.mcpLogoutServer(data.name);
132150
break;
133151

134152
case 'mcp/updateServer':
135153
// Respond immediately to prevent webviewSendAndGet from hanging
154+
// (no REST endpoint for updating MCP server config in web context)
136155
console.log(`[outbound] MCP update not available in web`);
137156
if (data.requestId) {
138157
dispatch('editor/readInput', { requestId: data.requestId, value: null });

src/bridge/transport.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ import { chatToRestoreEvents } from './chat-restore';
2222
import { handleOutbound, type OutboundContext } from './outbound-handler';
2323
import { SSEClient, type SSEEvent } from './sse';
2424
import type {
25+
ChatEntry,
26+
ChatListChangeCallback,
2527
MCPServerUpdatedParams,
2628
RemoteChat,
2729
SessionConfig,
2830
SessionState,
2931
SSEChatStatusPayload,
3032
SSESessionConnectedPayload,
3133
SSESessionMessagePayload,
34+
SSETrustUpdatedPayload,
3235
} from './types';
3336

3437
/** Timeout for the initial SSE handshake (session:connected). */
@@ -43,6 +46,15 @@ export class WebBridge {
4346
private outboundListener: ((e: Event) => void) | null = null;
4447
private mcpServers: MCPServerUpdatedParams[] = [];
4548

49+
/**
50+
* Lightweight chat index exposed to the React shell for the sidebar.
51+
* Kept in sync as chat events flow through the bridge.
52+
*/
53+
private chatEntries: ChatEntry[] = [];
54+
55+
/** Callback invoked whenever the chat list or selection changes. */
56+
private onChatListChange: ChatListChangeCallback | null = null;
57+
4658
/**
4759
* True after `disconnect()` has been called. Checked after every async
4860
* boundary in `connect()` so that orphaned bridges (e.g. from React
@@ -101,6 +113,48 @@ export class WebBridge {
101113
return this.connected;
102114
}
103115

116+
// ---------------------------------------------------------------------------
117+
// Chat list API (for the sidebar)
118+
// ---------------------------------------------------------------------------
119+
120+
/** Register a listener for chat list changes (used by the sidebar). */
121+
onChatListChanged(cb: ChatListChangeCallback): void {
122+
this.onChatListChange = cb;
123+
// Immediately fire with current state
124+
cb([...this.chatEntries], this.currentChatId);
125+
}
126+
127+
/** Get the current chat entries snapshot. */
128+
getChatEntries(): ChatEntry[] {
129+
return [...this.chatEntries];
130+
}
131+
132+
/** Get the currently selected chat ID. */
133+
getSelectedChatId(): string | null {
134+
return this.currentChatId;
135+
}
136+
137+
/** Select a chat by ID — dispatches to the webview and updates tracking. */
138+
selectChat(chatId: string): void {
139+
this.currentChatId = chatId;
140+
this.dispatch('chat/selectChat', chatId);
141+
this.notifyChatListChange();
142+
}
143+
144+
/** Create a new chat — dispatches to the webview. */
145+
newChat(): void {
146+
this.dispatch('chat/createNewChat', undefined);
147+
}
148+
149+
/** Delete a chat by ID — calls the REST API. */
150+
async deleteChatFromSidebar(chatId: string): Promise<void> {
151+
try {
152+
await this.api.deleteChat(chatId);
153+
} catch (err) {
154+
console.error('[Bridge] Failed to delete chat:', err);
155+
}
156+
}
157+
104158
// ---------------------------------------------------------------------------
105159
// SSE connection
106160
// ---------------------------------------------------------------------------
@@ -165,6 +219,7 @@ export class WebBridge {
165219
mcpServers: data.mcpServers,
166220
chats: data.chats,
167221
config: this.buildSessionConfig(data),
222+
trust: data.trust ?? false,
168223
};
169224
} catch (err) {
170225
console.error('[Bridge] Failed to parse session:connected', err);
@@ -185,6 +240,14 @@ export class WebBridge {
185240
switch (event.event) {
186241
case 'chat:content-received':
187242
this.dispatch('chat/contentReceived', data);
243+
// Track new chats and title updates for the sidebar
244+
if (data.chatId) {
245+
this.upsertChatEntry(data.chatId, {});
246+
if (data.content?.type === 'metadata' && data.content?.title) {
247+
this.upsertChatEntry(data.chatId, { title: data.content.title });
248+
}
249+
this.notifyChatListChange();
250+
}
188251
break;
189252

190253
case 'chat:cleared':
@@ -193,10 +256,14 @@ export class WebBridge {
193256

194257
case 'chat:deleted':
195258
this.dispatch('chat/deleted', data.chatId);
259+
this.removeChatEntry(data.chatId);
260+
this.notifyChatListChange();
196261
break;
197262

198263
case 'chat:status-changed': {
199264
const status = data as SSEChatStatusPayload;
265+
this.upsertChatEntry(status.chatId, { status: status.status });
266+
this.notifyChatListChange();
200267
if (status.status === 'idle') {
201268
this.dispatch('chat/contentReceived', {
202269
chatId: status.chatId,
@@ -227,6 +294,12 @@ export class WebBridge {
227294
break;
228295
}
229296

297+
case 'trust:updated': {
298+
const trustData = data as SSETrustUpdatedPayload;
299+
this.dispatch('server/setTrust', trustData.trust);
300+
break;
301+
}
302+
230303
case 'session:disconnecting':
231304
console.warn('[Bridge] Server shutting down:', data.reason);
232305
this.dispatch('server/statusChanged', 'Stopped');
@@ -262,6 +335,9 @@ export class WebBridge {
262335
this.dispatch('tool/serversUpdated', this.mcpServers);
263336
}
264337

338+
// Sync trust mode from server state
339+
this.dispatch('server/setTrust', this.sessionState.trust ?? false);
340+
265341
await this.restoreChats();
266342
} finally {
267343
this.restoring = false;
@@ -314,11 +390,18 @@ export class WebBridge {
314390
if (!chat?.id) continue;
315391
this.currentChatId = chat.id;
316392

393+
// Track in sidebar
394+
this.upsertChatEntry(chat.id, {
395+
title: chat.title ?? `Chat`,
396+
status: chat.status ?? 'idle',
397+
});
398+
317399
const events = chatToRestoreEvents(chat);
318400
for (const event of events) {
319401
this.dispatch('chat/contentReceived', event);
320402
}
321403
}
404+
this.notifyChatListChange();
322405
}
323406

324407
// ---------------------------------------------------------------------------
@@ -392,4 +475,35 @@ export class WebBridge {
392475
private dispatch(type: string, data: unknown): void {
393476
window.postMessage({ type, data }, '*');
394477
}
478+
479+
// ---------------------------------------------------------------------------
480+
// Chat entry tracking (for sidebar)
481+
// ---------------------------------------------------------------------------
482+
483+
/** Notify the sidebar callback of chat list changes. */
484+
private notifyChatListChange(): void {
485+
this.onChatListChange?.([...this.chatEntries], this.currentChatId);
486+
}
487+
488+
/** Upsert a chat entry by ID (creates if missing, updates if present). */
489+
private upsertChatEntry(id: string, partial: Partial<Omit<ChatEntry, 'id'>>): void {
490+
const idx = this.chatEntries.findIndex((e) => e.id === id);
491+
if (idx >= 0) {
492+
this.chatEntries = [
493+
...this.chatEntries.slice(0, idx),
494+
{ ...this.chatEntries[idx], ...partial },
495+
...this.chatEntries.slice(idx + 1),
496+
];
497+
} else {
498+
this.chatEntries = [
499+
...this.chatEntries,
500+
{ id, title: partial.title ?? `Chat`, status: partial.status ?? 'idle' },
501+
];
502+
}
503+
}
504+
505+
/** Remove a chat entry by ID. */
506+
private removeChatEntry(id: string): void {
507+
this.chatEntries = this.chatEntries.filter((e) => e.id !== id);
508+
}
395509
}

src/bridge/types.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ export type SSEEventType =
161161
| 'chat:deleted'
162162
| 'chat:status-changed'
163163
| 'config:updated'
164-
| 'tool:server-updated';
164+
| 'tool:server-updated'
165+
| 'trust:updated';
165166

166167
export interface SSESessionConnectedPayload {
167168
workspaceFolders?: WorkspaceFolder[];
@@ -174,6 +175,11 @@ export interface SSESessionConnectedPayload {
174175
selectAgent?: string;
175176
variants?: string[];
176177
selectedVariant?: string | null;
178+
trust?: boolean;
179+
}
180+
181+
export interface SSETrustUpdatedPayload {
182+
trust: boolean;
177183
}
178184

179185
export interface SSEChatStatusPayload {
@@ -197,6 +203,7 @@ export interface SessionState {
197203
mcpServers?: MCPServerUpdatedParams[];
198204
chats?: RemoteChat[];
199205
config?: SessionConfig;
206+
trust?: boolean;
200207
}
201208

202209
export interface SessionConfig {
@@ -246,7 +253,8 @@ export type OutboundMessage =
246253
| { type: 'mcp/stopServer'; data: { name: string } }
247254
| { type: 'mcp/connectServer'; data: { name: string } }
248255
| { type: 'mcp/logoutServer'; data: { name: string } }
249-
| { type: 'mcp/updateServer'; data: { requestId?: string } };
256+
| { type: 'mcp/updateServer'; data: { requestId?: string } }
257+
| { type: 'server/setTrust'; data: boolean };
250258

251259
export interface UserPromptData {
252260
chatId?: string;
@@ -258,6 +266,20 @@ export interface UserPromptData {
258266
contexts?: ChatContext[];
259267
}
260268

269+
// ---------------------------------------------------------------------------
270+
// Chat sidebar types (shell-level chat list for the sidebar)
271+
// ---------------------------------------------------------------------------
272+
273+
/** Lightweight chat entry exposed to the React shell for the sidebar. */
274+
export interface ChatEntry {
275+
id: string;
276+
title: string;
277+
status: 'idle' | 'running';
278+
}
279+
280+
/** Callback signature for chat list change notifications. */
281+
export type ChatListChangeCallback = (chats: ChatEntry[], selectedChatId: string | null) => void;
282+
261283
// ---------------------------------------------------------------------------
262284
// Webview dispatch types (bridge → webview via postMessage)
263285
// ---------------------------------------------------------------------------
@@ -269,6 +291,7 @@ export interface UserPromptData {
269291
export type DispatchType =
270292
| 'server/statusChanged'
271293
| 'server/setWorkspaceFolders'
294+
| 'server/setTrust'
272295
| 'config/updated'
273296
| 'tool/serversUpdated'
274297
| 'chat/contentReceived'

0 commit comments

Comments
 (0)