From c8d9af6191402f6b9d2a012c9a02f95746624c00 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Mon, 15 Jun 2026 15:20:48 +0330 Subject: [PATCH 01/11] MCP tool-use execution loop for autonomous agent runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Autonomous agents with MCP tools now run a bounded tool-use loop: the model may call tools (executed via the gateway, results fed back) until it returns a final answer. Gated/DraftOnly agents get the tool catalog as data but never auto-call — a human-in-the-loop agent never autonomously reaches an external tool. Extends IModelClient with tool definitions and a tool-use conversation, adds the OpenAI-compatible tool serialization/parsing plus a deterministic "tooluse" stub client, and records every tool call in the run trace. Co-Authored-By: Claude Opus 4.8 --- .../Runtime/AgentRunExecutor.cs | 62 +++++++++- .../Ai/ModelClients.cs | 111 ++++++++++++++++-- .../IntegrationsModule.cs | 1 + .../TeamUp.SharedKernel/Ai/IModelClient.cs | 33 +++++- .../ModelClientToolTests.cs | 94 +++++++++++++++ 5 files changed, 285 insertions(+), 16 deletions(-) create mode 100644 tests/TeamUp.IntegrationTests/ModelClientToolTests.cs diff --git a/src/Modules/TeamUp.Modules.Assembler/Runtime/AgentRunExecutor.cs b/src/Modules/TeamUp.Modules.Assembler/Runtime/AgentRunExecutor.cs index 4d8f3cd..defabe2 100644 --- a/src/Modules/TeamUp.Modules.Assembler/Runtime/AgentRunExecutor.cs +++ b/src/Modules/TeamUp.Modules.Assembler/Runtime/AgentRunExecutor.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using TeamUp.Modules.Assembler.Domain; using TeamUp.Modules.Assembler.Persistence; +using TeamUp.SharedKernel.Access; using TeamUp.SharedKernel.Ai; namespace TeamUp.Modules.Assembler.Runtime; @@ -61,9 +62,7 @@ internal sealed class AgentRunExecutor( : null) ?? throw new InvalidOperationException("No usable model config for the agent."); - var completion = await modelClient.CompleteAsync( - new ModelRequest(config.Provider, config.Model, config.ApiKey, config.Endpoint, assembled.Prompt, MaxTokens: 512), - cancellationToken); + var (completion, output, toolCalls) = await RunModelAsync(context, assembled, config, tools, cancellationToken); if (!completion.Success) { @@ -79,9 +78,9 @@ internal sealed class AgentRunExecutor( action = assembled.PrimaryAction, risk = assembled.PrimaryActionRisk, skill = context.SkillKeys.Count > 0 ? context.SkillKeys[0] : null, + toolCalls, }); - var output = completion.Text ?? string.Empty; run.Complete(output, assembled.PrimaryAction, assembled.PrimaryActionRisk, result, completion.LatencyMs, clock.GetUtcNow()); await db.SaveChangesAsync(cancellationToken); @@ -107,4 +106,59 @@ internal sealed class AgentRunExecutor( logger.LogError(ex, "Agent-run job {JobId} failed.", job.Id); } } + + /// + /// One model call by default. For an Autonomous agent with MCP tools available, runs a bounded + /// tool-use loop: the model may call tools (executed via the gateway, results fed back) until it + /// returns a final answer. Gated/DraftOnly agents get the tool catalog as data but never auto-call + /// — a human-in-the-loop agent never autonomously reaches an external tool. The final artifact + /// still goes through the action gate; every tool call is recorded in the run trace. + /// + private async Task<(ModelCompletion Completion, string Output, IReadOnlyList ToolCalls)> RunModelAsync( + AgentRunContext context, AssembledPrompt assembled, ResolvedApiConfig config, + IReadOnlyList tools, CancellationToken cancellationToken) + { + ModelRequest Request(IReadOnlyList? toolDefs, IReadOnlyList? messages) => + new(config.Provider, config.Model, config.ApiKey, config.Endpoint, assembled.Prompt, MaxTokens: 512, toolDefs, messages); + + if (context.Autonomy != Autonomy.Autonomous || tools.Count == 0) + { + var single = await modelClient.CompleteAsync(Request(null, null), cancellationToken); + return (single, single.Text ?? string.Empty, []); + } + + var byName = tools + .GroupBy(t => t.Name, StringComparer.Ordinal) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal); + var toolDefs = tools.Select(t => new ModelTool(t.Name, t.Description, t.InputSchemaJson)).ToList(); + var messages = new List { new("user", assembled.Prompt) }; + var trace = new List(); + ModelCompletion completion = new(false, null, "No model response.", 0); + + const int maxIterations = 4; + for (var iteration = 0; iteration < maxIterations; iteration++) + { + completion = await modelClient.CompleteAsync(Request(toolDefs, messages), cancellationToken); + if (!completion.Success || completion.ToolCalls is not { Count: > 0 }) + { + break; + } + + messages.Add(new ModelMessage("assistant", completion.Text, completion.ToolCalls)); + foreach (var call in completion.ToolCalls) + { + byName.TryGetValue(call.Name, out var descriptor); + var toolResult = descriptor is null + ? new McpToolResult(false, null, $"Unknown tool '{call.Name}'.") + : await mcpGateway.CallToolAsync(context.OrganizationId, descriptor.ServerId, call.Name, call.ArgumentsJson, cancellationToken); + + var content = toolResult.Success ? toolResult.Content ?? string.Empty : $"ERROR: {toolResult.Error}"; + messages.Add(new ModelMessage("tool", content, ToolCallId: call.Id)); + trace.Add(new { tool = call.Name, server = descriptor?.ServerName, ok = toolResult.Success }); + logger.LogInformation("Run {RunId} tool call {Tool} → {Ok}.", context.AgentId, call.Name, toolResult.Success); + } + } + + return (completion, completion.Text ?? string.Empty, trace); + } } diff --git a/src/Modules/TeamUp.Modules.Integrations/Ai/ModelClients.cs b/src/Modules/TeamUp.Modules.Integrations/Ai/ModelClients.cs index a1eaea8..c312474 100644 --- a/src/Modules/TeamUp.Modules.Integrations/Ai/ModelClients.cs +++ b/src/Modules/TeamUp.Modules.Integrations/Ai/ModelClients.cs @@ -16,9 +16,32 @@ internal sealed class StubModelClient : IModelClient LatencyMs: 0)); } +/// +/// Deterministic adapter for the "tooluse" provider, used to exercise the MCP tool-use loop without a +/// real model: when tools are offered and no tool result is in the conversation yet, it asks to call +/// the first tool; once a tool result is present, it produces a final answer. +/// +internal sealed class ToolUseStubModelClient : IModelClient +{ + public Task CompleteAsync(ModelRequest request, CancellationToken cancellationToken = default) + { + var hasToolResult = request.Messages?.Any(m => m.Role == "tool") ?? false; + if (!hasToolResult && request.Tools is { Count: > 0 }) + { + var tool = request.Tools[0]; + return Task.FromResult(new ModelCompletion(true, null, null, 0, [new ModelToolCall("call_1", tool.Name, "{}")])); + } + + var prompt = request.Messages?.FirstOrDefault(m => m.Role == "user")?.Content ?? request.Prompt; + return Task.FromResult(new ModelCompletion(true, $"[tooluse {request.Model}] {prompt}", null, 0)); + } +} + /// /// OpenAI-compatible /v1/chat/completions adapter (OpenAI, Ollama, vLLM, and OpenAI-compatible -/// gateways). Returns a failed completion rather than throwing, so the connection test can report it. +/// gateways). Supports the tool-use loop: it forwards a conversation () +/// and tool definitions, and parses any tool calls out of the response. Returns a failed completion +/// rather than throwing, so the connection test can report it. /// internal sealed class OpenAiCompatibleModelClient(HttpClient http) : IModelClient { @@ -34,12 +57,20 @@ internal sealed class OpenAiCompatibleModelClient(HttpClient http) : IModelClien message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", request.ApiKey); } - message.Content = JsonContent.Create(new + var body = new Dictionary { - model = request.Model, - max_tokens = request.MaxTokens, - messages = new[] { new { role = "user", content = request.Prompt } }, - }); + ["model"] = request.Model, + ["max_tokens"] = request.MaxTokens, + ["messages"] = BuildMessages(request), + }; + if (request.Tools is { Count: > 0 }) + { + body["tools"] = request.Tools + .Select(t => new { type = "function", function = new { name = t.Name, description = t.Description, parameters = ParseSchema(t.ParametersJson) } }) + .ToArray(); + } + + message.Content = JsonContent.Create(body); using var response = await http.SendAsync(message, cancellationToken); stopwatch.Stop(); @@ -49,8 +80,28 @@ internal sealed class OpenAiCompatibleModelClient(HttpClient http) : IModelClien } var doc = await response.Content.ReadFromJsonAsync(cancellationToken); - var text = doc.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString(); - return new ModelCompletion(true, text, null, stopwatch.ElapsedMilliseconds); + var msg = doc.GetProperty("choices")[0].GetProperty("message"); + var text = msg.TryGetProperty("content", out var content) && content.ValueKind == JsonValueKind.String + ? content.GetString() + : null; + + List? toolCalls = null; + if (msg.TryGetProperty("tool_calls", out var calls) && calls.ValueKind == JsonValueKind.Array && calls.GetArrayLength() > 0) + { + toolCalls = []; + foreach (var call in calls.EnumerateArray()) + { + var id = call.TryGetProperty("id", out var idEl) ? idEl.GetString() ?? string.Empty : string.Empty; + var fn = call.GetProperty("function"); + var name = fn.GetProperty("name").GetString() ?? string.Empty; + var args = fn.TryGetProperty("arguments", out var a) + ? (a.ValueKind == JsonValueKind.String ? a.GetString() ?? "{}" : a.GetRawText()) + : "{}"; + toolCalls.Add(new ModelToolCall(id, name, args)); + } + } + + return new ModelCompletion(true, text, null, stopwatch.ElapsedMilliseconds, toolCalls); } catch (Exception ex) { @@ -58,15 +109,57 @@ internal sealed class OpenAiCompatibleModelClient(HttpClient http) : IModelClien return new ModelCompletion(false, null, ex.Message, stopwatch.ElapsedMilliseconds); } } + + private static object[] BuildMessages(ModelRequest request) + { + if (request.Messages is not { Count: > 0 }) + { + return [new { role = "user", content = request.Prompt }]; + } + + return request.Messages.Select(object (m) => m.Role switch + { + "tool" => new { role = "tool", tool_call_id = m.ToolCallId, content = m.Content ?? string.Empty }, + _ when m.ToolCalls is { Count: > 0 } => new + { + role = m.Role, + content = m.Content, + tool_calls = m.ToolCalls + .Select(tc => new { id = tc.Id, type = "function", function = new { name = tc.Name, arguments = tc.ArgumentsJson } }) + .ToArray(), + }, + _ => new { role = m.Role, content = m.Content ?? string.Empty }, + }).ToArray(); + } + + private static JsonElement ParseSchema(string json) + { + if (!string.IsNullOrWhiteSpace(json)) + { + try + { + using var parsed = JsonDocument.Parse(json); + return parsed.RootElement.Clone(); + } + catch (JsonException) + { + // Fall through to a permissive default. + } + } + + using var fallback = JsonDocument.Parse("""{"type":"object"}"""); + return fallback.RootElement.Clone(); + } } /// Routes a request to the adapter for its provider. -internal sealed class ModelClientRouter(StubModelClient stub, OpenAiCompatibleModelClient openAi) : IModelClient +internal sealed class ModelClientRouter(StubModelClient stub, ToolUseStubModelClient toolUse, OpenAiCompatibleModelClient openAi) : IModelClient { public Task CompleteAsync(ModelRequest request, CancellationToken cancellationToken = default) => request.Provider.ToLowerInvariant() switch { "stub" or "echo" or "test" => stub.CompleteAsync(request, cancellationToken), + "tooluse" => toolUse.CompleteAsync(request, cancellationToken), _ => openAi.CompleteAsync(request, cancellationToken), }; } diff --git a/src/Modules/TeamUp.Modules.Integrations/IntegrationsModule.cs b/src/Modules/TeamUp.Modules.Integrations/IntegrationsModule.cs index 07f3ada..87199eb 100644 --- a/src/Modules/TeamUp.Modules.Integrations/IntegrationsModule.cs +++ b/src/Modules/TeamUp.Modules.Integrations/IntegrationsModule.cs @@ -38,6 +38,7 @@ public sealed class IntegrationsModule : IModule // Model clients: a router over per-provider adapters. services.AddSingleton(); + services.AddSingleton(); services.AddHttpClient(); services.AddScoped(); diff --git a/src/Shared/TeamUp.SharedKernel/Ai/IModelClient.cs b/src/Shared/TeamUp.SharedKernel/Ai/IModelClient.cs index 964c965..820a935 100644 --- a/src/Shared/TeamUp.SharedKernel/Ai/IModelClient.cs +++ b/src/Shared/TeamUp.SharedKernel/Ai/IModelClient.cs @@ -1,15 +1,42 @@ namespace TeamUp.SharedKernel.Ai; -/// One model invocation. The key is passed explicitly (BYOK, server-side only). +/// A tool the model may call (OpenAI "function" tool). Parameters is a JSON-Schema string. +public sealed record ModelTool(string Name, string? Description, string ParametersJson); + +/// A tool call the model asked for, to be executed and fed back. +public sealed record ModelToolCall(string Id, string Name, string ArgumentsJson); + +/// +/// One message in a tool-use conversation. Role is user|assistant|tool. An assistant turn may carry +/// ; a tool turn carries the result for . +/// +public sealed record ModelMessage( + string Role, + string? Content, + IReadOnlyList? ToolCalls = null, + string? ToolCallId = null); + +/// +/// One model invocation. The key is passed explicitly (BYOK, server-side only). When +/// is set it is the full conversation (for the tool-use loop) and overrides +/// ; offers callable tools. +/// public sealed record ModelRequest( string Provider, string Model, string ApiKey, string? Endpoint, string Prompt, - int MaxTokens = 256); + int MaxTokens = 256, + IReadOnlyList? Tools = null, + IReadOnlyList? Messages = null); -public sealed record ModelCompletion(bool Success, string? Text, string? Error, long LatencyMs); +public sealed record ModelCompletion( + bool Success, + string? Text, + string? Error, + long LatencyMs, + IReadOnlyList? ToolCalls = null); /// /// Provider-agnostic model client. Implemented in Integrations (a router over per-provider HTTP diff --git a/tests/TeamUp.IntegrationTests/ModelClientToolTests.cs b/tests/TeamUp.IntegrationTests/ModelClientToolTests.cs new file mode 100644 index 0000000..1537df3 --- /dev/null +++ b/tests/TeamUp.IntegrationTests/ModelClientToolTests.cs @@ -0,0 +1,94 @@ +using System.Net; +using System.Text; +using TeamUp.Modules.Integrations.Ai; +using TeamUp.SharedKernel.Ai; +using Xunit; + +namespace TeamUp.IntegrationTests; + +/// +/// The model-client tool-use plumbing: the OpenAI-compatible adapter serializes tools + a tool-use +/// conversation and parses tool calls out of the reply; the deterministic "tooluse" stub drives the +/// loop (ask for a tool, then answer once a result is present). +/// +public sealed class ModelClientToolTests +{ + [Fact] + public async Task OpenAi_adapter_sends_tools_and_parses_tool_calls() + { + const string reply = + """{"choices":[{"message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"search_issues","arguments":"{\"q\":\"bug\"}"}}]}}]}"""; + var handler = new CapturingHandler(reply); + var client = new OpenAiCompatibleModelClient(new HttpClient(handler)); + + var request = new ModelRequest( + "openai", "gpt-4o", "sk-test", null, "find bugs", MaxTokens: 512, + Tools: [new ModelTool("search_issues", "Search the tracker.", """{"type":"object"}""")], + Messages: [new ModelMessage("user", "find bugs")]); + + var completion = await client.CompleteAsync(request); + + Assert.True(completion.Success); + var call = Assert.Single(completion.ToolCalls!); + Assert.Equal("call_abc", call.Id); + Assert.Equal("search_issues", call.Name); + Assert.Contains("bug", call.ArgumentsJson); + + // The outgoing body carries the tool definition and the conversation. + Assert.Contains("\"tools\"", handler.LastBody); + Assert.Contains("search_issues", handler.LastBody); + Assert.Contains("find bugs", handler.LastBody); + } + + [Fact] + public async Task OpenAi_adapter_returns_plain_text_when_no_tool_calls() + { + const string reply = """{"choices":[{"message":{"role":"assistant","content":"Here are the bugs."}}]}"""; + var client = new OpenAiCompatibleModelClient(new HttpClient(new CapturingHandler(reply))); + + var completion = await client.CompleteAsync(new ModelRequest("openai", "gpt-4o", "sk", null, "hi")); + + Assert.True(completion.Success); + Assert.Equal("Here are the bugs.", completion.Text); + Assert.Null(completion.ToolCalls); + } + + [Fact] + public async Task ToolUse_stub_asks_for_a_tool_then_answers_once_a_result_is_present() + { + var stub = new ToolUseStubModelClient(); + List tools = [new ModelTool("lookup", null, "{}")]; + + var first = await stub.CompleteAsync(new ModelRequest( + "tooluse", "m", "", null, "do it", Tools: tools, Messages: [new ModelMessage("user", "do it")])); + var toolCall = Assert.Single(first.ToolCalls!); + Assert.Equal("lookup", toolCall.Name); + Assert.Null(first.Text); + + var second = await stub.CompleteAsync(new ModelRequest( + "tooluse", "m", "", null, "do it", Tools: tools, + Messages: + [ + new ModelMessage("user", "do it"), + new ModelMessage("assistant", null, first.ToolCalls), + new ModelMessage("tool", "the result", ToolCallId: toolCall.Id), + ])); + + Assert.Null(second.ToolCalls); + Assert.Contains("do it", second.Text!); + } + + private sealed class CapturingHandler(string responseJson) : HttpMessageHandler + { + public string LastBody { get; private set; } = string.Empty; + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + LastBody = await request.Content!.ReadAsStringAsync(cancellationToken); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseJson, Encoding.UTF8, "application/json"), + }; + } + } +} From d50cd2790e55c75699666c9d46796d5708424fba Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Mon, 15 Jun 2026 15:21:10 +0330 Subject: [PATCH 02/11] Animated agent faces driven by live run state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each AI agent now has an expressive Companion face (AgentFace) whose animation maps to its real AgentRun state — idle, thinking (queued), working (running), review (held), done, failed — so a glance at the board or org chart reads as live status, the same way the seat-state triad reads human/open/AI. Pure CSS keyframes (no animation dependency), em-scaled across four sizes, per-agent hue derived deterministically in the indigo band, reduced-motion respected. Adds a per-team agent-activity read endpoint (latest run status per agent) and a self-contained polling hook (useAgentActivity) that merges run activity with governance holds. Wired into the board assignee chips and the org chart (a custom React Flow seat node with hidden handles so edges still connect). Co-Authored-By: Claude Opus 4.8 --- client/src/components/AgentFace.tsx | 69 +++++++++ client/src/components/agent-face.css | 146 ++++++++++++++++++ client/src/lib/useAgentActivity.ts | 91 +++++++++++ client/src/pages/BoardPage.tsx | 12 +- client/src/pages/OrgChartPage.tsx | 77 +++++++-- .../Endpoints/AssemblerDtos.cs | 6 + .../Endpoints/AssemblerEndpoints.cs | 47 ++++++ 7 files changed, 433 insertions(+), 15 deletions(-) create mode 100644 client/src/components/AgentFace.tsx create mode 100644 client/src/components/agent-face.css create mode 100644 client/src/lib/useAgentActivity.ts diff --git a/client/src/components/AgentFace.tsx b/client/src/components/AgentFace.tsx new file mode 100644 index 0000000..d877179 --- /dev/null +++ b/client/src/components/AgentFace.tsx @@ -0,0 +1,69 @@ +import { cn } from '@/lib/utils' +import './agent-face.css' + +/** + * The live state of an agent, mapped from its latest AgentRun (+ governance hold) onto an expression. + * `idle` = nothing in flight; `thinking` = queued; `working` = running; `review` = output held in the + * inbox; `done` = just completed & executed; `failed` = the run errored. + */ +export type FaceState = 'idle' | 'thinking' | 'working' | 'review' | 'done' | 'failed' + +export type FaceSize = 'sm' | 'md' | 'lg' | 'xl' + +interface AgentFaceProps { + name?: string | null + /** Used only to seed the per-agent hue and the accessible label — never drawn on the face. */ + monogram?: string | null + state?: FaceState + size?: FaceSize + className?: string +} + +const STATE_LABEL: Record = { + idle: 'idle', + thinking: 'queued', + working: 'working', + review: 'awaiting review', + done: 'done', + failed: 'failed', +} + +/** + * Deterministic hue in the indigo–violet band [225, 265] so every agent is distinct yet stays inside + * the AI = indigo identity. Seeded by the agent's monogram/name so it is stable across renders and + * needs no stored field. + */ +function hueFor(seed: string): number { + let h = 0 + for (let i = 0; i < seed.length; i += 1) h = (h * 31 + seed.charCodeAt(i)) >>> 0 + return 225 + (h % 41) +} + +/** The expressive Companion face. One component, every surface — sized by `size`, animated by `state`. */ +export function AgentFace({ name, monogram, state = 'idle', size = 'md', className }: AgentFaceProps) { + const hue = hueFor((monogram || name || 'agent').trim().toLowerCase()) + const label = `${name ?? 'AI agent'} — ${STATE_LABEL[state]}` + + return ( + + + + + + + + + + ) +} diff --git a/client/src/components/agent-face.css b/client/src/components/agent-face.css new file mode 100644 index 0000000..7cf3212 --- /dev/null +++ b/client/src/components/agent-face.css @@ -0,0 +1,146 @@ +/* + * The Companion agent face. One expressive face used at every size; the animation is load-bearing — + * it maps to a real AgentRun state (queued/running/held/completed/failed) so a glance reads as live + * status, the same way the seat-state triad reads human/open/AI. All metrics are in `em` and the size + * classes set the root font-size, so the whole face scales from a board chip to the configurator. + */ +.agent-face { + position: relative; + display: inline-block; + width: 6em; + height: 6em; + flex: none; + line-height: 0; + --rc: #64748b; /* state ring colour, overridden per state */ + --hue: 245; +} +.agent-face.af-sm { font-size: 3.3px; } +.agent-face.af-md { font-size: 7.3px; } +.agent-face.af-lg { font-size: 14px; } +.agent-face.af-xl { font-size: 20px; } + +.af-head { + position: absolute; + inset: 0; + border-radius: 30%; + background: hsl(var(--hue) 62% 62%); + animation: af-breathe 3.4s ease-in-out infinite; +} +.af-ring { + position: absolute; + inset: -0.55em; + border-radius: 32%; + border: 0.18em solid var(--rc); + opacity: 0.85; + transition: border-color 0.35s ease, opacity 0.35s ease; +} +.af-spin { + position: absolute; + inset: -0.55em; + border-radius: 32%; + border: 0.18em solid transparent; + border-top-color: var(--rc); + opacity: 0; +} +.af-eye { + position: absolute; + top: 0.42em; + width: 0.13em; + height: 0.13em; + width: 0.8em; + height: 0.8em; + background: #fff; + border-radius: 50%; + animation: af-blink 4s infinite; +} +.af-eye-l { left: 0.27em; } +.af-eye-r { right: 0.27em; } +.af-mouth { + position: absolute; + bottom: 0.24em; + left: 50%; + transform: translateX(-50%); + width: 1.15em; + height: 0.2em; + border-radius: 0.2em; + background: rgba(255, 255, 255, 0.85); +} +.af-dots { + position: absolute; + top: -0.15em; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 0.22em; + opacity: 0; +} +.af-dots i { + width: 0.36em; + height: 0.36em; + border-radius: 50%; + background: #6366f1; + animation: af-bob 0.9s infinite; +} +.af-dots i:nth-child(2) { animation-delay: 0.15s; } +.af-dots i:nth-child(3) { animation-delay: 0.3s; } + +/* The mouth and thinking-dots are clutter at chip size — eyes + ring carry the state there. */ +.af-sm .af-mouth, +.af-sm .af-dots { display: none; } + +/* ---- state: ring colour ---- */ +.agent-face[data-state='idle'] { --rc: #64748b; } +.agent-face[data-state='thinking'] { --rc: #6366f1; } +.agent-face[data-state='working'] { --rc: #6366f1; } +.agent-face[data-state='review'] { --rc: #f59e0b; } +.agent-face[data-state='done'] { --rc: #14b8a6; } +.agent-face[data-state='failed'] { --rc: #ef4444; } + +/* ---- state: expression ---- */ +.agent-face[data-state='thinking'] .af-eye { top: 0.36em; height: 0.5em; border-radius: 40%; } +.agent-face[data-state='thinking'] .af-dots { opacity: 1; } +.agent-face[data-state='thinking'] .af-ring { animation: af-rpulse 1.4s ease-in-out infinite; } + +.agent-face[data-state='working'] .af-eye { height: 0.92em; top: 0.4em; } +.agent-face[data-state='working'] .af-mouth { width: 0.6em; } +.agent-face[data-state='working'] .af-spin { opacity: 1; animation: af-spin 1.05s linear infinite; } +.agent-face[data-state='working'] .af-ring { opacity: 0.3; } + +.agent-face[data-state='review'] .af-ring { animation: af-rpulse 1s ease-in-out infinite; } +.agent-face[data-state='review'] .af-eye { top: 0.34em; } + +.agent-face[data-state='done'] .af-eye { + height: 0.42em; + border-radius: 0 0 0.8em 0.8em; + top: 0.5em; +} +.agent-face[data-state='done'] .af-mouth { + width: 1.4em; + height: 0.62em; + border-radius: 0 0 1.4em 1.4em; + border-bottom: 0.2em solid #fff; + background: transparent; +} +.agent-face[data-state='done'] .af-ring { animation: af-pop 0.5s ease-out; } + +.agent-face[data-state='failed'] .af-head { background: hsl(var(--hue) 14% 56%); } +.agent-face[data-state='failed'] .af-eye { height: 0.28em; border-radius: 0.14em; top: 0.56em; background: #e6e0ef; } +.agent-face[data-state='failed'] .af-mouth { + width: 0.85em; + height: 0.55em; + border-radius: 1.4em 1.4em 0 0; + border-top: 0.2em solid #e6e0ef; + background: transparent; + bottom: 0.2em; +} + +@media (prefers-reduced-motion: reduce) { + .af-head, .af-ring, .af-spin, .af-eye, .af-dots i { animation: none !important; } +} + +@keyframes af-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.045); } } +@keyframes af-blink { 0%, 92%, 100% { transform: scaleY(1); } 96% { transform: scaleY(0.1); } } +@keyframes af-spin { to { transform: rotate(360deg); } } +@keyframes af-rpulse { 0%, 100% { opacity: 0.85; } 50% { opacity: 0.3; } } +@keyframes af-pop { 0% { transform: scale(0.8); } 60% { transform: scale(1.12); } 100% { transform: scale(1); } } +@keyframes af-bob { 0%, 100% { transform: translateY(0); opacity: 0.5; } 50% { transform: translateY(-0.3em); opacity: 1; } } diff --git a/client/src/lib/useAgentActivity.ts b/client/src/lib/useAgentActivity.ts new file mode 100644 index 0000000..997d9dc --- /dev/null +++ b/client/src/lib/useAgentActivity.ts @@ -0,0 +1,91 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { api } from '@/lib/api' +import type { FaceState } from '@/components/AgentFace' + +interface AgentActivity { + agentId: string + status: string + workItemId: string + updatedAtUtc: string +} + +interface PendingReview { + agentId: string +} + +/** A just-completed run shows the `done` (teal) face for this long, then settles to `idle`. */ +const DONE_WINDOW_MS = 45_000 +const POLL_MS = 4_000 + +function faceFor(activity: AgentActivity | undefined, held: boolean): FaceState { + if (held) return 'review' + if (!activity) return 'idle' + switch (activity.status) { + case 'Failed': + return 'failed' + case 'Running': + return 'working' + case 'Queued': + return 'thinking' + case 'Completed': { + const age = Date.now() - new Date(activity.updatedAtUtc).getTime() + return age >= 0 && age < DONE_WINDOW_MS ? 'done' : 'idle' + } + default: + return 'idle' + } +} + +/** + * Polls per-agent run activity (Assembler) and pending holds (Governance) and maps each agent to a + * live face state. Self-contained polling — no query client needed. Pass the agent ids currently on + * screen (the caller already holds them via its seats); an empty list disables the poll. + */ +export function useAgentActivity(organizationId: string | null, agentIds: (string | null | undefined)[]) { + const ids = agentIds.filter((x): x is string => !!x) + const key = [...new Set(ids)].sort().join(',') + + const [activity, setActivity] = useState>({}) + const [held, setHeld] = useState>(new Set()) + const keyRef = useRef(key) + keyRef.current = key + + useEffect(() => { + if (!key) { + setActivity({}) + setHeld(new Set()) + return + } + + let cancelled = false + const tick = async () => { + try { + const [runs, reviews] = await Promise.all([ + api.get(`/api/assembler/agent-activity?agentIds=${encodeURIComponent(key)}`), + organizationId + ? api.get(`/api/governance/reviews?organizationId=${organizationId}&status=Pending`) + : Promise.resolve([] as PendingReview[]), + ]) + if (cancelled) return + setActivity(Object.fromEntries(runs.map((r) => [r.agentId, r]))) + setHeld(new Set(reviews.map((r) => r.agentId))) + } catch { + // Keep the last known state on a transient failure — the face just stops updating briefly. + } + } + + void tick() + const timer = setInterval(tick, POLL_MS) + return () => { + cancelled = true + clearInterval(timer) + } + // `key` captures the set of agent ids; re-poll when it or the org changes. + }, [key, organizationId]) + + return useCallback( + (agentId?: string | null): FaceState => + agentId ? faceFor(activity[agentId], held.has(agentId)) : 'idle', + [activity, held], + ) +} diff --git a/client/src/pages/BoardPage.tsx b/client/src/pages/BoardPage.tsx index 7b3c872..130a042 100644 --- a/client/src/pages/BoardPage.tsx +++ b/client/src/pages/BoardPage.tsx @@ -31,7 +31,9 @@ import { SelectValue, } from '@/components/ui/select' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' +import { AgentFace, type FaceState } from '@/components/AgentFace' import { api } from '@/lib/api' +import { useAgentActivity } from '@/lib/useAgentActivity' import { useMembers, useSeats, type MemberRow, type SeatRow } from '@/lib/useDirectory' import { useAuth } from '@/store/auth' @@ -79,6 +81,7 @@ export function BoardPage() { const members = useMembers(organizationId) const seats = useSeats(teamId) + const agentState = useAgentActivity(organizationId, seats.map((s) => s.agentId)) const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })) @@ -244,6 +247,7 @@ export function BoardPage() { memberId={memberId} members={members} seats={seats} + agentState={agentState} onOpen={() => setOpenTaskId(task.id)} /> ))} @@ -298,12 +302,14 @@ function DraggableCard({ memberId, members, seats, + agentState, onOpen, }: { task: Task memberId: string | null members: MemberRow[] seats: SeatRow[] + agentState: (agentId?: string | null) => FaceState onOpen: () => void }) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: task.id }) @@ -324,7 +330,7 @@ function DraggableCard({ {task.title} {task.type} - + @@ -337,17 +343,19 @@ function AssigneeChip({ memberId, members, seats, + agentState, }: { task: Task memberId: string | null members: MemberRow[] seats: SeatRow[] + agentState: (agentId?: string | null) => FaceState }) { if (task.assigneeKind === 'Agent') { const seat = seats.find((s) => s.agentId === task.assigneeId) return ( - AI + {seat?.roleName ?? 'AI seat'} ) diff --git a/client/src/pages/OrgChartPage.tsx b/client/src/pages/OrgChartPage.tsx index 0c928e4..9fc6b63 100644 --- a/client/src/pages/OrgChartPage.tsx +++ b/client/src/pages/OrgChartPage.tsx @@ -1,12 +1,59 @@ import { useEffect, useMemo, useState } from 'react' -import { Background, ReactFlow, type Edge, type Node } from '@xyflow/react' +import { Background, Handle, Position, ReactFlow, type Edge, type Node, type NodeProps } from '@xyflow/react' import '@xyflow/react/dist/style.css' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' +import { AgentFace, type FaceState } from '@/components/AgentFace' import { api } from '@/lib/api' +import { useAgentActivity } from '@/lib/useAgentActivity' import { useAuth } from '@/store/auth' import type { SeatRow } from '@/lib/useDirectory' +interface SeatNodeData { + roleName: string + seatState: string + isAi: boolean + faceState: FaceState + [key: string]: unknown +} + +const SEAT_BG: Record = { Ai: '#4f46e5', Human: '#475569', Open: '#d97706' } + +/** A seat in the org chart. AI seats wear their live face; the triad colour stays load-bearing. */ +function SeatNode({ data }: NodeProps) { + const d = data as SeatNodeData + return ( +
+ + {d.isAi ? ( + + ) : ( + + )} + + + {d.roleName} + + {d.isAi ? d.faceState : d.seatState} + + +
+ ) +} + +const nodeTypes = { seat: SeatNode } + interface Division { id: string name: string @@ -66,9 +113,14 @@ export function OrgChartPage() { })() }, [organizationId]) + const agentState = useAgentActivity( + organizationId, + Object.values(seatsByTeam).flat().map((s) => s.agentId), + ) + const { nodes, edges } = useMemo( - () => buildGraph(divisions, products, teams, seatsByTeam), - [divisions, products, teams, seatsByTeam], + () => buildGraph(divisions, products, teams, seatsByTeam, agentState), + [divisions, products, teams, seatsByTeam, agentState], ) return ( @@ -83,7 +135,7 @@ export function OrgChartPage() {

- +
@@ -97,6 +149,7 @@ function buildGraph( products: Product[], teams: Team[], seatsByTeam: Record, + agentStateFor: (agentId?: string | null) => FaceState, ): { nodes: Node[]; edges: Edge[] } { const nodes: Node[] = [] const edges: Edge[] = [] @@ -141,18 +194,16 @@ function buildGraph( const seats = seatsByTeam[team.id] ?? [] seats.forEach((seat, seatIndex) => { - const color = seat.state === 'Ai' ? '#4f46e5' : seat.state === 'Human' ? '#475569' : '#d97706' + const isAi = seat.state === 'Ai' nodes.push({ id: seat.id, + type: 'seat', position: { x: x + 10, y: seatY + seatIndex * SEAT_HEIGHT }, - data: { label: `${seat.roleName} · ${seat.state === 'Ai' ? 'AI' : seat.state}` }, - style: { - background: color, - color: 'white', - borderRadius: 8, - border: 'none', - fontSize: 12, - width: 180, + data: { + roleName: seat.roleName, + seatState: seat.state, + isAi, + faceState: isAi ? agentStateFor(seat.agentId) : 'idle', }, }) edges.push({ id: `${team.id}-${seat.id}`, source: team.id, target: seat.id }) diff --git a/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerDtos.cs b/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerDtos.cs index 087f5d5..9a8148e 100644 --- a/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerDtos.cs +++ b/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerDtos.cs @@ -13,3 +13,9 @@ internal sealed record RunResponse( string? Prompt, string? Output, string? Error); + +internal sealed record AgentActivityResponse( + Guid AgentId, + string Status, + Guid WorkItemId, + DateTimeOffset UpdatedAtUtc); diff --git a/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerEndpoints.cs b/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerEndpoints.cs index 988660b..f2eec8a 100644 --- a/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerEndpoints.cs +++ b/src/Modules/TeamUp.Modules.Assembler/Endpoints/AssemblerEndpoints.cs @@ -18,6 +18,53 @@ internal static class AssemblerEndpoints group.MapGet("/ping", () => TypedResults.Ok(new ModulePing("assembler"))); group.MapPost("/runs", CreateRun).RequireAuthorization(); group.MapGet("/runs/{id:guid}", GetRun).RequireAuthorization(); + group.MapGet("/agent-activity", GetAgentActivity).RequireAuthorization(); + } + + // The live pulse behind each agent's face: the latest run status per agent. The client passes the + // ids of the AI seats it is showing (it already holds them) and composes the on-screen face state — + // this keeps the module boundary clean (Assembler owns runs; it never reaches into seats/teams). + private static async Task GetAgentActivity( + string? agentIds, AssemblerDbContext db, CancellationToken ct) + { + var ids = (agentIds ?? string.Empty) + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(s => Guid.TryParse(s, out var g) ? g : (Guid?)null) + .Where(g => g.HasValue) + .Select(g => g!.Value) + .Distinct() + .ToList(); + + if (ids.Count == 0) + { + return Results.Ok(Array.Empty()); + } + + // Latest run per agent. Project the few columns we need, then pick the newest per agent in + // memory — at dogfood scale this is a small set and avoids brittle GroupBy translation. + var runs = await db.AgentRuns + .Where(r => r.AgentId != null && ids.Contains(r.AgentId!.Value)) + .Select(r => new + { + AgentId = r.AgentId!.Value, + r.Status, + r.WorkItemId, + r.CreatedAtUtc, + r.CompletedAtUtc, + }) + .ToListAsync(ct); + + var activity = runs + .GroupBy(r => r.AgentId) + .Select(g => g.OrderByDescending(r => r.CreatedAtUtc).First()) + .Select(r => new AgentActivityResponse( + r.AgentId, + r.Status.ToString(), + r.WorkItemId, + r.CompletedAtUtc ?? r.CreatedAtUtc)) + .ToList(); + + return Results.Ok(activity); } // Dispatch a task to an AI seat: record a queued AgentRun and enqueue the job. The worker From 4758e4b5deacba2f37c3896e4265609c7290b58c Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Mon, 15 Jun 2026 15:26:14 +0330 Subject: [PATCH 03/11] Markdown Edit/Preview tabs + read-only .md viewer for skills & profiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds MarkdownEditor (react-markdown + remark-gfm, no raw HTML — authored/retrieved content is data, not markup) with Edit | Preview tabs, wired into the AGENTS.md and SKILL.md editors, the agent persona, and the review artifact. Adds a read-only "View" on every skill and agent-profile card — including builtins, which previously had no way to be inspected at all — rendering the full SKILL.md / AGENTS.md (frontmatter + body + actions/golden tests). Collapses a same-version builtin that an org has forked so its own copy shadows it, keeping the version picker unambiguous and the item clearly editable/versionable. Also lands the agent-face wiring on the seat configurator (a live xl preview with a state cycler) and the review inbox header. Co-Authored-By: Claude Opus 4.8 --- client/package-lock.json | 1460 +++++++++++++++++++++- client/package.json | 2 + client/src/components/MarkdownEditor.tsx | 99 ++ client/src/components/markdown.css | 114 ++ client/src/pages/AgentProfilesPage.tsx | 55 +- client/src/pages/ReviewsPage.tsx | 14 +- client/src/pages/SeatsPage.tsx | 38 +- client/src/pages/SkillsPage.tsx | 79 +- 8 files changed, 1841 insertions(+), 20 deletions(-) create mode 100644 client/src/components/MarkdownEditor.tsx create mode 100644 client/src/components/markdown.css diff --git a/client/package-lock.json b/client/package-lock.json index f91a58d..48c71fd 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22,8 +22,10 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "react-hook-form": "^7.78.0", + "react-markdown": "^10.1.0", "react-router": "^7.17.0", "recharts": "^3.8.1", + "remark-gfm": "^4.0.1", "shadcn": "^4.11.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", @@ -4023,6 +4025,15 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -4034,9 +4045,26 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4044,6 +4072,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.1.tgz", @@ -4058,7 +4101,6 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4074,6 +4116,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -4329,6 +4377,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -4569,6 +4623,16 @@ "node": ">=4" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -4753,6 +4817,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -4765,6 +4839,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -4825,6 +4939,16 @@ "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", @@ -4957,7 +5081,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { @@ -5179,6 +5302,19 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dedent": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", @@ -5258,6 +5394,15 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5274,6 +5419,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", @@ -5693,6 +5851,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5835,6 +6003,12 @@ "node": ">= 0.6" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6284,6 +6458,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -6310,6 +6524,16 @@ "node": ">=16.9.0" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -6419,6 +6643,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -6446,12 +6676,46 @@ "node": ">= 0.10" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -6488,6 +6752,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-in-ssh": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", @@ -7086,6 +7360,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7114,6 +7398,16 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7123,6 +7417,288 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.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.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -7159,6 +7735,569 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -7577,6 +8716,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -7778,6 +8942,16 @@ "node": ">=6" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7981,6 +9155,33 @@ "license": "MIT", "peer": true }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", @@ -8166,6 +9367,72 @@ "redux": "^5.0.0" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -8582,6 +9849,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -8647,6 +9924,20 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stringify-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz", @@ -8697,6 +9988,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/tailwind-merge": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", @@ -8772,6 +10081,26 @@ "node": ">=0.6" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -8925,6 +10254,93 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -9059,6 +10475,34 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/victory-vendor": { "version": "37.3.6", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", @@ -9317,6 +10761,16 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/client/package.json b/client/package.json index df3d60b..00b68f2 100644 --- a/client/package.json +++ b/client/package.json @@ -24,8 +24,10 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "react-hook-form": "^7.78.0", + "react-markdown": "^10.1.0", "react-router": "^7.17.0", "recharts": "^3.8.1", + "remark-gfm": "^4.0.1", "shadcn": "^4.11.0", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", diff --git a/client/src/components/MarkdownEditor.tsx b/client/src/components/MarkdownEditor.tsx new file mode 100644 index 0000000..72dee81 --- /dev/null +++ b/client/src/components/MarkdownEditor.tsx @@ -0,0 +1,99 @@ +import { useState } from 'react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { Textarea } from '@/components/ui/textarea' +import { cn } from '@/lib/utils' +import './markdown.css' + +interface MarkdownEditorProps { + value: string + onChange?: (value: string) => void + rows?: number + /** Monospace editing font — use for raw .md files (AGENTS.md / SKILL.md). */ + mono?: boolean + /** Split a leading YAML frontmatter block and show it above the rendered body in the preview. */ + frontmatter?: boolean + placeholder?: string + id?: string + /** Which tab to open on first render. Defaults to Preview when read-only (no onChange), else Edit. */ + defaultTab?: Tab + /** Extra classes for the textarea (edit tab). */ + className?: string +} + +type Tab = 'edit' | 'preview' + +/** Strips a leading `---\n…\n---` frontmatter block so the preview can render the body as prose. */ +function splitFrontmatter(src: string): { fm: string | null; body: string } { + const match = src.match(/^---\n([\s\S]*?)\n---\n?/) + return match ? { fm: match[1], body: src.slice(match[0].length) } : { fm: null, body: src } +} + +/** + * A markdown field with Edit | Preview tabs. Edit is the familiar textarea; Preview renders + * GitHub-flavored markdown (react-markdown + remark-gfm, no raw HTML — retrieved/authored content is + * data, not markup). Used wherever the app authors markdown: AGENTS.md, SKILL.md, agent persona, and + * the review artifact. + */ +export function MarkdownEditor({ + value, + onChange, + rows = 8, + mono = false, + frontmatter = false, + placeholder, + id, + defaultTab, + className, +}: MarkdownEditorProps) { + const [tab, setTab] = useState(defaultTab ?? (onChange ? 'edit' : 'preview')) + const { fm, body } = frontmatter ? splitFrontmatter(value) : { fm: null, body: value } + const hasContent = (frontmatter ? body : value).trim().length > 0 + + return ( +
+
+ {(['edit', 'preview'] as Tab[]).map((t) => ( + + ))} +
+ + {tab === 'edit' ? ( +