tab-close + chat archive/delete + landing-card buttons + 1000px content cap
Feature 1 — Tab close menu (pure local pane state, no API):
- ChatTabBar context menu: Rename / sep / Close / Close others / Close to right / Close all
- Workspace bulk-tab primitives: closeOtherTabs, closeTabsToRight, closeAllTabs (manipulate panes[].chatIds, no fetch)
- Drop in-bar Delete; landing card's name-typed Delete is the canonical destructive path
Feature 2 — Chat archive + delete:
- chats.status vocabulary aligned with projects ('open' | 'archived'); DROP old inline CHECK, UPDATE 'closed' → 'archived', ADD new named chats_status_chk
- POST /api/chats/:id/archive (204) + POST /api/chats/:id/unarchive (200) + GET /api/sessions/:id/chats?status=archived; DELETE publishes chat_deleted; PATCH simplified to name-only
- 3 new WS frames: chat_archived, chat_unarchived, chat_deleted (renamed from chat_closed)
- Same dedup discipline: server-only publish, no local sessionEvents.emit in client
- SessionLandingPage: right-click ContextMenu (Open / Rename / Archive / sep / Delete-destructive), inline rename, archive confirm dialog, delete dialog with name-typed Input gated until typed text === chat.name, Archived chats collapsible section with Restore
- Card-level Archive + Delete icon buttons reusing the same dialog state setters; stopPropagation on both so card click still opens the chat; archived cards keep only Restore
UX — chat content width cap:
- ChatPane content (MessageList, queue chips, stop button, ChatInput) wrapped in inner max-w-[1000px] mx-auto w-full so messages center; outer border-t / scroll containers stay full-width so pane chrome and backgrounds remain edge-to-edge
- No new deps, no media queries (narrow viewports collapse to width naturally)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,7 @@ const CreateBody = z.object({
|
||||
});
|
||||
|
||||
const PatchBody = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
status: z.enum(['open', 'closed']).optional(),
|
||||
name: z.string().min(1).max(200),
|
||||
});
|
||||
|
||||
export function registerChatRoutes(
|
||||
@@ -18,7 +17,7 @@ export function registerChatRoutes(
|
||||
sql: Sql,
|
||||
broker: Broker
|
||||
): void {
|
||||
app.get<{ Params: { id: string } }>(
|
||||
app.get<{ Params: { id: string }; Querystring: { status?: string } }>(
|
||||
'/api/sessions/:id/chats',
|
||||
async (req, reply) => {
|
||||
const session = await sql`SELECT id FROM sessions WHERE id = ${req.params.id}`;
|
||||
@@ -26,10 +25,8 @@ export function registerChatRoutes(
|
||||
reply.code(404);
|
||||
return { error: 'session not found' };
|
||||
}
|
||||
const status = req.query.status === 'archived' ? 'archived' : 'open';
|
||||
// Enriched list: computed per-chat fields via LATERAL joins.
|
||||
// `effective_context_tokens` = ctx_used (prompt tokens) on the most
|
||||
// recent complete assistant message — represents the current context
|
||||
// window consumption post-compact.
|
||||
const rows = await sql<Chat[]>`
|
||||
SELECT
|
||||
c.id, c.session_id, c.name, c.status, c.created_at, c.updated_at,
|
||||
@@ -55,7 +52,7 @@ export function registerChatRoutes(
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
) ec ON TRUE
|
||||
WHERE c.session_id = ${req.params.id}
|
||||
WHERE c.session_id = ${req.params.id} AND c.status = ${status}
|
||||
ORDER BY c.updated_at DESC
|
||||
`;
|
||||
return rows;
|
||||
@@ -98,17 +95,10 @@ export function registerChatRoutes(
|
||||
reply.code(400);
|
||||
return { error: 'invalid body', details: parsed.error.flatten() };
|
||||
}
|
||||
const { name, status } = parsed.data;
|
||||
if (name === undefined && status === undefined) {
|
||||
reply.code(400);
|
||||
return { error: 'must provide name or status' };
|
||||
}
|
||||
const rows = await sql<Chat[]>`
|
||||
UPDATE chats
|
||||
SET
|
||||
name = COALESCE(${name ?? null}, name),
|
||||
status = COALESCE(${status ?? null}, status),
|
||||
updated_at = clock_timestamp()
|
||||
SET name = ${parsed.data.name},
|
||||
updated_at = clock_timestamp()
|
||||
WHERE id = ${req.params.id}
|
||||
RETURNING id, session_id, name, status, created_at, updated_at
|
||||
`;
|
||||
@@ -117,21 +107,54 @@ export function registerChatRoutes(
|
||||
return { error: 'chat not found' };
|
||||
}
|
||||
const chat = rows[0]!;
|
||||
if (status === 'closed') {
|
||||
broker.publishUser('default', {
|
||||
type: 'chat_closed',
|
||||
chat_id: chat.id,
|
||||
session_id: chat.session_id,
|
||||
});
|
||||
} else {
|
||||
broker.publishUser('default', {
|
||||
type: 'chat_updated',
|
||||
chat_id: chat.id,
|
||||
session_id: chat.session_id,
|
||||
name: chat.name,
|
||||
updated_at: chat.updated_at,
|
||||
});
|
||||
broker.publishUser('default', {
|
||||
type: 'chat_updated',
|
||||
chat_id: chat.id,
|
||||
session_id: chat.session_id,
|
||||
name: chat.name,
|
||||
updated_at: chat.updated_at,
|
||||
});
|
||||
return chat;
|
||||
}
|
||||
);
|
||||
|
||||
app.post<{ Params: { id: string } }>(
|
||||
'/api/chats/:id/archive',
|
||||
async (req, reply) => {
|
||||
const rows = await sql<{ id: string; session_id: string }[]>`
|
||||
UPDATE chats SET status = 'archived', updated_at = clock_timestamp()
|
||||
WHERE id = ${req.params.id} AND status = 'open'
|
||||
RETURNING id, session_id
|
||||
`;
|
||||
if (rows.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'chat not found or already archived' };
|
||||
}
|
||||
const row = rows[0]!;
|
||||
broker.publishUser('default', {
|
||||
type: 'chat_archived',
|
||||
chat_id: row.id,
|
||||
session_id: row.session_id,
|
||||
});
|
||||
reply.code(204);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
app.post<{ Params: { id: string } }>(
|
||||
'/api/chats/:id/unarchive',
|
||||
async (req, reply) => {
|
||||
const rows = await sql<Chat[]>`
|
||||
UPDATE chats SET status = 'open', updated_at = clock_timestamp()
|
||||
WHERE id = ${req.params.id} AND status = 'archived'
|
||||
RETURNING id, session_id, name, status, created_at, updated_at
|
||||
`;
|
||||
if (rows.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'chat not found or not archived' };
|
||||
}
|
||||
const chat = rows[0]!;
|
||||
broker.publishUser('default', { type: 'chat_unarchived', chat });
|
||||
return chat;
|
||||
}
|
||||
);
|
||||
@@ -147,6 +170,12 @@ export function registerChatRoutes(
|
||||
reply.code(404);
|
||||
return { error: 'chat not found' };
|
||||
}
|
||||
const row = result[0]!;
|
||||
broker.publishUser('default', {
|
||||
type: 'chat_deleted',
|
||||
chat_id: row.id,
|
||||
session_id: row.session_id,
|
||||
});
|
||||
reply.code(204);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user