batch4.1-5.1: dedup audit, archive 400 fix, sidebar Delete, landing-page enrichment, auto-name tool-call fix

- Fastify global empty-JSON-body parser fixes archive/unarchive/stop 400s
- Removed redundant local sessionEvents.emit at all 5+2 sites with server-side WS publishers; added dedupe guards in useSidebar/Workspace/Project handlers
- Sidebar session right-click adds Delete (destructive) with confirm Dialog
- Session.tsx navigates away on session_deleted/session_archived for the active session
- SessionLandingPage chat rows show message_count, effective_context_tokens, last_message_preview via LATERAL joins on GET /api/sessions/:id/chats
- Workspace.tsx pane drag-to-reorder using native HTML5 events (no new deps)
- CompactCard: Copy toast, Send-to-chat with target chat name, empty-state in share popover, Re-run button
- auto_name.ts: filter count gate and assistant-fetch by content <> '' so tool-call assistant rows don't trip the once-and-only-once guard
- Adds CLAUDE.md and apps/web/src/lib/format.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 23:36:01 +00:00
parent c35ec65fc4
commit 051f3b96ae
15 changed files with 451 additions and 90 deletions

View File

@@ -24,6 +24,22 @@ async function main() {
logger: { level: config.LOG_LEVEL },
});
// Allow empty JSON bodies on POSTs that don't take a body (archive, unarchive, stop, etc.).
// Default Fastify parser throws FST_ERR_CTP_EMPTY_JSON_BODY on empty string.
app.removeContentTypeParser(['application/json']);
app.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) => {
const str = (body as string) ?? '';
if (str.trim().length === 0) {
done(null, {});
return;
}
try {
done(null, JSON.parse(str));
} catch (err) {
done(err as Error, undefined);
}
});
const sql = getSql(config);
await applySchema(sql);
app.log.info('database schema applied');

View File

@@ -26,11 +26,37 @@ export function registerChatRoutes(
reply.code(404);
return { error: 'session not found' };
}
// 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 id, session_id, name, status, created_at, updated_at
FROM chats
WHERE session_id = ${req.params.id}
ORDER BY updated_at DESC
SELECT
c.id, c.session_id, c.name, c.status, c.created_at, c.updated_at,
COALESCE(mc.cnt, 0)::int AS message_count,
lp.preview AS last_message_preview,
ec.tokens AS effective_context_tokens
FROM chats c
LEFT JOIN LATERAL (
SELECT COUNT(*) AS cnt FROM messages WHERE chat_id = c.id
) mc ON TRUE
LEFT JOIN LATERAL (
SELECT LEFT(BTRIM(REGEXP_REPLACE(content, E'[\\n\\r]+', ' ', 'g')), 80) AS preview
FROM messages
WHERE chat_id = c.id AND kind = 'message' AND content <> ''
ORDER BY created_at DESC
LIMIT 1
) lp ON TRUE
LEFT JOIN LATERAL (
SELECT ctx_used AS tokens
FROM messages
WHERE chat_id = c.id AND kind = 'message' AND role = 'assistant'
AND status = 'complete' AND ctx_used IS NOT NULL
ORDER BY created_at DESC
LIMIT 1
) ec ON TRUE
WHERE c.session_id = ${req.params.id}
ORDER BY c.updated_at DESC
`;
return rows;
}

View File

@@ -51,6 +51,7 @@ export async function maybeAutoNameChat(
WHERE chat_id = ${chatId}
AND role = 'assistant'
AND status = 'complete'
AND content <> ''
`;
if (counts[0]?.n !== 1) return;
@@ -80,6 +81,7 @@ export async function maybeAutoNameChat(
WHERE chat_id = ${chatId}
AND role = 'assistant'
AND status = 'complete'
AND content <> ''
ORDER BY created_at ASC
LIMIT 1
`;

View File

@@ -33,6 +33,10 @@ export interface Chat {
status: ChatStatus;
created_at: string;
updated_at: string;
// Populated by GET /api/sessions/:id/chats only.
message_count?: number;
last_message_preview?: string | null;
effective_context_tokens?: number | null;
}
// KEEP IN SYNC: apps/server/src/schema.sql messages_role_chk / messages_status_chk