feat(web): workspace components — ComparePane, Memory page, McpDialog, error boundaries, message-parts

- Add ComparePane.tsx: side-by-side AI response comparison
- Add Memory.tsx: memory management page with CRUD UI
- Add McpPermissionDialog.tsx: MCP tool permission approval dialog
- Add McpResponseDisplay.tsx: MCP response visualization
- Add MessageBoundary.tsx + MessageListErrorBoundary.tsx: error resilience
- Add EmptyState.tsx: contextual empty state component
- Add KeyboardShortcutsDialog.tsx: keyboard shortcut reference
- Add message-parts/: ActionRow, CompactCard, MistakeRecoverySentinel, ReasoningBlock, SendToTerminalMenu, StatsLine, SummaryCard
- Add useDraftPersistence.ts: draft message persistence hook
- Add useTerminals.ts: terminal session management hook
- Add keyboard-shortcuts.ts + tool-utils.ts: shared utilities
- Extend components: ChatInput, MessageBubble, MessageList, Workspace, panes
- Extend hooks: useTerminalSocket, useSessionStream test suite
- Update pages: Home, Project — workspace layout and session flow
This commit is contained in:
2026-06-08 03:49:22 +00:00
parent 51733c1338
commit 50de80ee75
51 changed files with 3352 additions and 96 deletions

View File

@@ -275,7 +275,7 @@ export const api = {
method: 'POST',
body: JSON.stringify(body ?? {}),
}),
update: (chatId: string, body: { name: string }) =>
update: (chatId: string, body: { name?: string; model?: string }) =>
request<Chat>(`/api/chats/${chatId}`, {
method: 'PATCH',
body: JSON.stringify(body),
@@ -331,6 +331,17 @@ export const api = {
method: 'POST',
body: JSON.stringify({ skill_name: skillName, user_message: userMessage }),
}),
// v2.8-compare: send the same message to N models and stream back
// parallel responses. Returns compare_group_id + per-model message ids.
compare: (chatId: string, message: string, models: string[]) =>
request<{
compare_group_id: string;
user_message_id: string;
responses: Array<{ model: string; assistant_message_id: string }>;
}>(`/api/chats/${chatId}/compare`, {
method: 'POST',
body: JSON.stringify({ message, models }),
}),
// v1.13.17-cross-repo-reads: resume a paused request_read_access. On
// 'allow' the server re-resolves the grant root and appends it to
// sessions.allowed_read_paths; the returned list reflects the post-
@@ -348,6 +359,14 @@ export const api = {
request<ToolTraceResponse>(
`/api/chats/${chatId}/traces?limit=${limit}&offset=${offset}`,
),
exportChat: (chatId: string, format: 'json' | 'markdown') =>
request<string>(`/api/chats/${chatId}/export?format=${format}`),
// MCP permission: approve/deny a tool call from an 'ask' state server.
mcpApprove: (chatId: string, toolCallId: string, permission: 'allow_once' | 'allow_always' | 'deny') =>
request<{ ok: true }>(`/api/chats/${chatId}/mcp-approve`, {
method: 'POST',
body: JSON.stringify({ tool_call_id: toolCallId, permission }),
}),
},
messages: {
@@ -388,6 +407,11 @@ export const api = {
request<{ html_content: string; char_count: number; title: string }>(
`/api/chats/${chatId}/messages/${messageId}/html_artifact`,
),
feedback: (chatId: string, messageId: string, value: 'up' | 'down') =>
request<{ ok: boolean }>(
`/api/chats/${chatId}/messages/${messageId}/feedback`,
{ method: 'POST', body: JSON.stringify({ value }) },
),
},
models: () => request<ModelInfo[]>('/api/models'),
@@ -654,17 +678,27 @@ export const api = {
// cols/rows are optional. When passed, booterm sizes the per-pane tmux
// session at creation time so the inner bash (and any TUI it spawns) is
// born with the correct PTY dimensions instead of tmux's 80x24 default.
start: (sessionId: string, paneId: string, cols?: number, rows?: number) =>
request<{ tmux_session: string }>(
start: (
sessionId: string,
paneId: string,
cols?: number,
rows?: number,
description?: string,
parentAgent?: string,
) => {
const body: Record<string, unknown> = {};
if (cols !== undefined) body.cols = cols;
if (rows !== undefined) body.rows = rows;
if (description !== undefined) body.description = description;
if (parentAgent !== undefined) body.parentAgent = parentAgent;
return request<{ tmux_session: string }>(
`/api/term/sessions/${sessionId}/panes/${paneId}/start`,
{
method: 'POST',
body:
cols !== undefined && rows !== undefined
? JSON.stringify({ cols, rows })
: undefined,
body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
},
),
);
},
kill: (sessionId: string, paneId: string) =>
request<{ ok: true }>(
`/api/term/sessions/${sessionId}/panes/${paneId}/kill`,