From 2464d23bb60d3b8f9febef1c0ead37240df3e539 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Thu, 14 May 2026 22:52:40 +0000 Subject: [PATCH] v1.1 batch 1: markdown, message actions, tok/s+ctx, AI naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four features land together on this branch: 1. Markdown rendering — assistant messages go through react-markdown + remark-gfm. Fenced code blocks render via existing CodeBlock (with copy button); inline `code` is styled inline. User messages stay plain text. No raw HTML (no rehype-raw). 2. Per-message Copy + Regenerate. New endpoint POST /api/sessions/:id/messages/:message_id/regenerate validates the target (404/400/409), atomically deletes the target plus any later messages in the session, inserts a fresh streaming assistant row, and enqueues a normal inference run. The DELETE bound uses a SQL subquery (`created_at >= (SELECT created_at FROM messages WHERE id = $1)`) instead of a JS round-trip so postgres TIMESTAMPTZ µs precision is preserved — otherwise sub-ms clock_timestamp() differences between the user row and the assistant row collapsed to the same JS Date, pulling the triggering user message into the >= bound. New `messages_deleted` WS frame so already-connected clients prune the stale tail without needing a full snapshot resend. 3. tok/s + ctx counter. Five new nullable message columns: tokens_used, ctx_used, ctx_max, started_at, finished_at. started_at is set right before the OpenAI call in services/inference.ts (not in the route, not in the frame handler); finished_at + tokens_used + ctx_used + ctx_max are committed in the same UPDATE that flips status to 'complete'. The inference request now opts into stream_options.include_usage so the final chunk carries usage; defensive parsing also picks up timings.n_ctx when llama.cpp emits it (currently absent for our llama-swap models, so ctx_max stays NULL and the UI just shows ` ctx`). message_complete frame extended with tokens_used / ctx_used / ctx_max / started_at / finished_at / model. Frontend StatsLine in MessageBubble computes tok/s client-side from the timestamps and renders muted mono text below the body of completed assistant messages. 4. AI chat naming after the first turn. Backend services/auto_name.ts runs via setImmediate after the top-level inference resolves; it checks that there is exactly one completed assistant message and that the session has not been user-renamed (`name IS NULL OR name = '' OR name = 'New session'`), then fires a single non-streaming chat completion with the spec prompt. Qwen3 chat templates emit chain-of- thought into reasoning_content and burn the entire max_tokens budget without producing visible output, so the request includes `chat_template_kwargs: { enable_thinking: false }` and max_tokens=30. Title is trimmed, quote-stripped, "Title:" prefix dropped, and truncated to 60 chars before a guarded UPDATE on sessions.name. New `session_renamed` WS frame propagates to the open session view directly and to the project's session list via a tiny module-scope event bus (apps/web/src/hooks/sessionEvents.ts) — kept dumb: one event type, two methods, no library. Cleanups: dropped the now-unused splitCodeBlocks export from CodeBlock.tsx (react-markdown supersedes it), and added a long-form NOTE in auto_name.ts documenting the enable_thinking + max_tokens pattern for any future Qwen- family non-streaming utility calls (planned: fork-message, agent-routing, web-search summarization). Schema bootstrap remains idempotent (ADD COLUMN IF NOT EXISTS). Auth, broker, clock_timestamp() conventions, and zod validation all unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/server/src/index.ts | 8 +- apps/server/src/routes/messages.ts | 64 +- apps/server/src/routes/ws.ts | 3 +- apps/server/src/schema.sql | 6 + apps/server/src/services/auto_name.ts | 157 ++++ apps/server/src/services/inference.ts | 178 ++++- apps/server/src/types/api.ts | 5 + apps/web/package.json | 2 + apps/web/src/api/client.ts | 5 + apps/web/src/api/types.ts | 17 +- apps/web/src/components/CodeBlock.tsx | 33 - apps/web/src/components/MessageBubble.tsx | 200 ++++- apps/web/src/components/MessageList.tsx | 5 +- apps/web/src/hooks/sessionEvents.ts | 32 + apps/web/src/hooks/useSessionStream.ts | 39 +- apps/web/src/hooks/useSessions.ts | 18 + apps/web/src/pages/Session.tsx | 13 +- pnpm-lock.yaml | 868 ++++++++++++++++++++++ 18 files changed, 1559 insertions(+), 94 deletions(-) create mode 100644 apps/server/src/services/auto_name.ts create mode 100644 apps/web/src/hooks/sessionEvents.ts diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 62455df..770fdf3 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -50,7 +50,7 @@ async function main() { }, }); registerMessageRoutes(app, sql, { - onSend: (sessionId, _userId, assistantId) => { + enqueueInference: (sessionId, assistantId) => { inference.enqueue(sessionId, assistantId); }, publishUserMessage: (sessionId, userMessageId, content) => { @@ -69,6 +69,12 @@ async function main() { message_id: userMessageId, }); }, + publishMessagesDeleted: (sessionId, messageIds) => { + broker.publish(sessionId, { + type: 'messages_deleted', + message_ids: messageIds, + }); + }, }); registerWebSocket(app, sql, broker); diff --git a/apps/server/src/routes/messages.ts b/apps/server/src/routes/messages.ts index 97c43eb..0c9f9e0 100644 --- a/apps/server/src/routes/messages.ts +++ b/apps/server/src/routes/messages.ts @@ -8,12 +8,13 @@ const SendBody = z.object({ }); interface MessageHandlers { - onSend: (sessionId: string, userMessageId: string, assistantMessageId: string) => void; + enqueueInference: (sessionId: string, assistantMessageId: string) => void; publishUserMessage: ( sessionId: string, userMessageId: string, content: string ) => void; + publishMessagesDeleted: (sessionId: string, messageIds: string[]) => void; } export function registerMessageRoutes( @@ -30,7 +31,8 @@ export function registerMessageRoutes( return { error: 'session not found' }; } const rows = await sql` - SELECT id, session_id, role, content, tool_calls, tool_results, status, last_seq, created_at + SELECT id, session_id, role, content, tool_calls, tool_results, status, last_seq, + tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at FROM messages WHERE session_id = ${req.params.id} ORDER BY created_at ASC, id ASC @@ -74,10 +76,66 @@ export function registerMessageRoutes( result.user_message_id, parsed.data.content ); - handlers.onSend(req.params.id, result.user_message_id, result.assistant_message_id); + handlers.enqueueInference(req.params.id, result.assistant_message_id); reply.code(202); return result; } ); + + app.post<{ Params: { id: string; message_id: string } }>( + '/api/sessions/:id/messages/:message_id/regenerate', + async (req, reply) => { + const { id: sessionId, message_id: targetId } = req.params; + + const target = await sql<{ id: string; role: string; status: string }[]>` + SELECT id, role, status + FROM messages + WHERE session_id = ${sessionId} AND id = ${targetId} + `; + if (target.length === 0) { + reply.code(404); + return { error: 'message not found' }; + } + const targetRow = target[0]!; + if (targetRow.role !== 'assistant') { + reply.code(400); + return { error: 'only assistant messages can be regenerated' }; + } + if (targetRow.status === 'streaming') { + reply.code(409); + return { error: 'message is still streaming' }; + } + + const { newAssistantId, deletedIds } = await sql.begin(async (tx) => { + // Subquery keeps created_at in postgres at TIMESTAMPTZ µs precision. + // Round-tripping through JS Date loses sub-ms precision and can pull + // earlier rows (e.g. the triggering user message) into the >= bound. + const deletedRows = await tx<{ id: string }[]>` + DELETE FROM messages + WHERE session_id = ${sessionId} + AND created_at >= ( + SELECT created_at FROM messages WHERE id = ${targetId} + ) + RETURNING id + `; + const [row] = await tx<{ id: string }[]>` + INSERT INTO messages (session_id, role, content, status, created_at) + VALUES (${sessionId}, 'assistant', '', 'streaming', clock_timestamp()) + RETURNING id + `; + await tx`UPDATE sessions SET updated_at = NOW() WHERE id = ${sessionId}`; + return { + newAssistantId: row!.id, + deletedIds: deletedRows.map((r) => r.id), + }; + }); + + handlers.publishMessagesDeleted(sessionId, deletedIds); + handlers.enqueueInference(sessionId, newAssistantId); + + reply.code(202); + return { assistant_message_id: newAssistantId }; + } + ); } diff --git a/apps/server/src/routes/ws.ts b/apps/server/src/routes/ws.ts index b1f8d33..c5b1a00 100644 --- a/apps/server/src/routes/ws.ts +++ b/apps/server/src/routes/ws.ts @@ -22,7 +22,8 @@ export function registerWebSocket( } const messages = await sql` - SELECT id, session_id, role, content, tool_calls, tool_results, status, last_seq, created_at + SELECT id, session_id, role, content, tool_calls, tool_results, status, last_seq, + tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at FROM messages WHERE session_id = ${sessionId} ORDER BY created_at ASC, id ASC diff --git a/apps/server/src/schema.sql b/apps/server/src/schema.sql index aabc4ed..91b4ac4 100644 --- a/apps/server/src/schema.sql +++ b/apps/server/src/schema.sql @@ -32,6 +32,12 @@ CREATE TABLE IF NOT EXISTS messages ( CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at); +ALTER TABLE messages ADD COLUMN IF NOT EXISTS tokens_used INTEGER; +ALTER TABLE messages ADD COLUMN IF NOT EXISTS ctx_used INTEGER; +ALTER TABLE messages ADD COLUMN IF NOT EXISTS ctx_max INTEGER; +ALTER TABLE messages ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ; +ALTER TABLE messages ADD COLUMN IF NOT EXISTS finished_at TIMESTAMPTZ; + CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value JSONB NOT NULL diff --git a/apps/server/src/services/auto_name.ts b/apps/server/src/services/auto_name.ts new file mode 100644 index 0000000..8198865 --- /dev/null +++ b/apps/server/src/services/auto_name.ts @@ -0,0 +1,157 @@ +import type { InferenceContext } from './inference.js'; + +const NAMING_SYSTEM_PROMPT = + 'You name chat sessions. Reply directly with no thinking, reasoning, or explanation. Output ONLY the title, 4 words max, no quotes, no punctuation, no prefix like "Title:".'; + +const MAX_TITLE_CHARS = 60; + +// QWEN3 NON-STREAMING UTILITY-CALL PATTERN +// ---------------------------------------- +// Qwen3-family chat templates default to chain-of-thought reasoning: the +// model emits a long block into `reasoning_content` and +// only finalizes a real reply in `content`. For short utility calls +// (naming, classification, routing, summarization) with a tight token +// budget, the model burns the entire budget on reasoning and returns: +// - content: "" +// - reasoning_content: "Thinking Process: 1. ..." (mid-thought, truncated) +// - finish_reason: "length" +// Fix: pass `chat_template_kwargs: { enable_thinking: false }` to skip the +// thinking block, and keep `max_tokens` low (~30 is plenty for a 4-word +// title). The kwarg is a no-op for non-Qwen chat templates, so it's safe +// to apply unconditionally for any short non-streaming model call. +// Apply this same pattern to: fork-message (planned), agent-routing +// (planned), web-search summarization (planned). + +function cleanTitle(raw: string): string { + let name = raw.trim(); + // Strip surrounding straight or smart quotes (one layer). + const quotes = ['"', "'", '`', '‘', '’', '“', '”']; + while (name.length >= 2 && quotes.includes(name[0]!) && quotes.includes(name[name.length - 1]!)) { + name = name.slice(1, -1).trim(); + } + // Drop a leading "Title:" prefix if the model added one despite instructions. + name = name.replace(/^title\s*:\s*/i, '').trim(); + if (name.length > MAX_TITLE_CHARS) { + name = name.slice(0, MAX_TITLE_CHARS).trim(); + } + return name; +} + +interface NamingResponse { + choices?: Array<{ + message?: { + content?: string; + reasoning_content?: string; + }; + }>; +} + +// Some Qwen-family models emit "thinking" tokens into reasoning_content and +// only finalize a real reply in content. Pull a sensible candidate string. +function pickTitleSource(data: NamingResponse): string { + const choice = data.choices?.[0]?.message; + if (!choice) return ''; + if (choice.content && choice.content.trim().length > 0) return choice.content; + // Fallback: try to extract a last-line title from reasoning, if present. + const reasoning = choice.reasoning_content ?? ''; + if (reasoning.length === 0) return ''; + const lines = reasoning + .split('\n') + .map((l) => l.trim()) + .filter((l) => l.length > 0); + return lines[lines.length - 1] ?? ''; +} + +export async function maybeAutoNameSession( + ctx: InferenceContext, + sessionId: string +): Promise { + const counts = await ctx.sql<{ n: number }[]>` + SELECT COUNT(*)::int AS n + FROM messages + WHERE session_id = ${sessionId} + AND role = 'assistant' + AND status = 'complete' + `; + if (counts[0]?.n !== 1) return; + + const sessionRows = await ctx.sql< + { id: string; name: string; model: string }[] + >` + SELECT id, name, model FROM sessions WHERE id = ${sessionId} + `; + const session = sessionRows[0]; + if (!session) return; + const existingName = session.name ?? ''; + if (existingName !== '' && existingName !== 'New session') return; + + const userMsg = await ctx.sql<{ content: string }[]>` + SELECT content FROM messages + WHERE session_id = ${sessionId} AND role = 'user' + ORDER BY created_at ASC + LIMIT 1 + `; + const assistantMsg = await ctx.sql<{ content: string }[]>` + SELECT content FROM messages + WHERE session_id = ${sessionId} + AND role = 'assistant' + AND status = 'complete' + ORDER BY created_at ASC + LIMIT 1 + `; + if (!userMsg[0] || !assistantMsg[0]) return; + + const userText = userMsg[0].content.slice(0, 2000); + const assistantText = assistantMsg[0].content.slice(0, 2000); + + const body = { + model: session.model, + messages: [ + { role: 'system', content: NAMING_SYSTEM_PROMPT }, + { + role: 'user', + content: `First user message: ${userText}\nFirst assistant reply: ${assistantText}`, + }, + ], + max_tokens: 30, + temperature: 0.3, + stream: false, + // Qwen-family models default to chain-of-thought; this template kwarg + // tells llama.cpp's chat template renderer to skip the thinking block. + // Harmless for non-Qwen models. + chat_template_kwargs: { enable_thinking: false }, + }; + + const res = await fetch(`${ctx.config.LLAMA_SWAP_URL}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`naming request failed: ${res.status} ${text.slice(0, 200)}`); + } + const data = (await res.json()) as NamingResponse; + const raw = pickTitleSource(data); + const name = cleanTitle(raw); + if (!name) { + ctx.log.warn({ sessionId, raw }, 'auto-name: empty title from model'); + return; + } + + const updated = await ctx.sql<{ id: string; name: string }[]>` + UPDATE sessions + SET name = ${name}, updated_at = NOW() + WHERE id = ${sessionId} + AND (name IS NULL OR name = '' OR name = 'New session') + RETURNING id, name + `; + if (updated.length === 0) return; + + ctx.publish(sessionId, { + type: 'session_renamed', + session_id: sessionId, + name, + }); + ctx.log.info({ sessionId, name }, 'session auto-named'); +} diff --git a/apps/server/src/services/inference.ts b/apps/server/src/services/inference.ts index d026b32..f30f03b 100644 --- a/apps/server/src/services/inference.ts +++ b/apps/server/src/services/inference.ts @@ -4,6 +4,7 @@ import type { Config } from '../config.js'; import type { Message, Project, Session, ToolCall } from '../types/api.js'; import { ALL_TOOLS, TOOLS_BY_NAME, toolJsonSchemas } from './tools.js'; import { PathScopeError, resolveProjectRoot } from './path_guard.js'; +import { maybeAutoNameSession } from './auto_name.js'; const BASE_SYSTEM_PROMPT = (projectPath: string) => `You are BooCode Chat, a code investigation assistant. The user is working on a project located at ${projectPath}. Use the file-read tools (view_file, list_dir, grep, find_files) to investigate code when needed. Be concise. Cite file paths and line numbers when discussing code. Do not hallucinate file contents — read the file first. Tool results may be truncated; if so, narrow your query rather than guessing.`; @@ -12,8 +13,17 @@ const DB_FLUSH_INTERVAL_MS = 500; const MAX_TOOL_LOOP_DEPTH = 5; export interface InferenceFrame { - type: 'message_started' | 'delta' | 'tool_call' | 'tool_result' | 'message_complete' | 'error'; + type: + | 'message_started' + | 'delta' + | 'tool_call' + | 'tool_result' + | 'message_complete' + | 'messages_deleted' + | 'session_renamed' + | 'error'; message_id?: string; + message_ids?: string[]; tool_message_id?: string; tool_call_id?: string; role?: 'assistant' | 'tool' | 'user'; @@ -22,6 +32,14 @@ export interface InferenceFrame { output?: unknown; truncated?: boolean; error?: string; + tokens_used?: number | null; + ctx_used?: number | null; + ctx_max?: number | null; + started_at?: string | null; + finished_at?: string | null; + model?: string; + session_id?: string; + name?: string; } export type FramePublisher = (sessionId: string, frame: InferenceFrame) => void; @@ -49,13 +67,21 @@ interface ChatCompletionDelta { } interface ChatCompletionChunk { - choices: Array<{ + choices?: Array<{ delta: ChatCompletionDelta; finish_reason: string | null; }>; + usage?: { + prompt_tokens?: number; + completion_tokens?: number; + total_tokens?: number; + }; + timings?: { + n_ctx?: number; + }; } -interface InferenceContext { +export interface InferenceContext { sql: Sql; config: Config; log: FastifyBaseLogger; @@ -130,7 +156,8 @@ async function loadContext( const project = projectRows[0]!; const history = await sql` - SELECT id, session_id, role, content, tool_calls, tool_results, status, last_seq, created_at + SELECT id, session_id, role, content, tool_calls, tool_results, status, last_seq, + tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at FROM messages WHERE session_id = ${sessionId} ORDER BY created_at ASC, id ASC @@ -162,14 +189,28 @@ async function* sseLines(stream: ReadableStream): AsyncGenerator void -): Promise<{ finishReason: string | null; content: string; toolCalls: ToolCall[] }> { - const body: Record = { model, messages, stream: true }; +): Promise { + const body: Record = { + model, + messages, + stream: true, + stream_options: { include_usage: true }, + }; if (includeTools) { body['tools'] = toolJsonSchemas(); body['tool_choice'] = 'auto'; @@ -187,6 +228,9 @@ async function streamCompletion( let content = ''; let finishReason: string | null = null; + let promptTokens: number | null = null; + let completionTokens: number | null = null; + let nCtx: number | null = null; const toolCallsBuffer = new Map(); for await (const line of sseLines(res.body)) { @@ -199,6 +243,19 @@ async function streamCompletion( } catch { continue; } + + if (parsed.usage) { + if (typeof parsed.usage.prompt_tokens === 'number') { + promptTokens = parsed.usage.prompt_tokens; + } + if (typeof parsed.usage.completion_tokens === 'number') { + completionTokens = parsed.usage.completion_tokens; + } + } + if (parsed.timings && typeof parsed.timings.n_ctx === 'number') { + nCtx = parsed.timings.n_ctx; + } + const choice = parsed.choices?.[0]; if (!choice) continue; const delta = choice.delta ?? {}; @@ -232,7 +289,7 @@ async function streamCompletion( toolCalls.push({ id: t.id || `call_${toolCalls.length}`, name: t.name, args }); } - return { finishReason, content, toolCalls }; + return { finishReason, content, toolCalls, promptTokens, completionTokens, nCtx }; } async function executeToolCall( @@ -279,7 +336,9 @@ async function runAssistantTurn( if (depth > MAX_TOOL_LOOP_DEPTH) { await ctx.sql` UPDATE messages - SET status = 'failed', content = ${'tool loop depth exceeded'} + SET status = 'failed', + content = ${'tool loop depth exceeded'}, + finished_at = clock_timestamp() WHERE id = ${assistantMessageId} `; ctx.publish(sessionId, { @@ -299,6 +358,14 @@ async function runAssistantTurn( const projectRoot = await resolveProjectRoot(project.path); const messages = buildMessagesPayload(session, project, history); + const startedRow = await ctx.sql<{ started_at: string }[]>` + UPDATE messages + SET started_at = clock_timestamp() + WHERE id = ${assistantMessageId} + RETURNING started_at + `; + const startedAt = startedRow[0]?.started_at ?? null; + ctx.publish(sessionId, { type: 'message_started', message_id: assistantMessageId, @@ -328,12 +395,9 @@ async function runAssistantTurn( }, DB_FLUSH_INTERVAL_MS); }; - let content = ''; - let finishReason: string | null = null; - let toolCalls: ToolCall[] = []; - + let result: StreamResult; try { - const result = await streamCompletion( + result = await streamCompletion( ctx, session.model, messages, @@ -349,9 +413,6 @@ async function runAssistantTurn( scheduleFlush(); } ); - content = result.content; - finishReason = result.finishReason; - toolCalls = result.toolCalls; } catch (err) { if (pendingFlushTimer) { clearTimeout(pendingFlushTimer); @@ -360,7 +421,9 @@ async function runAssistantTurn( const errMsg = err instanceof Error ? err.message : String(err); await ctx.sql` UPDATE messages - SET status = 'failed', content = ${accumulated} + SET status = 'failed', + content = ${accumulated}, + finished_at = clock_timestamp() WHERE id = ${assistantMessageId} `; ctx.publish(sessionId, { @@ -378,12 +441,22 @@ async function runAssistantTurn( } await flushPromise; + const { content, finishReason, toolCalls, promptTokens, completionTokens, nCtx } = result; + if (toolCalls.length > 0) { - await ctx.sql` + const [updated] = await ctx.sql< + { tokens_used: number | null; ctx_used: number | null; ctx_max: number | null; finished_at: string | null }[] + >` UPDATE messages - SET content = ${content}, status = 'complete', - tool_calls = ${ctx.sql.json(toolCalls as never)} + SET content = ${content}, + status = 'complete', + tool_calls = ${ctx.sql.json(toolCalls as never)}, + tokens_used = ${completionTokens}, + ctx_used = ${promptTokens}, + ctx_max = ${nCtx}, + finished_at = clock_timestamp() WHERE id = ${assistantMessageId} + RETURNING tokens_used, ctx_used, ctx_max, finished_at `; for (const tc of toolCalls) { ctx.publish(sessionId, { @@ -395,6 +468,12 @@ async function runAssistantTurn( ctx.publish(sessionId, { type: 'message_complete', message_id: assistantMessageId, + tokens_used: updated?.tokens_used ?? null, + ctx_used: updated?.ctx_used ?? null, + ctx_max: updated?.ctx_max ?? null, + started_at: startedAt, + finished_at: updated?.finished_at ?? null, + model: session.model, }); await Promise.all( @@ -405,12 +484,12 @@ async function runAssistantTurn( RETURNING id `; const toolMessageId = toolRow!.id; - const result = await executeToolCall(projectRoot, tc); + const tres = await executeToolCall(projectRoot, tc); const stored = { tool_call_id: tc.id, - output: result.output, - truncated: result.truncated, - ...(result.error ? { error: result.error } : {}), + output: tres.output, + truncated: tres.truncated, + ...(tres.error ? { error: tres.error } : {}), }; await ctx.sql` UPDATE messages @@ -421,9 +500,9 @@ async function runAssistantTurn( type: 'tool_result', tool_message_id: toolMessageId, tool_call_id: tc.id, - output: result.output, - truncated: result.truncated, - ...(result.error ? { error: result.error } : {}), + output: tres.output, + truncated: tres.truncated, + ...(tres.error ? { error: tres.error } : {}), }); }) ); @@ -437,16 +516,40 @@ async function runAssistantTurn( return; } - await ctx.sql` + const [updated] = await ctx.sql< + { tokens_used: number | null; ctx_used: number | null; ctx_max: number | null; finished_at: string | null }[] + >` UPDATE messages - SET content = ${content}, status = 'complete' + SET content = ${content}, + status = 'complete', + tokens_used = ${completionTokens}, + ctx_used = ${promptTokens}, + ctx_max = ${nCtx}, + finished_at = clock_timestamp() WHERE id = ${assistantMessageId} + RETURNING tokens_used, ctx_used, ctx_max, finished_at `; ctx.publish(sessionId, { type: 'message_complete', message_id: assistantMessageId, + tokens_used: updated?.tokens_used ?? null, + ctx_used: updated?.ctx_used ?? null, + ctx_max: updated?.ctx_max ?? null, + started_at: startedAt, + finished_at: updated?.finished_at ?? null, + model: session.model, }); - ctx.log.info({ sessionId, assistantMessageId, finishReason, chars: content.length }, 'inference complete'); + ctx.log.info( + { + sessionId, + assistantMessageId, + finishReason, + chars: content.length, + tokens_used: updated?.tokens_used, + ctx_used: updated?.ctx_used, + }, + 'inference complete' + ); } export async function runInference( @@ -460,9 +563,18 @@ export async function runInference( export function createInferenceRunner(ctx: InferenceContext) { return { enqueue(sessionId: string, assistantMessageId: string) { - void runInference(ctx, sessionId, assistantMessageId).catch((err) => { - ctx.log.error({ err }, 'unhandled inference error'); - }); + void (async () => { + try { + await runInference(ctx, sessionId, assistantMessageId); + setImmediate(() => { + void maybeAutoNameSession(ctx, sessionId).catch((err) => { + ctx.log.warn({ err, sessionId }, 'auto-name failed'); + }); + }); + } catch (err) { + ctx.log.error({ err }, 'unhandled inference error'); + } + })(); }, }; } diff --git a/apps/server/src/types/api.ts b/apps/server/src/types/api.ts index 1cd2b5c..a6e08d0 100644 --- a/apps/server/src/types/api.ts +++ b/apps/server/src/types/api.ts @@ -46,6 +46,11 @@ export interface Message { tool_results: ToolResult | null; status: MessageStatus; last_seq: number; + tokens_used: number | null; + ctx_used: number | null; + ctx_max: number | null; + started_at: string | null; + finished_at: string | null; created_at: string; } diff --git a/apps/web/package.json b/apps/web/package.json index 8e7c2f7..19a5c6f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,7 +19,9 @@ "radix-ui": "^1.4.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", "react-router-dom": "^6.26.0", + "remark-gfm": "^4.0.1", "shadcn": "^4.7.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index da174d1..146e861 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -83,6 +83,11 @@ export const api = { body: JSON.stringify({ content }), } ), + regenerate: (sessionId: string, messageId: string) => + request<{ assistant_message_id: string }>( + `/api/sessions/${sessionId}/messages/${messageId}/regenerate`, + { method: 'POST' } + ), }, models: () => request('/api/models'), diff --git a/apps/web/src/api/types.ts b/apps/web/src/api/types.ts index eaa7578..da5dacf 100644 --- a/apps/web/src/api/types.ts +++ b/apps/web/src/api/types.ts @@ -46,6 +46,11 @@ export interface Message { tool_results: ToolResult | null; status: MessageStatus; last_seq: number; + tokens_used: number | null; + ctx_used: number | null; + ctx_max: number | null; + started_at: string | null; + finished_at: string | null; created_at: string; } @@ -67,5 +72,15 @@ export type WsFrame = truncated: boolean; error?: string; } - | { type: 'message_complete'; message_id: string } + | { + type: 'message_complete'; + message_id: string; + tokens_used?: number | null; + ctx_used?: number | null; + ctx_max?: number | null; + started_at?: string | null; + finished_at?: string | null; + } + | { type: 'messages_deleted'; message_ids: string[] } + | { type: 'session_renamed'; session_id: string; name: string } | { type: 'error'; message_id?: string; error: string }; diff --git a/apps/web/src/components/CodeBlock.tsx b/apps/web/src/components/CodeBlock.tsx index 63d9467..99b0256 100644 --- a/apps/web/src/components/CodeBlock.tsx +++ b/apps/web/src/components/CodeBlock.tsx @@ -42,36 +42,3 @@ export function CodeBlock({ code, lang }: Props) { ); } - -interface SegmentText { - kind: 'text'; - value: string; -} -interface SegmentCode { - kind: 'code'; - lang?: string; - value: string; -} -export type Segment = SegmentText | SegmentCode; - -export function splitCodeBlocks(input: string): Segment[] { - const segments: Segment[] = []; - const fence = /```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/g; - let lastIndex = 0; - let match: RegExpExecArray | null; - while ((match = fence.exec(input)) !== null) { - if (match.index > lastIndex) { - segments.push({ kind: 'text', value: input.slice(lastIndex, match.index) }); - } - segments.push({ - kind: 'code', - lang: match[1] || undefined, - value: (match[2] ?? '').replace(/\n$/, ''), - }); - lastIndex = match.index + match[0].length; - } - if (lastIndex < input.length) { - segments.push({ kind: 'text', value: input.slice(lastIndex) }); - } - return segments; -} diff --git a/apps/web/src/components/MessageBubble.tsx b/apps/web/src/components/MessageBubble.tsx index b3dfef2..019af39 100644 --- a/apps/web/src/components/MessageBubble.tsx +++ b/apps/web/src/components/MessageBubble.tsx @@ -1,49 +1,209 @@ +import { useState } from 'react'; +import Markdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { Copy, RefreshCw, Check } from 'lucide-react'; +import { toast } from 'sonner'; import type { Message } from '@/api/types'; +import { api } from '@/api/client'; import { ToolCallCard } from './ToolCallCard'; -import { CodeBlock, splitCodeBlocks } from './CodeBlock'; +import { CodeBlock } from './CodeBlock'; interface Props { message: Message; + sessionId: string; } -export function MessageBubble({ message }: Props) { +function MarkdownBody({ content }: { content: string }) { + return ( + <>{children}, + code: (props) => { + const { children, className, ...rest } = props as { + children?: unknown; + className?: string; + }; + const text = String(children ?? '').replace(/\n$/, ''); + const langMatch = /language-([\w-]+)/.exec(className ?? ''); + const isBlock = !!langMatch || text.includes('\n'); + if (isBlock) { + return ; + } + return ( + + {children as React.ReactNode} + + ); + }, + a: ({ children, href }) => ( + + {children} + + ), + ul: ({ children }) => ( +
    {children}
+ ), + ol: ({ children }) => ( +
    {children}
+ ), + p: ({ children }) =>

{children}

, + h1: ({ children }) =>

{children}

, + h2: ({ children }) =>

{children}

, + h3: ({ children }) =>

{children}

, + blockquote: ({ children }) => ( +
+ {children} +
+ ), + table: ({ children }) => ( +
+ {children}
+
+ ), + th: ({ children }) => ( + {children} + ), + td: ({ children }) => ( + {children} + ), + }} + > + {content} +
+ ); +} + +function StatsLine({ message }: { message: Message }) { + const tokens = message.tokens_used; + if (typeof tokens !== 'number' || tokens <= 0) return null; + const started = message.started_at ? Date.parse(message.started_at) : NaN; + const finished = message.finished_at ? Date.parse(message.finished_at) : NaN; + let tps: number | null = null; + if (!Number.isNaN(started) && !Number.isNaN(finished) && finished > started) { + const seconds = (finished - started) / 1000; + if (seconds > 0) tps = Math.round((tokens / seconds) * 10) / 10; + } + const ctxUsed = message.ctx_used; + const ctxMax = message.ctx_max; + const ctxPart = + typeof ctxUsed === 'number' + ? typeof ctxMax === 'number' && ctxMax > 0 + ? `${ctxUsed} / ${ctxMax} ctx` + : `${ctxUsed} ctx` + : null; + + const parts: string[] = [`${tokens} tokens`]; + if (tps !== null) parts.push(`${tps.toFixed(1)} tok/s`); + if (ctxPart) parts.push(ctxPart); + + return ( +
+ {parts.join(' · ')} +
+ ); +} + +function ActionRow({ + message, + sessionId, +}: { + message: Message; + sessionId: string; +}) { + const [justCopied, setJustCopied] = useState(false); + const [regenerating, setRegenerating] = useState(false); + + async function copy() { + try { + await navigator.clipboard.writeText(message.content); + setJustCopied(true); + setTimeout(() => setJustCopied(false), 1200); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'copy failed'); + } + } + + async function regenerate() { + if (regenerating || message.status === 'streaming') return; + setRegenerating(true); + try { + await api.messages.regenerate(sessionId, message.id); + } catch (err) { + toast.error(err instanceof Error ? err.message : 'regenerate failed'); + } finally { + setRegenerating(false); + } + } + + const isAssistant = message.role === 'assistant'; + const canRegen = isAssistant && message.status !== 'streaming'; + + return ( +
+ + {isAssistant && ( + + )} +
+ ); +} + +export function MessageBubble({ message, sessionId }: Props) { if (message.role === 'tool') { return ; } if (message.role === 'user') { return ( -
+
{message.content}
+
); } const isStreaming = message.status === 'streaming'; const failed = message.status === 'failed'; + const hasContent = message.content.length > 0; + const hasToolCalls = (message.tool_calls?.length ?? 0) > 0; return ( -
+
{message.tool_calls?.map((tc) => ( ))} - {(message.content.length > 0 || (!message.tool_calls?.length && isStreaming)) && ( + {(hasContent || (!hasToolCalls && isStreaming)) && (
- {splitCodeBlocks(message.content).map((seg, i) => - seg.kind === 'code' ? ( - - ) : ( -
- {seg.value} - {isStreaming && i === splitCodeBlocks(message.content).length - 1 && ( - - )} -
- ) - )} - {message.content.length === 0 && isStreaming && ( + {hasContent ? : null} + {isStreaming && ( )}
@@ -51,6 +211,10 @@ export function MessageBubble({ message }: Props) { {failed && (
message failed
)} + {!isStreaming && } + {!isStreaming && (hasContent || hasToolCalls) && ( + + )}
); } diff --git a/apps/web/src/components/MessageList.tsx b/apps/web/src/components/MessageList.tsx index 005fe29..ccb521c 100644 --- a/apps/web/src/components/MessageList.tsx +++ b/apps/web/src/components/MessageList.tsx @@ -4,9 +4,10 @@ import { MessageBubble } from './MessageBubble'; interface Props { messages: Message[]; + sessionId: string; } -export function MessageList({ messages }: Props) { +export function MessageList({ messages, sessionId }: Props) { const endRef = useRef(null); useEffect(() => { @@ -24,7 +25,7 @@ export function MessageList({ messages }: Props) { return (
{messages.map((m) => ( - + ))}
diff --git a/apps/web/src/hooks/sessionEvents.ts b/apps/web/src/hooks/sessionEvents.ts new file mode 100644 index 0000000..e29b54e --- /dev/null +++ b/apps/web/src/hooks/sessionEvents.ts @@ -0,0 +1,32 @@ +// Tiny in-app event bus for session metadata changes that need to propagate +// across hooks (e.g. AI rename arriving via WS in the session view needs to +// also refresh the sidebar's session list). One event type for now. + +export interface SessionRenamedEvent { + type: 'session_renamed'; + session_id: string; + name: string; +} + +type SessionEvent = SessionRenamedEvent; +type Listener = (event: SessionEvent) => void; + +const listeners = new Set(); + +export const sessionEvents = { + emit(event: SessionEvent) { + for (const listener of listeners) { + try { + listener(event); + } catch { + // swallow — one bad listener shouldn't break others + } + } + }, + subscribe(listener: Listener): () => void { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; + }, +}; diff --git a/apps/web/src/hooks/useSessionStream.ts b/apps/web/src/hooks/useSessionStream.ts index 6f6490a..d708fd6 100644 --- a/apps/web/src/hooks/useSessionStream.ts +++ b/apps/web/src/hooks/useSessionStream.ts @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import type { Message, WsFrame } from '@/api/types'; +import { sessionEvents } from './sessionEvents'; interface State { messages: Message[]; @@ -24,6 +25,11 @@ function applyFrame(state: State, frame: WsFrame): State { tool_results: null, status: 'streaming', last_seq: 0, + tokens_used: null, + ctx_used: null, + ctx_max: null, + started_at: null, + finished_at: null, created_at: new Date().toISOString(), }; return { ...state, messages: [...state.messages, newMsg] }; @@ -76,16 +82,47 @@ function applyFrame(state: State, frame: WsFrame): State { }, status: 'complete', last_seq: 0, + tokens_used: null, + ctx_used: null, + ctx_max: null, + started_at: null, + finished_at: null, created_at: new Date().toISOString(), }; return { ...state, messages: [...state.messages, newMsg] }; } case 'message_complete': { const next = state.messages.map((m) => - m.id === frame.message_id ? { ...m, status: 'complete' as const } : m + m.id === frame.message_id + ? { + ...m, + status: 'complete' as const, + ...(frame.tokens_used !== undefined ? { tokens_used: frame.tokens_used } : {}), + ...(frame.ctx_used !== undefined ? { ctx_used: frame.ctx_used } : {}), + ...(frame.ctx_max !== undefined ? { ctx_max: frame.ctx_max } : {}), + ...(frame.started_at !== undefined ? { started_at: frame.started_at } : {}), + ...(frame.finished_at !== undefined ? { finished_at: frame.finished_at } : {}), + } + : m ); return { ...state, messages: next }; } + case 'messages_deleted': { + const removeSet = new Set(frame.message_ids); + return { + ...state, + messages: state.messages.filter((m) => !removeSet.has(m.id)), + }; + } + case 'session_renamed': { + // Side-effect, not state — dispatch via event bus to other hooks. + sessionEvents.emit({ + type: 'session_renamed', + session_id: frame.session_id, + name: frame.name, + }); + return state; + } case 'error': { const next = frame.message_id ? state.messages.map((m) => diff --git a/apps/web/src/hooks/useSessions.ts b/apps/web/src/hooks/useSessions.ts index a8f2b8f..35d6b6f 100644 --- a/apps/web/src/hooks/useSessions.ts +++ b/apps/web/src/hooks/useSessions.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { api } from '@/api/client'; import type { Session } from '@/api/types'; +import { sessionEvents } from './sessionEvents'; export function useSessions(projectId: string | undefined) { const [sessions, setSessions] = useState(null); @@ -24,6 +25,23 @@ export function useSessions(projectId: string | undefined) { void refresh(); }, [refresh]); + useEffect(() => { + return sessionEvents.subscribe((event) => { + if (event.type !== 'session_renamed') return; + setSessions((prev) => { + if (!prev) return prev; + let changed = false; + const next = prev.map((s) => { + if (s.id !== event.session_id) return s; + if (s.name === event.name) return s; + changed = true; + return { ...s, name: event.name }; + }); + return changed ? next : prev; + }); + }); + }, []); + const create = useCallback( async (body: { name?: string; model?: string; system_prompt?: string }) => { if (!projectId) throw new Error('no project'); diff --git a/apps/web/src/pages/Session.tsx b/apps/web/src/pages/Session.tsx index 898699f..a874fbc 100644 --- a/apps/web/src/pages/Session.tsx +++ b/apps/web/src/pages/Session.tsx @@ -5,6 +5,7 @@ import { toast } from 'sonner'; import { api } from '@/api/client'; import type { Session as SessionType } from '@/api/types'; import { useSessionStream } from '@/hooks/useSessionStream'; +import { sessionEvents } from '@/hooks/sessionEvents'; import { MessageList } from '@/components/MessageList'; import { ChatInput } from '@/components/ChatInput'; import { ModelPicker } from '@/components/ModelPicker'; @@ -39,6 +40,16 @@ export function Session() { .catch(() => {}); }, [id]); + useEffect(() => { + if (!id) return; + return sessionEvents.subscribe((event) => { + if (event.type !== 'session_renamed') return; + if (event.session_id !== id) return; + setSession((prev) => (prev ? { ...prev, name: event.name } : prev)); + setName((prev) => (editingName ? prev : event.name)); + }); + }, [id, editingName]); + async function saveName() { if (!id || !session) return; const trimmed = name.trim(); @@ -111,7 +122,7 @@ export function Session() { )} - + {id && }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51f11f5..e2c3ba6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,9 +75,15 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.28)(react@18.3.1) react-router-dom: specifier: ^6.26.0 version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 shadcn: specifier: ^4.7.0 version: 4.7.0(@types/node@20.19.41)(typescript@5.9.3) @@ -1667,9 +1673,24 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.19.41': resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} @@ -1690,12 +1711,21 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1766,6 +1796,9 @@ packages: avvio@8.4.0: resolution: {integrity: sha512-CDSwaxINFy59iNwhYnkvALBwZiTydGkOecZyPkqBpABYR1KqGEsET0VOOYDwtleZSUIdeY36DC2bSZ24CO1igA==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1821,10 +1854,25 @@ packages: caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -1858,6 +1906,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -1935,6 +1986,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -1963,6 +2017,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1970,6 +2028,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} @@ -2054,11 +2115,18 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -2089,6 +2157,9 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-content-type-parse@1.1.0: resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} @@ -2256,6 +2327,12 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + headers-polyfill@5.0.1: resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} @@ -2263,6 +2340,9 @@ packages: resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} engines: {node: '>=16.9.0'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -2298,6 +2378,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -2306,9 +2389,18 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2326,6 +2418,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -2518,6 +2613,9 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2536,10 +2634,58 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -2555,6 +2701,90 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2696,6 +2926,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -2784,6 +3017,9 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -2824,6 +3060,12 @@ packages: peerDependencies: react: ^18.3.1 + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -2887,6 +3129,18 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3033,6 +3287,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -3070,6 +3327,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-object@5.0.0: resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} engines: {node: '>=14.16'} @@ -3094,6 +3354,12 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + tagged-tag@1.0.0: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} @@ -3137,6 +3403,12 @@ packages: resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-morph@26.0.0: resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} @@ -3175,6 +3447,24 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -3228,6 +3518,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3331,6 +3627,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -4814,8 +5113,26 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@20.19.41': dependencies: undici-types: 6.21.0 @@ -4837,12 +5154,18 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@types/validate-npm-package-name@4.0.2': {} '@types/ws@8.18.1': dependencies: '@types/node': 20.19.41 + '@ungap/structured-clone@1.3.1': {} + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.41)(lightningcss@1.32.0))': dependencies: '@babel/core': 7.29.0 @@ -4906,6 +5229,8 @@ snapshots: '@fastify/error': 3.4.1 fastq: 1.20.1 + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -4966,8 +5291,18 @@ snapshots: caniuse-lite@1.0.30001792: {} + ccount@2.0.1: {} + chalk@5.6.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -4996,6 +5331,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + commander@11.1.0: {} commander@14.0.3: {} @@ -5048,6 +5385,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + dedent@1.7.2: {} deepmerge@4.3.1: {} @@ -5063,10 +5404,16 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + diff@8.0.4: {} dotenv@17.4.2: {} @@ -5187,8 +5534,12 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@5.0.0: {} + esprima@4.0.1: {} + estree-util-is-identifier-name@3.0.0: {} + etag@1.8.1: {} eventsource-parser@3.0.8: {} @@ -5262,6 +5613,8 @@ snapshots: transitivePeerDependencies: - supports-color + extend@3.0.2: {} + fast-content-type-parse@1.1.0: {} fast-decode-uri-component@1.0.1: {} @@ -5449,6 +5802,30 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + headers-polyfill@5.0.1: dependencies: '@types/set-cookie-parser': 2.4.10 @@ -5456,6 +5833,8 @@ snapshots: hono@4.12.18: {} + html-url-attributes@3.0.1: {} + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -5496,12 +5875,23 @@ snapshots: inherits@2.0.4: {} + inline-style-parser@0.2.7: {} + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arrayish@0.2.1: {} + is-decimal@2.0.1: {} + is-docker@3.0.0: {} is-extglob@2.1.1: {} @@ -5512,6 +5902,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -5650,6 +6042,8 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -5668,8 +6062,163 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + media-typer@1.1.0: {} merge-descriptors@2.0.0: {} @@ -5678,6 +6227,197 @@ snapshots: merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5817,6 +6557,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -5899,6 +6649,8 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + property-information@7.1.0: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -5990,6 +6742,24 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-markdown@10.1.0(@types/react@18.3.28)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 18.3.28 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@18.3.28)(react@18.3.1): @@ -6051,6 +6821,40 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -6264,6 +7068,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + split2@4.2.0: {} statuses@2.0.1: {} @@ -6298,6 +7104,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + stringify-object@5.0.0: dependencies: get-own-enumerable-keys: 1.0.0 @@ -6318,6 +7129,14 @@ snapshots: strip-final-newline@4.0.0: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + tagged-tag@1.0.0: {} tailwind-merge@3.6.0: {} @@ -6350,6 +7169,10 @@ snapshots: dependencies: tldts: 7.0.30 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-morph@26.0.0: dependencies: '@ts-morph/common': 0.27.0 @@ -6387,6 +7210,39 @@ snapshots: unicorn-magic@0.3.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -6424,6 +7280,16 @@ snapshots: vary@1.1.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@5.4.21(@types/node@20.19.41)(lightningcss@1.32.0): dependencies: esbuild: 0.21.5 @@ -6492,3 +7358,5 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} + + zwitch@2.0.4: {}