diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..1082ffd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "AI Agent Web App", + "image": "mcr.microsoft.com/devcontainers/dotnet:10.0", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "22" + }, + "ghcr.io/azure/azure-dev/azd:latest": {}, + "ghcr.io/devcontainers/features/azure-cli:1": {}, + "ghcr.io/devcontainers/features/powershell:1": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csdevkit", + "ms-azuretools.azure-dev", + "dbaeumer.vscode-eslint", + "GitHub.copilot", + "GitHub.copilot-chat" + ], + "settings": { + "dotnetAcquisition.version": "10.0" + } + } + }, + "forwardPorts": [5173, 8080], + "postCreateCommand": "dotnet restore backend/WebApp.Api/WebApp.Api.csproj && cd frontend && npm install --legacy-peer-deps", + "remoteUser": "vscode" +} diff --git a/.github/agents/WebAppAgent.agent.md b/.github/agents/WebAppAgent.agent.md deleted file mode 100644 index 7a75cc1..0000000 --- a/.github/agents/WebAppAgent.agent.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -description: Azure AI Foundry Agent Service development mode - SDK research, MCP integration, and agent implementation patterns -tools: ['edit', 'search', 'new', 'runCommands', 'runTasks', 'Microsoft Docs/*', 'github/github-mcp-server/get_file_contents', 'github/github-mcp-server/search_code', 'microsoft/playwright-mcp/*', 'runSubagent', 'usages', 'vscodeAPI', 'problems', 'changes', 'fetch', 'githubRepo', 'extensions', 'todos'] -model: Claude Sonnet 4.5 (copilot) ---- - -# Azure AI Agent Development Mode - -**Purpose**: Specialized mode for Azure AI Foundry Agent Service development with ASP.NET Core + React. - -**When to use**: AI agent features, authentication, SDK integrations, state management, UI components. - -## Documentation Layers - -Avoid token waste by understanding what lives where: - -1. **copilot-instructions.md** (always loaded) → Architecture, workflows, deployment commands, critical patterns -2. **AGENTS.md files** (loaded on-demand) → Implementation details when touching backend/, frontend/, infra/, deployment/ -3. **This file** → SDK research patterns, MCP tool usage, testing workflows - -**Your role**: Research SDKs, validate with tests, connect documentation sources. Don't duplicate what's in copilot-instructions.md. - -## Azure AI Agent SDK Research Pattern - -**CRITICAL**: Don't guess SDK usage. Follow this research workflow: - -### 1. Search Official Documentation (Start here) - -**Official SDK Repository**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Agents.Persistent - -Use available MCP tools to search Microsoft Learn documentation for Azure AI Agents Persistent SDK features, patterns, and examples. - -### 2. Check Semantic Kernel Samples (Complementary patterns) - -**Repository**: https://github.com/microsoft/semantic-kernel - -**Relevant paths**: -- `dotnet/samples/GettingStartedWithAgents/AzureAIAgent/` - Getting started examples -- `dotnet/samples/Concepts/Agents/` - Advanced patterns (Step##_*.cs files) - -Semantic Kernel provides rich examples of Azure AI Agent patterns using its abstraction layer. Use available GitHub MCP tools to search and browse these samples for proven implementation patterns. - -**Note**: Semantic Kernel abstracts agent operations through its framework. When adapting patterns, translate SK abstractions to direct Azure.AI.Agents.Persistent SDK types. - -### 3. Azure AI Foundry Agent Samples - -**Official Samples Repository**: https://github.com/azure-ai-foundry/foundry-samples - -**Key paths**: -- `samples/microsoft/csharp/getting-started-agents/` - C# quickstart samples -- `samples/microsoft/python/getting-started-agents/` - Python quickstart samples -- `samples/microsoft/typescript/getting-started-agents/` - TypeScript quickstart samples -- `samples/microsoft/data/` - Sample data files (product info, etc.) - -These are the official Azure AI Foundry Agent Service samples showing function calling, file search, code interpreter, and streaming patterns. Use available GitHub MCP tools to explore language-specific implementations. - -**Additional UI Sample**: https://github.com/Azure-Samples/get-started-with-ai-agents -- React-based chat UI components and UX patterns -- **Note**: Backend uses Node.js - focus on frontend patterns only - -### 4. Broad Code Search (Last resort) - -Use available GitHub search tools to find usage examples of specific Azure.AI.Agents.Persistent types across public repositories when official documentation is insufficient. - -### Current SDK Version - -**Package**: `Azure.AI.Agents.Persistent` v1.2.0-beta.6 (pinned in WebApp.Api.csproj) - -**Why pinned**: Beta SDK with evolving API surface. Upgrade deliberately to avoid breaking changes. - -**Resources**: -- **NuGet**: https://www.nuget.org/packages/Azure.AI.Agents.Persistent -- **SDK Source**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Agents.Persistent -- **API Reference**: https://learn.microsoft.com/en-us/dotnet/api/azure.ai.agents.persistent -- **Official Samples**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Agents.Persistent/samples - -**Key sample files**: -- Function calling with streaming -- MCP tool integration -- File upload and vector search -- Async patterns - -### Microsoft Agent Framework (Higher-level abstraction) - -**Package**: `Microsoft.Agents.AI.AzureAI` v1.0.0-preview (also in project) - -The Microsoft Agent Framework provides a higher-level abstraction over the Azure AI Foundry Agent Service, offering simplified agent creation and orchestration patterns. - -**Resources**: -- **NuGet**: https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI -- **Documentation**: https://learn.microsoft.com/en-us/agent-framework/user-guide/agents/agent-types/azure-ai-foundry-agent -- **GitHub Samples**: https://github.com/microsoft/agent-framework/tree/main/dotnet/samples -- **Quickstart**: https://learn.microsoft.com/en-us/agent-framework/tutorials/quick-start - -**When to use**: -- Need unified agent abstraction across multiple AI services -- Want simplified agent lifecycle management -- Building multi-agent orchestration scenarios -- Prefer higher-level `AIAgent` abstractions over direct SDK calls - -**Relationship**: Agent Framework wraps `PersistentAgentsClient` and provides `CreateAIAgentAsync()` extension methods for simplified agent creation. Both can coexist in the same project. - -See backend/AGENTS.md for full implementation patterns (credentials, streaming, error handling, cancellation tokens). - -## Testing with Playwright MCP - -**CRITICAL**: Always test changes before completion. - -### Testing Priority (Token efficiency) - -1. **Console logs** - State transitions, errors (0 tokens) -2. **Network tab** - API calls, status codes (minimal tokens) -3. **Accessibility snapshot** - DOM structure (low tokens) -4. **Screenshots** - Visual verification (high tokens - only when essential) - -### Workflow - -```powershell -# Start servers -.\deployment\scripts\start-local-dev.ps1 - -# Then use Playwright MCP: -# 1. Navigate to http://localhost:5173 -# 2. Check console (before/after interactions) -# 3. Verify network requests -# 4. Take accessibility snapshot for DOM validation -``` - -### When to Test (Not optional) - -- After UI component or API endpoint changes -- Before committing multi-step implementations -- When user reports issues - -### Validation Checklist - -- [ ] Console shows expected actions (🔄 [timestamp] ACTION_TYPE) -- [ ] No console errors/warnings -- [ ] Network tab shows correct status codes (200/400/401/500) -- [ ] DOM elements present in accessibility snapshot - -## MCP Tool Usage Strategy - -### Documentation Research - -Use available Microsoft Learn documentation tools to: -1. **Search** Microsoft Learn for Azure AI Agents SDK topics -2. **Fetch** complete documentation pages when search results need more depth -3. Find official samples, API references, and best practices - -### GitHub Repository Access - -Use available GitHub MCP tools to: -1. **Search code** across repositories for implementation examples -2. **Browse files** in specific paths for sample code -3. Access repositories: Azure SDK, Semantic Kernel, Azure Samples - -### Browser Testing - -Use available browser automation tools to: -1. Navigate to http://localhost:5173 after starting local dev -2. Check console logs for state transitions and errors -3. Inspect network requests for API validation -4. Capture accessibility snapshots for DOM structure -5. Take screenshots only when visual verification is essential - -## Project-Specific Context - -**Architecture**: Single-conversation UI (full-width chat, no sidebar/history) -**State**: Redux-style via React Context + useReducer with dev logging - -### AI Agent Service Configuration - -**Auto-discovery** (`azd up`): Searches subscription for AI Foundry resources → prompts user to select if multiple exist → discovers agents via REST API → validates RBAC permissions → configures everything automatically. - -**Change resource**: `azd provision` (re-runs discovery + updates RBAC + regenerates `.env` files) or `azd env set AI_FOUNDRY_RESOURCE_GROUP ` then `azd provision`. - -**Implementation**: `deployment/hooks/preprovision.ps1` (discovery), `infra/main.bicep` (RBAC via `core/security/role-assignment.bicep`). diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2cf988b..ddb4e6c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,141 +1,60 @@ -# AI Agent Web App - Copilot Instructions +Azure AI Foundry Agent Service sample app — Entra ID auth, SSE streaming, Container Apps deployment. -**Purpose**: AI-powered web application with Entra ID authentication and Azure AI Foundry Agent Service integration. +## Architecture -## Architecture Overview +| Layer | Tech | Port | Entry Point | +|-------|------|------|-------------| +| **Frontend** | React 19 + Vite | 5173 | `frontend/src/App.tsx` | +| **Backend** | ASP.NET Core 9 | 8080 | `backend/WebApp.Api/Program.cs` | +| **Auth** | MSAL.js → JWT Bearer | — | `frontend/src/config/authConfig.ts` | +| **AI SDK** | Azure.AI.Projects (GA) + Azure.AI.Extensions.OpenAI | — | `backend/.../AgentFrameworkService.cs` | +| **Deploy** | Azure Container Apps | — | `infra/main.bicep` | -### Single Container Pattern -ASP.NET Core serves both REST API (`/api/*`) and React SPA (same origin). +**Key Flow**: React → MSAL token → POST /api/chat/stream → AI Foundry → SSE chunks → UI -### Authentication Flow -1. Browser → `MSAL.js` (PKCE flow) → JWT with `Chat.ReadWrite` scope -2. Frontend → Backend (JWT Bearer token) -3. Backend → Azure AI Foundry Agent Service (`ManagedIdentityCredential`) +## Design Decisions -### Configuration Strategy -- **Local**: `.env` files (gitignored, auto-generated) -- **Production**: Environment variables + Docker build args +- **Single container** — Backend serves API (`/api/*`) and React SPA from `wwwroot`, deployed as one Container App resource. +- **Credential strategy** — Production: `ManagedIdentityCredential` (user-assigned MI) or `OnBehalfOfCredential` (OBO) — mutually exclusive, controlled by `ENTRA_BACKEND_CLIENT_ID` env var. Development: `ChainedTokenCredential(AzureCliCredential, AzureDeveloperCliCredential)`. +- **OBO admin consent** — `AIProjectClient` scope `https://ai.azure.com/.default` resolves to **Azure Machine Learning Services** (appId `18a66f5f-...`). Admin consent must target this service principal. +- **OBO scope** — `OnBehalfOfCredential` authenticates Agent Service API calls. Agent tools (MCP, OpenAPI, Logic Apps) use the agent's own identity from Foundry portal. +- **User-assigned MI** — Created in the infrastructure module; used for ACR pull (`AcrPull` role) and reused for OBO FIC when enabled. +- **`.npmrc`** — Configures `--legacy-peer-deps` for React 19 peer-dep resolution. Run `npm install` from `frontend/` directory. +- **Observability** — Separate Application Insights resources for backend (OpenTelemetry via `Azure.Monitor.OpenTelemetry.AspNetCore`) and frontend (`@microsoft/applicationinsights-web`), sharing one Log Analytics workspace. Backend connection string is an env var; frontend connection string is injected at Docker build time via `VITE_APPLICATIONINSIGHTS_CONNECTION_STRING`. -## Key Files +## Deployment (Non-Obvious) -| File | Purpose | -|------|---------|| -| `backend/WebApp.Api/Program.cs` | Middleware + JWT + API endpoints | -| `frontend/src/config/authConfig.ts` | MSAL configuration | -| `deployment/hooks/preprovision.ps1` | Entra app + `.env` generation + AI Foundry discovery | -| `deployment/hooks/postprovision.ps1` | Docker build + deployment | -| `infra/main-app.bicep` | Container App configuration | +`azd up` phases: **preprovision** → **provision** (Bicep) → **postprovision** → **predeploy** → **deploy** -## Development Commands +**What's intentionally CLI (not Bicep)**: +- **Entra redirect URI + identifierUri update** — Entra app is created by Bicep (`infra/entra-app.bicep`), but `identifierUri` (`api://{appId}`) can't reference the auto-generated `appId` in the same declaration, and redirect URIs need the Container App FQDN which isn't available until after provision. Both are set in `postprovision.ps1`. +- **Federated Identity Credential (FIC)** — FIC is a child of the backend app registration. Graph API eventual consistency causes the FIC to fail when created in the same Bicep deployment as its parent app. Created in `postprovision.ps1` after Graph has replicated. +- **AI Foundry discovery** — Discovers user's *existing* external AI Foundry resource via `az cognitiveservices account list`. This is a data-plane discovery operation, not resource deployment. +- **Cross-RG RBAC** — Done via CLI so `azd down` only deletes our resource group, not the external AI Foundry resources. +- **Entra app deletion** — Microsoft Graph resources are not tied to Azure resource groups; `azd down` (which deletes the RG) won't clean them up. `postdown.ps1` handles this. -| Command | Purpose | Time | -|---------|---------|------| -| `azd up` | Full deployment (Entra app + infrastructure + container) | 10-12 min | -| `.\deployment\scripts\deploy.ps1` | Code-only deployment (Docker rebuild + push) | 3-5 min | -| VS Code task: "Start Local Dev Servers" | Start local dev (backend + frontend) | Instant | -| `.\deployment\scripts\start-local-dev.ps1` | Start local dev (manual) | Instant | +**Health probes** are conditional: disabled when placeholder image is deployed (first provision), enabled when real image exists. -**Note**: `azd deploy` is not used. This template uses an infra-only pattern where `postprovision` handles initial deployment. For code updates, run the deployment script directly to avoid redundant builds. +**Service Management Reference**: Some orgs (notably Microsoft) require this on Entra app registrations. Set via `azd env set ENTRA_SERVICE_MANAGEMENT_REFERENCE ` before running `azd up`; Bicep passes it to the Microsoft Graph extension. -## Development Workflow +## Documentation Rules -### Step 1: Initial Setup -- **Goal**: Configure authentication and generate config files -- **Action**: Run `azd up` (creates Entra app, deploys to Azure) -- **Result**: `.env` files generated in `frontend/` and `backend/WebApp.Api/` +- **Reference `*.csproj` or `package.json` for versions** — keep docs version-free +- **State current behavior** — not how we got here +- **State each fact once, cross-reference elsewhere** +- **Document only non-obvious behavior** that cannot be inferred from code -### Step 2: Daily Development -- **Goal**: Run local servers with hot reload -- **Action**: Run VS Code task "Start Local Dev Servers" -- **Result**: Backend (port 8080) + Frontend (port 5173) in separate terminals +## Development -### Step 3: Deploy Changes -- **Goal**: Update cloud deployment with code changes -- **Action**: Run `.\deployment\scripts\deploy.ps1` (Docker rebuild + push) -- **Transition**: Test at `https://.azurecontainerapps.io` - -## Custom npm Registries - -**Pattern**: Add `.npmrc` to `frontend/` directory - -```ini -# frontend/.npmrc -registry=https://registry.example.com/ -//registry.example.com/:_authToken=${NPM_TOKEN} +```powershell +# Ctrl+Shift+B → "Start Dev (VS Code Terminals)" +# Or: azd up ``` -Dockerfile copies `.npmrc` if present. Don't commit tokens. - -## Critical Patterns - -### Middleware Order (NEVER reorder) -**Goal**: Serve static files, validate auth, route APIs, fallback to SPA - -**See**: `backend/WebApp.Api/Program.cs` for correct ordering: -1. `UseDefaultFiles()` / `UseStaticFiles()` - Serve SPA assets -2. `UseAuthentication()` / `UseAuthorization()` - Validate JWT -3. Map API endpoints -4. `MapFallbackToFile("index.html")` - MUST BE LAST - -### API Endpoint Pattern -**Always use**: `.RequireAuthorization("RequireChatScope")` + `CancellationToken` + `IHostEnvironment` (for error handling) - -**See**: `backend/WebApp.Api/Program.cs` for endpoint patterns with: -- `ErrorResponseFactory.CreateFromException()` for RFC 7807-compliant errors -- Development vs production error detail sanitization -- Proper exception handling in streaming and non-streaming endpoints - -### Credential Strategy -**Local**: `ChainedTokenCredential` (uses `az login`) -**Production**: `ManagedIdentityCredential` (system-assigned) - -**See**: `backend/WebApp.Api/Services/AzureAIAgentService.cs` constructor for environment-aware credential selection - -## Deployment Phases - -1. **preprovision** → Entra app + AI Foundry auto-discovery + `.env` generation -2. **provision** → Deploy Azure resources via Bicep -3. **postprovision** → Updates redirect URIs + calls `build-and-deploy-container.ps1` to build/deploy container - -**Deployment logic**: Shared `build-and-deploy-container` module (DRY) uses local Docker if available, ACR cloud build otherwise. - -**Code-only deployment**: Use `.\deployment\scripts\deploy.ps1` (faster than `azd up`). - -## Troubleshooting - -| Issue | Fix | -|-------|-----| -| `VITE_ENTRA_SPA_CLIENT_ID not set` | Run `azd up` | -| `AI_AGENT_ENDPOINT not configured` | Run `azd provision` to re-discover AI Foundry resources | -| No AI Foundry resources found | Create an AI Foundry resource at https://ai.azure.com | -| 401 on `/api/*` | Verify token has `Chat.ReadWrite` scope | -| `ManagedIdentityCredential` error locally | Set `ASPNETCORE_ENVIRONMENT=Development` | -| Multiple AI Foundry resources | Run `azd provision` to select a different resource | - -## Folder Documentation - -See `AGENTS.md` files for implementation details: -- `backend/AGENTS.md` → ASP.NET Core + JWT + AI Agent SDK -- `frontend/AGENTS.md` → React + MSAL + Vite -- `infra/AGENTS.md` → Bicep + RBAC + Container Apps -- `deployment/AGENTS.md` → Hooks + Docker + Deployment - -## Essential Rules - -### ✅ Always Do -- Use `.RequireAuthorization("RequireChatScope")` on all API endpoints -- Accept and propagate `CancellationToken` in async methods -- Use `ErrorResponseFactory.CreateFromException()` for consistent error responses -- Implement `IDisposable` for services with disposable resources (e.g., `SemaphoreSlim`) -- Validate file uploads before processing (size, count, type) -- Use explicit credentials: `ChainedTokenCredential` (local) or `ManagedIdentityCredential` (cloud) -- Try `acquireTokenSilent()` first, fallback to `acquireTokenPopup()` -- Access `import.meta.env.*` at module level only +## Hooks -### ❌ Never Do -- Commit `.env*` files -- Use `.Result` or `.Wait()` on async methods -- Expose internal error details in production (use `IHostEnvironment.IsDevelopment()`) -- Forget disposal guards in `IDisposable` methods -- Reorder middleware pipeline -- Access `import.meta.env.*` inside functions +| Hook | Event | What It Does | +|------|-------|-------------| +| **Commit Gate** | `preToolUse` | Blocks direct `git commit`. Follow `committing-code` skill → commit via `-F COMMIT_MESSAGE.md`. | +| **Test Reminder** | `preToolUse` | Advisory: reminds to run tests if test files exist for staged changes. | +| **Doc Sync** | `postToolUse` | Reminds to update `ARCHITECTURE-FLOW.md` when architecture-sensitive files are edited. | diff --git a/.github/hooks/README.md b/.github/hooks/README.md new file mode 100644 index 0000000..2d92f66 --- /dev/null +++ b/.github/hooks/README.md @@ -0,0 +1,144 @@ +# Copilot Hooks + +Hooks are scripts that run at lifecycle events during AI-assisted development. They intercept tool calls made by Copilot agents — letting you enforce policies, suggest best practices, or trigger reminders automatically. + +## How It Works + +Copilot supports two lifecycle events: + +- **`preToolUse`** — Runs *before* a tool call executes. Can block the call (deny) or inject advisory messages. +- **`postToolUse`** — Runs *after* a tool call completes. Can inject follow-up reminders. + +Hooks are defined in `.github/hooks/commit-gate.json` and reference scripts in `.github/hooks/scripts/`. + +## Hooks in This Repo + +| Hook | Event | Purpose | Blocks? | +|------|-------|---------|---------| +| [Commit Gate](scripts/commit-gate.ps1) | `preToolUse` | Enforces `committing-code` skill workflow for commits | Yes (deny) | +| [Test Reminder](scripts/test-reminder.ps1) | `preToolUse` | Suggests running tests before committing | No (advisory) | +| [Doc Sync](scripts/doc-sync.ps1) | `postToolUse` | Reminds to update ARCHITECTURE-FLOW.md after editing sensitive files | No (advisory) | + +## JSON Contract + +### Input (stdin) + +Every hook receives a JSON object on stdin: + +```json +{ "toolName": "powershell", "toolArgs": "{\"command\":\"git commit -m 'msg'\"}" } +``` + +- `toolName` — The tool being called (e.g., `powershell`, `edit`, `create`) +- `toolArgs` — A JSON *string* containing the tool's arguments (must be parsed separately) + +### Output (stdout) + +**To block a tool call** (preToolUse only): + +```json +{ "permissionDecision": "deny", "permissionDecisionReason": "Explain why and what to do instead." } +``` + +**To show an advisory message** (preToolUse or postToolUse): + +```json +{ "message": "💡 Helpful reminder or suggestion." } +``` + +**To allow silently** — produce no output and exit 0. + +## Creating Your Own Hook + +### 1. Write the script + +Create a new `.ps1` file in `.github/hooks/scripts/`: + +``` +.github/hooks/scripts/my-hook.ps1 +``` + +Use the template below as a starting point. + +### 2. Register it in the config + +Add an entry to `commit-gate.json` under the appropriate event: + +```json +{ + "version": 1, + "hooks": { + "preToolUse": [ + { + "type": "command", + "powershell": "./scripts/my-hook.ps1", + "cwd": ".github/hooks", + "timeoutSec": 10 + } + ] + } +} +``` + +### 3. Test it + +Run the script manually by piping JSON to stdin: + +```powershell +'{"toolName":"powershell","toolArgs":"{\"command\":\"git commit -m test\"}"}' | pwsh -File .github/hooks/scripts/my-hook.ps1 +``` + +## Template + +Copy this as a starting point for new hooks: + +```powershell +# Hook Name - PreToolUse/PostToolUse +# Brief description of what this hook does +# +# Input: { "toolName": "...", "toolArgs": "..." } +# Output: { "permissionDecision": "deny", "permissionDecisionReason": "..." } to block +# { "message": "..." } for advisory messages +# (no output) to allow silently + +$ErrorActionPreference = 'SilentlyContinue' +$rawInput = [Console]::In.ReadToEnd() + +try { + $hookData = $rawInput | ConvertFrom-Json + $toolName = $hookData.toolName + $toolArgs = $null + if ($hookData.toolArgs) { + $toolArgs = $hookData.toolArgs | ConvertFrom-Json + } + + # --- Your logic here --- + + # To block (preToolUse only): + # $response = @{ + # permissionDecision = "deny" + # permissionDecisionReason = "Reason for blocking." + # } + # $response | ConvertTo-Json -Compress + # exit 0 + + # To advise: + # $response = @{ message = "💡 Advisory message." } + # $response | ConvertTo-Json -Compress + # exit 0 + + # To allow silently: do nothing (fall through) + +} catch { + # On error, allow silently (non-blocking) +} +``` + +## Tips + +- **Keep hooks fast** — enforce a `timeoutSec` of 10 or less. Slow hooks degrade the agent experience. +- **Handle errors silently** — a failing hook should never block the agent. Wrap logic in `try/catch` and let errors fall through. +- **Prefer advisory over blocking** — use `message` to suggest, not `deny` to enforce, unless the policy is critical. +- **Parse `toolArgs` separately** — it arrives as a JSON string inside the outer JSON, so it needs a second `ConvertFrom-Json` call. +- **Check `toolName` early** — exit immediately for irrelevant tools to avoid unnecessary work. +- **Test manually** — pipe sample JSON to your script before committing to verify the output format. diff --git a/.github/hooks/commit-gate.json b/.github/hooks/commit-gate.json new file mode 100644 index 0000000..75bf8c3 --- /dev/null +++ b/.github/hooks/commit-gate.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "hooks": { + "preToolUse": [ + { + "type": "command", + "powershell": "./scripts/commit-gate.ps1", + "cwd": ".github/hooks", + "timeoutSec": 10 + }, + { + "type": "command", + "powershell": "./scripts/setup-check.ps1", + "cwd": ".github/hooks", + "timeoutSec": 10 + }, + { + "type": "command", + "powershell": "./scripts/test-reminder.ps1", + "cwd": ".github/hooks", + "timeoutSec": 10 + } + ], + "postToolUse": [ + { + "type": "command", + "powershell": "./scripts/doc-sync.ps1", + "cwd": ".github/hooks", + "timeoutSec": 10 + } + ] + } +} diff --git a/.github/hooks/scripts/commit-gate.ps1 b/.github/hooks/scripts/commit-gate.ps1 new file mode 100644 index 0000000..7b77259 --- /dev/null +++ b/.github/hooks/scripts/commit-gate.ps1 @@ -0,0 +1,70 @@ +# Commit Gate Hook - PreToolUse +# Blocks direct git commit commands and redirects to the committing-code skill workflow. +# Allows commits via -F COMMIT_MESSAGE.md (the skill's prescribed workflow). +# +# Input format (CLI/coding agent): { "toolName": "powershell", "toolArgs": "{\"command\":\"git commit -m 'msg'\"}" } +# Output format: { "permissionDecision": "deny", "permissionDecisionReason": "..." } + +$ErrorActionPreference = 'SilentlyContinue' + +# Read JSON input from stdin +$rawInput = [Console]::In.ReadToEnd() + +try { + $hookData = $rawInput | ConvertFrom-Json + + $toolName = $hookData.toolName + # toolArgs is a JSON string in CLI format, parse it + $toolArgs = $null + if ($hookData.toolArgs) { + $toolArgs = $hookData.toolArgs | ConvertFrom-Json + } + + # Extract command string from tool args + $command = $null + if ($toolArgs.command) { $command = $toolArgs.command } + elseif ($toolArgs.input) { $command = $toolArgs.input } + + # Only intercept terminal/command tools + $terminalTools = @('bash', 'powershell', 'terminal', 'runTerminalCommand', 'runInTerminal', 'execute_runInTerminal', 'run_terminal_command') + $isTerminalTool = $terminalTools -contains $toolName + + # Check if any command segment is actually 'git commit' (not git grep/log with "commit" in args) + # Splits on shell operators, then checks that 'commit' is the git subcommand (first non-flag word) + $isGitCommit = $false + if ($isTerminalTool -and $command) { + foreach ($seg in ($command -split '(?:&&|\|\||[;|])')) { + if ($seg.Trim() -match '^\s*git\s+(-\S+\s+)*commit(\s|$)') { + $isGitCommit = $true + break + } + } + } + + if ($isGitCommit) { + # Allow commits that use -F COMMIT_MESSAGE.md (the skill's prescribed workflow) + if ($command -match '-F\s+COMMIT_MESSAGE\.md' -or $command -match '--file\s+COMMIT_MESSAGE\.md') { + exit 0 + } + + # Block direct commits and redirect to the committing-code skill workflow + $response = @{ + permissionDecision = "deny" + permissionDecisionReason = @" +Direct git commits are blocked by repository policy. Instead, follow the committing-code skill rules: +1. Run 'git diff --staged' to review all staged changes +2. Write a commit message to COMMIT_MESSAGE.md following the skill's conventional commit format +3. Execute 'git commit -F COMMIT_MESSAGE.md' +4. Delete COMMIT_MESSAGE.md after the commit succeeds +Do NOT attempt to commit directly again. +"@ + } + + $response | ConvertTo-Json -Compress + exit 0 + } +} catch { + # On error, allow the tool call to proceed (non-blocking) +} + +# Allow all non-commit tool calls (no output = allow) diff --git a/.github/hooks/scripts/doc-sync.ps1 b/.github/hooks/scripts/doc-sync.ps1 new file mode 100644 index 0000000..3e85427 --- /dev/null +++ b/.github/hooks/scripts/doc-sync.ps1 @@ -0,0 +1,65 @@ +# Doc Sync Hook - PostToolUse +# After an agent edits an architecture-sensitive file, reminds to update ARCHITECTURE-FLOW.md. +# +# Input format: { "toolName": "edit", "toolArgs": "{\"path\":\"backend/.../Program.cs\"}" } +# Output format: { "message": "⚠️ Architecture-sensitive file edited: ..." } + +$ErrorActionPreference = 'SilentlyContinue' + +# Read JSON input from stdin +$rawInput = [Console]::In.ReadToEnd() + +try { + $hookData = $rawInput | ConvertFrom-Json + + $toolName = $hookData.toolName + $toolArgs = $null + if ($hookData.toolArgs) { + $toolArgs = $hookData.toolArgs | ConvertFrom-Json + } + + # Only trigger on file edit/create tools + $editTools = @('edit', 'create', 'write', 'write_to_file', 'insert', 'replace', 'str_replace_editor') + $isEditTool = $editTools -contains $toolName + + if (-not $isEditTool) { + exit 0 + } + + # Extract file path from tool args + $filePath = $null + if ($toolArgs.path) { $filePath = $toolArgs.path } + elseif ($toolArgs.file_path) { $filePath = $toolArgs.file_path } + elseif ($toolArgs.filePath) { $filePath = $toolArgs.filePath } + + if (-not $filePath) { + exit 0 + } + + # Architecture-sensitive file names + $sensitiveFiles = @( + 'Program.cs', + 'AgentFrameworkService.cs', + 'AppContext.tsx', + 'appReducer.ts', + 'chatService.ts', + 'ChatInterface.tsx', + 'AgentChat.tsx' + ) + + # Check if the edited file matches any sensitive file + $fileName = [System.IO.Path]::GetFileName($filePath) + $isSensitive = $sensitiveFiles -contains $fileName + + if ($isSensitive) { + $response = @{ + message = "⚠️ Architecture-sensitive file edited: $fileName. If you changed endpoints, state actions, SSE events, or component contracts, also update ARCHITECTURE-FLOW.md (sections 1.1, 1.5, 2.7, 2.8)." + } + $response | ConvertTo-Json -Compress + exit 0 + } +} catch { + # On error, allow silently (non-blocking) +} + +# No output = no reminder needed diff --git a/.github/hooks/scripts/setup-check.ps1 b/.github/hooks/scripts/setup-check.ps1 new file mode 100644 index 0000000..318dbe9 --- /dev/null +++ b/.github/hooks/scripts/setup-check.ps1 @@ -0,0 +1,124 @@ +# Setup Check Hook - PreToolUse +# Context-aware validation: checks only the env vars relevant to the command being run. +# Frontend commands (npm/vite) → check frontend env. Backend commands (dotnet) → check backend env. +# Full-stack commands (start-local-dev) → check both. +# Returns an advisory message when config is incomplete — does NOT block the command. +# +# Input: { "toolName": "powershell", "toolArgs": "{\"command\":\"npm run dev\"}" } +# Output: { "message": "⚠️ ..." } or nothing (allow silently) + +$ErrorActionPreference = 'SilentlyContinue' +$rawInput = [Console]::In.ReadToEnd() + +try { + $hookData = $rawInput | ConvertFrom-Json + $toolName = $hookData.toolName + $toolArgs = $null + if ($hookData.toolArgs) { + $toolArgs = $hookData.toolArgs | ConvertFrom-Json + } + + # Extract command string from whichever field the agent uses + $command = $null + if ($toolArgs.command) { $command = $toolArgs.command } + elseif ($toolArgs.input) { $command = $toolArgs.input } + + # Only check terminal/command tools — exit fast for file reads, edits, etc. + $terminalTools = @('bash', 'powershell', 'terminal', 'runTerminalCommand', 'runInTerminal', + 'execute_runInTerminal', 'run_terminal_command') + if ($terminalTools -notcontains $toolName -or -not $command) { exit 0 } + + # Classify command → which layer(s) it touches + $checkFrontend = $false + $checkBackend = $false + + # Frontend-only commands + if ($command -match 'npm\s+run\s+(dev|build|start)' -or $command -match 'npx\s+vite') { + $checkFrontend = $true + } + # Backend-only commands + elseif ($command -match 'dotnet\s+(watch|run|build)') { + $checkBackend = $true + } + # Full-stack commands + elseif ($command -match 'start-local-dev') { + $checkFrontend = $true + $checkBackend = $true + } + else { + exit 0 # Not a dev command we care about + } + + # Resolve project root (hooks run from .github/hooks/scripts/) + $projectRoot = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent + + $issues = @() + $frontendEnv = Join-Path $projectRoot "frontend/.env.local" + $backendEnv = Join-Path $projectRoot "backend/WebApp.Api/.env" + $azdDir = Join-Path $projectRoot ".azure" + + # --- Frontend checks --- + if ($checkFrontend) { + if (-not (Test-Path $frontendEnv)) { + $issues += "frontend/.env.local is missing (needed for Entra SPA auth)" + } else { + $content = Get-Content $frontendEnv -Raw + if ($content -notmatch 'VITE_ENTRA_SPA_CLIENT_ID=\S') { + $issues += "VITE_ENTRA_SPA_CLIENT_ID is empty in frontend/.env.local" + } + if ($content -notmatch 'VITE_ENTRA_TENANT_ID=\S') { + $issues += "VITE_ENTRA_TENANT_ID is empty in frontend/.env.local" + } + } + } + + # --- Backend checks --- + if ($checkBackend) { + if (-not (Test-Path $backendEnv)) { + $issues += "backend/WebApp.Api/.env is missing (needed for JWT auth + AI agent config)" + } else { + $content = Get-Content $backendEnv -Raw + if ($content -notmatch 'AzureAd__ClientId=\S') { + $issues += "AzureAd__ClientId is empty in backend/.env" + } + if ($content -notmatch 'AI_AGENT_ENDPOINT=\S') { + $issues += "AI_AGENT_ENDPOINT is empty in backend/.env (backend will crash on first API call)" + } + if ($content -notmatch 'AI_AGENT_ID=\S') { + $issues += "AI_AGENT_ID is empty in backend/.env (backend will crash on first API call)" + } + } + } + + if ($issues.Count -eq 0) { exit 0 } + + # Build targeted advisory + $issueList = ($issues | ForEach-Object { " - $_" }) -join "`n" + $hasAzd = Test-Path $azdDir + + $fixStep = if ($hasAzd) { + "Run 'azd provision' (or 'azd up') then restart dev servers." + } else { + "Run 'azd up' from the repo root (creates Entra app + generates .env files)." + } + + $layer = if ($checkFrontend -and $checkBackend) { "full-stack" } + elseif ($checkFrontend) { "frontend" } + else { "backend" } + + $response = @{ + message = @" +⚠️ Incomplete $layer setup: +$issueList + +$fixStep + +Load the 'validating-local-setup' skill for detailed diagnostics. +"@ + } + $response | ConvertTo-Json -Compress + exit 0 + +} catch { + # On error, allow silently — don't break the agent's workflow +} diff --git a/.github/hooks/scripts/test-reminder.ps1 b/.github/hooks/scripts/test-reminder.ps1 new file mode 100644 index 0000000..d8fe1a4 --- /dev/null +++ b/.github/hooks/scripts/test-reminder.ps1 @@ -0,0 +1,109 @@ +# Test Reminder Hook - PreToolUse +# Before a commit via -F COMMIT_MESSAGE.md, checks for test files matching staged source files +# and reminds to run them. Advisory only — does NOT block the commit. +# +# Input format: { "toolName": "powershell", "toolArgs": "{\"command\":\"git commit -F COMMIT_MESSAGE.md\"}" } +# Output format: { "message": "💡 Test files exist for staged changes: ..." } + +$ErrorActionPreference = 'SilentlyContinue' + +# Read JSON input from stdin +$rawInput = [Console]::In.ReadToEnd() + +try { + $hookData = $rawInput | ConvertFrom-Json + + $toolName = $hookData.toolName + $toolArgs = $null + if ($hookData.toolArgs) { + $toolArgs = $hookData.toolArgs | ConvertFrom-Json + } + + # Extract command string from tool args + $command = $null + if ($toolArgs.command) { $command = $toolArgs.command } + elseif ($toolArgs.input) { $command = $toolArgs.input } + + # Only trigger on terminal tools with git commit commands + $terminalTools = @('bash', 'powershell', 'terminal', 'runTerminalCommand', 'runInTerminal', 'execute_runInTerminal', 'run_terminal_command') + $isTerminalTool = $terminalTools -contains $toolName + + # Check if any command segment is actually 'git commit' (not git grep/log with "commit" in args) + $isGitCommit = $false + if ($isTerminalTool -and $command) { + foreach ($seg in ($command -split '(?:&&|\|\||[;|])')) { + if ($seg.Trim() -match '^\s*git\s+(-\S+\s+)*commit(\s|$)') { + $isGitCommit = $true + break + } + } + } + if (-not $isGitCommit) { + exit 0 + } + + # Only run on commits via -F COMMIT_MESSAGE.md (the committing-code skill workflow) + if (-not ($command -match '-F\s+COMMIT_MESSAGE\.md' -or $command -match '--file\s+COMMIT_MESSAGE\.md')) { + exit 0 + } + + # Get staged files + $stagedFiles = git diff --cached --name-only 2>$null + if (-not $stagedFiles) { + exit 0 + } + + $matchingTests = @() + + foreach ($file in $stagedFiles) { + $fileName = [System.IO.Path]::GetFileNameWithoutExtension($file) + $extension = [System.IO.Path]::GetExtension($file) + + # Skip test files themselves and non-source files + if ($file -match '\.test\.' -or $file -match '\.spec\.' -or $file -match 'Tests\.cs$' -or $file -match '__tests__') { + continue + } + + # TypeScript/JavaScript: look for __tests__/name.test.ts(x) or name.test.ts(x) + if ($extension -match '^\.(ts|tsx|js|jsx)$') { + $testPatterns = @( + "**/__tests__/$fileName.test$extension", + "**/__tests__/$fileName.test.ts", + "**/__tests__/$fileName.test.tsx", + "**/$fileName.test$extension", + "**/$fileName.spec$extension" + ) + foreach ($pattern in $testPatterns) { + $found = git ls-files $pattern 2>$null + if ($found) { + $matchingTests += $found + } + } + } + + # C#: look for matching *Tests.cs files + if ($extension -eq '.cs') { + $testPattern = "**/${fileName}Tests.cs" + $found = git ls-files $testPattern 2>$null + if ($found) { + $matchingTests += $found + } + } + } + + # Deduplicate + $matchingTests = $matchingTests | Select-Object -Unique + + if ($matchingTests.Count -gt 0) { + $testList = ($matchingTests | ForEach-Object { [System.IO.Path]::GetFileName($_) }) -join ', ' + $response = @{ + message = "💡 Test files exist for staged changes: $testList. Consider running tests before committing." + } + $response | ConvertTo-Json -Compress + exit 0 + } +} catch { + # On error, allow silently (non-blocking) +} + +# No output = no reminder diff --git a/.github/instructions/bicep.instructions.md b/.github/instructions/bicep.instructions.md deleted file mode 100644 index c802c91..0000000 --- a/.github/instructions/bicep.instructions.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -description: Bicep coding standards and patterns for Azure infrastructure -applyTo: "**/*.bicep" ---- - -# Bicep Instructions - -**Goal**: Create consistent, secure Azure infrastructure - -## Naming Convention - -**Use**: `resourceToken` from `uniqueString(subscription().id, environmentName, location)` - -**Pattern**: `--` (see `abbreviations.json`) -**Exception**: ACR requires alphanumeric only: `cr${resourceToken}` - -```bicep -var token = toLower(uniqueString(subscription().id, environmentName, location)) -name: '${abbrs.appContainerApps}web-${token}' // ca-web-abc123 -``` - -## Parameters - -**Always**: Add `@description()` and use `@allowed()` for constrained values - -```bicep -@description('Environment (dev, prod)') -param environmentName string - -@description('Azure region') -@allowed(['eastus2', 'westus2']) -param location string = 'eastus2' -``` - -## Outputs - -**Purpose**: Expose key identifiers for `azd` and other modules - -```bicep -output containerAppName string = containerApp.name -output webEndpoint string = 'https://${containerApp.properties.configuration.ingress.fqdn}' -output identityPrincipalId string = containerApp.identity.principalId -``` - -## Reference Existing Resources - -```bicep -resource aiFoundry 'Microsoft.CognitiveServices/accounts@2023-05-01' existing = { - scope: resourceGroup(aiFoundryResourceGroup) - name: aiFoundryResourceName -} -``` - -## Managed Identity - -**Always use**: System-assigned identity + output `principalId` for RBAC - -```bicep -identity: { type: 'SystemAssigned' } -output identityPrincipalId string = resource.identity.principalId -``` - -## RBAC Assignments - -**Pattern**: Use `guid()` for names + specify `principalType` - -```bicep -resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(resource.id, principalId, roleId) - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId) - principalId: principalId - principalType: 'ServicePrincipal' - } -} -``` - -## Secrets - -**Use**: Container App secrets + `listCredentials()` pattern - -```bicep -secrets: [{ - name: 'registry-password' - value: containerRegistry.listCredentials().passwords[0].value -}] -``` - -## Container Apps - -**Key settings**: -- `minReplicas: 0` (scale-to-zero) -- `targetPort: 8080` (convention) -- `allowInsecure: false` (HTTPS only) - -## Validation - -```powershell -az bicep build --file main.bicep -az deployment group what-if --template-file main.bicep -``` diff --git a/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md deleted file mode 100644 index 29aa4b6..0000000 --- a/.github/instructions/csharp.instructions.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -description: C# and ASP.NET Core coding standards -applyTo: "**/*.cs" ---- - -# C# Instructions - -**Goal**: Write secure, async, maintainable ASP.NET Core code - -## Minimal API Pattern - -**Use**: Minimal APIs (not Controllers unless 10+ endpoints) - -**Always include**: `.RequireAuthorization()` + `CancellationToken` - -```csharp -app.MapPost("/api/endpoint", async ( - RequestModel request, - ServiceClass service, - CancellationToken ct) => -{ - var result = await service.DoWorkAsync(request, ct); - return Results.Ok(result); -}) -.RequireAuthorization("RequireChatScope"); -``` - -## Authentication - -**Use**: `Microsoft.Identity.Web` with scope-based policies - -```csharp -builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(options => { - builder.Configuration.Bind("AzureAd", options); - var clientId = builder.Configuration["AzureAd:ClientId"]; - options.TokenValidationParameters.ValidAudiences = - new[] { clientId, $"api://{clientId}" }; - }, options => builder.Configuration.Bind("AzureAd", options)); - -builder.Services.AddAuthorization(options => - options.AddPolicy("RequireChatScope", policy => - policy.RequireAuthenticatedUser() - .RequireClaim("scp", "Chat.ReadWrite"))); -``` - -## Async Best Practices - -- ✅ Always accept `CancellationToken` -- ✅ Propagate tokens through call chain -- ❌ Never use `.Result` or `.Wait()` - -## Dependency Injection - -**Lifetimes**: -- `Singleton`: Stateless, thread-safe (AI clients) -- `Scoped`: Per-request (DB contexts) -- `Transient`: Stateful or lightweight - -```csharp -builder.Services.AddSingleton(); -builder.Services.AddScoped(); -``` - -## Azure SDK - -**Use**: Environment-aware credentials - -```csharp -var credential = env == "Development" - ? new ChainedTokenCredential(new AzureCliCredential(), new AzureDeveloperCliCredential()) - : new ManagedIdentityCredential(); -var client = new PersistentAgentsClient(endpoint, credential); -``` - -## Error Handling - -```csharp -try { - return Results.Ok(await service.DoWorkAsync(request, ct)); -} catch (ArgumentException ex) { - return Results.BadRequest(ex.Message); -} catch (Exception ex) { - return Results.Problem(ex.Message, statusCode: 500); -} -``` - -## Configuration - -**Use**: `IConfiguration` + environment variables (never commit secrets) - -```csharp -builder.Services.Configure( - builder.Configuration.GetSection("MySettings")); -``` - -## Models - -**Use**: Records for immutable DTOs + nullable reference types - -```csharp -public record ChatRequest(string ConversationId, string Message); -public record ChatResponse(string Message, string? ConversationId = null); -``` - -## Common Mistakes - -❌ Skipping `.RequireAuthorization()` -❌ Using `.Result` or `.Wait()` -❌ Forgetting `CancellationToken` -❌ Putting `MapFallbackToFile` before API mappings -❌ Storing secrets in `appsettings.json` diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md deleted file mode 100644 index 5af11cf..0000000 --- a/.github/instructions/typescript.instructions.md +++ /dev/null @@ -1,120 +0,0 @@ ---- -description: TypeScript and React coding standards -applyTo: "**/*.ts,**/*.tsx" ---- - -# TypeScript Instructions - -**Goal**: Write type-safe React components with proper MSAL integration - -## TypeScript Config - -**Enable**: Strict mode + explicit types (avoid `any`, use `unknown`) - -```json -{ - "compilerOptions": { - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true - } -} -``` - -## React Components - -**Use**: Functional components + hooks + typed props - -```typescript -interface MessageProps { - message: string; - sender: 'user' | 'agent'; -} - -function Message({ message, sender }: MessageProps) { - return
{message}
; -} -``` - -## MSAL Pattern - -**Always**: Try silent first, fallback to popup - -```typescript -try { - const { accessToken } = await instance.acquireTokenSilent({ - ...tokenRequest, - account: accounts[0] - }); - return accessToken; -} catch { - const { accessToken } = await instance.acquireTokenPopup(tokenRequest); - return accessToken; -} -``` - -## API Calls - -**Always**: Include `Authorization` header + use `async/await` - -```typescript -const response = await fetch('/api/endpoint', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) -}); - -if (!response.ok) throw new Error(`API error: ${response.status}`); -``` - -## Environment Variables - -**Critical**: Access at module level only (build-time replacement) - -```typescript -// ✅ Correct - module level -const clientId = import.meta.env.VITE_ENTRA_CLIENT_ID; - -// ❌ Wrong - inside function -function getClientId() { - return import.meta.env.VITE_ENTRA_CLIENT_ID; // Won't work after build -} -``` - -## State Management - -**Use**: `useState` (local) or Context API (shared) - -```typescript -const [messages, setMessages] = useState([]); -const [loading, setLoading] = useState(false); -const [error, setError] = useState(null); -``` - -## Hooks Rules - -- ✅ Call at top level only -- ✅ Name custom hooks with `use` prefix -- ❌ Never call conditionally or in loops - -## npm Dependencies - -**React 19**: Use `--legacy-peer-deps` flag - -```bash -npm install --legacy-peer-deps -``` - -**Custom Registries**: Add `.npmrc` to `frontend/` directory - -## Common Mistakes - -- Accessing `import.meta.env.*` in functions -- Calling hooks conditionally -- Using `any` type -- Storing tokens in component state -- Forgetting error boundaries -- Running `npm install` without `--legacy-peer-deps` diff --git a/.github/skills/committing-code/SKILL.md b/.github/skills/committing-code/SKILL.md new file mode 100644 index 0000000..1550ac4 --- /dev/null +++ b/.github/skills/committing-code/SKILL.md @@ -0,0 +1,69 @@ +--- +name: committing-code +description: > + Provides commit message format and workflow for this repository. + Use when creating git commits, reviewing staged changes, or + generating conventional commit messages. +--- + +# Commit Message Format + +```text +(): + +## Summary +Brief description of what was done. Fixes #. + +## New Components + +### ComponentName (path/to/file.ts) +- Bullet points describing the component + +## Enhanced Components + +### ExistingComponent.ts +- What was changed and why + +## Files Changed +- path/to/file1.ts +- path/to/file2.ts + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +``` + +## Type Prefixes + +| Type | When to Use | +|------|-------------| +| `feat` | New feature or capability | +| `fix` | Bug fix | +| `refactor` | Code restructuring (no behavior change) | +| `chore` | Dependencies, config, tooling | +| `docs` | Documentation only | + +Scope is optional — use when the change targets a specific area (e.g., `feat(citations):`, `fix(auth):`). + +## Commit Workflow + +1. Run `git diff --staged` to review all staged changes +2. Read files for context if needed; look for related issue numbers +3. Write the commit message to `COMMIT_MESSAGE.md` +4. Run `git commit -F COMMIT_MESSAGE.md` +5. Delete `COMMIT_MESSAGE.md` + +## Rules + +- Subject line: max 72 chars, imperative mood ("Add" not "Added") +- Include `Fixes #N` or `Closes #N` in Summary when applicable +- Always list all changed files in the Files Changed section +- Omit "Testing" and "Breaking Changes" sections — not used in this repo +- New components: document with bullet points describing key features +- Enhanced components: explain what changed and why +- Always include the Co-authored-by trailer as the last line of the commit body: + `Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>` + +## Constraints + +- ❌ Don't stage files (`git add`) — that's the caller's job +- ❌ Don't push (`git push`) — user decides when to push +- ❌ Don't modify code — only create commits during the commit workflow diff --git a/.github/skills/deploying-to-azure/SKILL.md b/.github/skills/deploying-to-azure/SKILL.md new file mode 100644 index 0000000..4a72a4e --- /dev/null +++ b/.github/skills/deploying-to-azure/SKILL.md @@ -0,0 +1,219 @@ +--- +name: deploying-to-azure +description: Provides deployment commands and troubleshooting for Azure Container Apps. Use when running azd commands, deploying containers, debugging deployment failures, or updating infrastructure in this repository. +--- + +# Deploying to Azure + +## Subagent Delegation for Deployment Analysis + +**Container logs and deployment output can be massive** (1000+ lines). Delegate to subagent for: +- Analyzing full deployment logs +- Debugging container startup failures +- RBAC permission troubleshooting +- Multi-resource status checks + +### Delegation Pattern + +```text +runSubagent( + prompt: "ANALYSIS task - analyze deployment issue. + + **Problem**: [describe the deployment failure] + + **Run these commands**: + 1. az containerapp logs show --name --resource-group --tail 200 + 2. az containerapp show --name --resource-group --query 'properties.provisioningState' + + **Find**: + - Error messages or stack traces + - Resource provisioning failures + - Configuration mismatches + + **Return** (max 15 lines): + - Root cause (1-2 sentences) + - Key error lines only (max 5) + - Suggested fix command + + Do NOT include full log output.", + description: "Debug: [deployment issue]" +) +``` + +### When to Delegate vs Inline + +| Delegate to Subagent | Keep Inline | +|----------------------|-------------| +| Full log analysis (100+ lines) | Quick status check | +| Multi-resource debugging | Single az command | +| RBAC permission audit | Container image query | +| Startup failure diagnosis | Provisioning state check | + +## Quick Commands + +| Command | Purpose | Time | +|---------|---------|------| +| `azd up` | Full deployment (Entra app + infrastructure + container) | 10-12 min | +| `azd deploy` | Code-only deployment (Docker rebuild + push) | 3-5 min | +| `azd provision` | Re-run infrastructure + AI Foundry discovery | 5-7 min | + +## Deployment Phases + +1. **preprovision** → AI Foundry auto-discovery + tenant detection. For CI: `azd env set ENTRA_SERVICE_MANAGEMENT_REFERENCE ""` +2. **provision** → Deploy Azure resources via Bicep (infrastructure + Entra app via Microsoft Graph Bicep extension + placeholder container image) +3. **postprovision** → Sets `identifierUri` on Entra app + updates redirect URIs + assigns RBAC to AI Foundry + generates local dev config +4. **predeploy** → Builds container (local Docker or ACR cloud build) + +**Implementation**: +- `infra/entra-app.bicep` (Entra app registration via Microsoft Graph Bicep extension) +- `deployment/hooks/preprovision.ps1` (AI Foundry discovery) +- `deployment/hooks/postprovision.ps1` (Entra config + RBAC + local config generation) +- `deployment/hooks/predeploy.ps1` (container build + push) +- `deployment/hooks/modules/Get-AIFoundryAgents.ps1` (agent discovery via REST) + +## Docker Multi-Stage Build + +Build order: React → .NET → Runtime + +- Frontend: `deployment/docker/frontend.Dockerfile` +- Backend: `deployment/docker/backend.Dockerfile` +- Custom npm registries: Add `.npmrc` to `frontend/` directory + +## AI Foundry Resource Configuration + +**Auto-discovery** (`azd up`): Searches subscription for AI Foundry resources → prompts to select if multiple → discovers agents via REST API → configures RBAC. + +**Change resource**: Run `azd provision` to re-run discovery, or: +```powershell +azd env set AI_FOUNDRY_RESOURCE_GROUP +azd provision +``` + +## Container Infrastructure + +- **Health Probes**: Liveness (`GET /api/health` every 30s) and startup (`GET /api/health` every 10s, 5s initial delay) probes configured on the Container App +- **ACR Pull**: Uses a user-assigned managed identity with `AcrPull` role — no admin credentials or secrets. The MI is created in `main-infrastructure.bicep` before the Container App, avoiding the chicken-and-egg problem. +- **Resource Defaults**: 0.5 vCPU, 1GB RAM, 0-3 replicas (all parameterized in `container-app.bicep`) + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| `VITE_ENTRA_SPA_CLIENT_ID not set` | Run `azd up` to generate `.env` files | +| `AI_AGENT_ENDPOINT not configured` | Run `azd provision` to re-discover AI Foundry | +| No AI Foundry resources found | Create at https://ai.azure.com | +| Multiple AI Foundry resources | Run `azd provision` to select different resource | +| Container not updating | Check `az containerapp logs show --name $app --resource-group $rg` | +| Container fails health check | Verify `/api/health` endpoint returns 200 — check container logs for startup errors | + +## Useful Commands + +```powershell +# Check current container image +az containerapp show --name $app --resource-group $rg ` + --query "properties.template.containers[0].image" + +# View container logs +az containerapp logs show --name $app --resource-group $rg --tail 100 + +# Check RBAC assignments +$principalId = az containerapp show --name $app --resource-group $rg ` + --query "identity.principalId" -o tsv +az role assignment list --assignee $principalId +``` + +--- + +## Preprovision Hook Details + +**File**: `deployment/hooks/preprovision.ps1` + +**What it does**: +1. Discovers AI Foundry resources in subscription (prompts if multiple) +2. Discovers agents via REST API using `Get-AIFoundryAgents.ps1` +3. Auto-detects tenant ID +4. Sets azd environment variables + +**Note**: Entra app registration is handled by Bicep (`infra/entra-app.bicep`), not this hook. + +## Postprovision Hook Details + +**File**: `deployment/hooks/postprovision.ps1` + +**What it does**: +1. Sets `identifierUri` (`api://{clientId}`) on Entra app — can't be done in Bicep because it references the auto-generated `appId` +2. Updates Entra app redirect URIs (localhost + Container App FQDN) +3. Assigns Cognitive Services User role to Container App's managed identity on AI Foundry resource (via Azure CLI, not Bicep) +4. Generates local dev config files (`.env.local` for frontend, `.env` for backend) + +**Why RBAC via CLI?**: Using Azure CLI for role assignment prevents azd from tracking the external AI Foundry resource group, avoiding accidental deletion on `azd down`. + +## Predeploy Hook Details + +**File**: `deployment/hooks/predeploy.ps1` + +**What it does**: +1. Detects if Docker is available and running +2. Uses local Docker build + push if available (~2 min) +3. Falls back to ACR cloud build if Docker unavailable (~4-5 min) +4. Updates Container App with new image (if it exists) +5. Sets `SERVICE_WEB_IMAGE_NAME` env var for Bicep + +## Dockerfile Example + +**File**: `deployment/docker/frontend.Dockerfile` (production build) + +```dockerfile +# Stage 1: Build React Frontend +FROM node:22-alpine AS frontend-builder +ARG ENTRA_SPA_CLIENT_ID +ARG ENTRA_TENANT_ID +WORKDIR /app/frontend +COPY frontend/ ./ +RUN npm ci +# Remove local .env files to prevent localhost config +RUN rm -f .env.local .env.development .env +ENV NODE_ENV=production +ENV VITE_ENTRA_SPA_CLIENT_ID=$ENTRA_SPA_CLIENT_ID +ENV VITE_ENTRA_TENANT_ID=$ENTRA_TENANT_ID +RUN npm run build + +# Stage 2: Build .NET Backend +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS backend-builder +WORKDIR /app +COPY backend/WebApp.sln ./ +COPY backend/WebApp.Api/WebApp.Api.csproj ./backend/WebApp.Api/ +COPY backend/WebApp.ServiceDefaults/WebApp.ServiceDefaults.csproj ./backend/WebApp.ServiceDefaults/ +RUN dotnet restore backend/WebApp.Api/WebApp.Api.csproj +COPY backend/ ./backend/ +RUN dotnet publish backend/WebApp.Api/WebApp.Api.csproj -c Release -o /app/publish + +# Stage 3: Runtime - Single container serving API + static files +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine +WORKDIR /app +COPY --from=backend-builder /app/publish ./ +COPY --from=frontend-builder /app/frontend/dist ./wwwroot +EXPOSE 8080 +ENV ASPNETCORE_URLS=http://+:8080 +ENV ASPNETCORE_ENVIRONMENT=Production +ENTRYPOINT ["dotnet", "WebApp.Api.dll"] +``` + +## Related Skills + +- **writing-csharp-code** - Backend coding patterns for Container App configuration +- **writing-bicep-templates** - Infrastructure templates for Azure resources +- **troubleshooting-authentication** - Entra ID and RBAC debugging + +## Official Documentation + +| Topic | URL | +|-------|-----| +| **Azure Container Apps** | https://learn.microsoft.com/azure/container-apps/overview | +| **Container Apps quickstart** | https://learn.microsoft.com/azure/container-apps/quickstart-portal | +| **Azure Developer CLI (azd)** | https://learn.microsoft.com/azure/developer/azure-developer-cli/overview | +| **azd templates** | https://learn.microsoft.com/azure/developer/azure-developer-cli/azd-templates | +| **Azure AI Foundry overview** | https://learn.microsoft.com/azure/ai-foundry/what-is-ai-foundry | +| **AI Foundry Agent Service** | https://learn.microsoft.com/azure/ai-foundry/agents/overview | +| **Managed Identity** | https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview | +| **Cognitive Services RBAC** | https://learn.microsoft.com/azure/ai-services/authentication | diff --git a/.github/skills/implementing-chat-streaming/SKILL.md b/.github/skills/implementing-chat-streaming/SKILL.md new file mode 100644 index 0000000..42e3834 --- /dev/null +++ b/.github/skills/implementing-chat-streaming/SKILL.md @@ -0,0 +1,205 @@ +--- +name: implementing-chat-streaming +description: Provides SSE streaming patterns for the chat API and frontend. Use when implementing or modifying chat streaming, handling SSE events, or troubleshooting message flow between frontend and backend. +--- + +# Chat Streaming Implementation + +## Backend: SSE Endpoint + +```csharp +app.MapPost("/api/chat/stream", async ( + ChatRequest request, + AgentFrameworkService agentService, + HttpContext httpContext, + CancellationToken cancellationToken) => +{ + httpContext.Response.Headers.Append("Content-Type", "text/event-stream"); + httpContext.Response.Headers.Append("Cache-Control", "no-cache"); + + var conversationId = request.ConversationId + ?? await agentService.CreateConversationAsync(request.Message, cancellationToken); + + // Send conversation ID first + await httpContext.Response.WriteAsync( + $"data: {{\"type\":\"conversationId\",\"conversationId\":\"{conversationId}\"}}\n\n", + cancellationToken); + await httpContext.Response.Body.FlushAsync(cancellationToken); + + // Stream chunks + await foreach (var chunk in agentService.StreamMessageAsync( + conversationId, request.Message, request.ImageDataUris, cancellationToken)) + { + var json = JsonSerializer.Serialize(new { type = "chunk", content = chunk }); + await httpContext.Response.WriteAsync($"data: {json}\n\n", cancellationToken); + await httpContext.Response.Body.FlushAsync(cancellationToken); + } + + await httpContext.Response.WriteAsync("data: {\"type\":\"done\"}\n\n", cancellationToken); +}) +.RequireAuthorization("RequireChatScope"); +``` + +## Backend: IAsyncEnumerable Service + +**Actual return type**: `IAsyncEnumerable` (not raw strings) + +**Why direct SDK?** Uses `ProjectResponsesClient` directly because we need typed access to MCP approvals, file search quotes, and citation annotations. See `.github/skills/researching-azure-ai-sdk/SKILL.md` for full rationale. + +```csharp +public async IAsyncEnumerable StreamMessageAsync( + string conversationId, + string message, + List? imageDataUris = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) +{ + ObjectDisposedException.ThrowIf(_disposed, this); + + // Stream response - yields StreamChunk with text deltas OR annotations + await foreach (var update in responsesClient.CreateResponseStreamingAsync(...)) + { + if (update is StreamingResponseOutputTextDeltaUpdate deltaUpdate) + { + yield return StreamChunk.Text(deltaUpdate.Delta); + } + else if (update is StreamingResponseOutputItemDoneUpdate itemDoneUpdate) + { + var annotations = ExtractAnnotations(itemDoneUpdate.Item, fileSearchQuotes); + if (annotations.Count > 0) + { + yield return StreamChunk.WithAnnotations(annotations); + } + } + } +} +``` + +**StreamChunk model** (`backend/WebApp.Api/Models/StreamChunk.cs`): +- `IsText` / `TextDelta` - Text content +- `HasAnnotations` / `Annotations` - Citation metadata + +## Frontend: Action Flow + +```text +CHAT_SEND_MESSAGE + → CHAT_ADD_ASSISTANT_MESSAGE + → CHAT_START_STREAM + → (repeat CHAT_STREAM_CHUNK) + → CHAT_STREAM_ANNOTATIONS (optional, for citations) + → CHAT_STREAM_COMPLETE (with usage metrics) +``` + +If user cancels: `CHAT_CANCEL_STREAM` sets status to `idle`. + +## Frontend: ChatService Pattern + +See: `frontend/src/services/ChatService.ts` + +Key patterns: +- AbortController for cancellation +- EventSource or fetch with ReadableStream +- Parse SSE `data:` lines +- Dispatch actions for each event type + +## Image Validation + +**Backend limits** (see `AzureAIAgentService.cs`): +- Max 5 images per request +- Max 5MB per image (decoded) +- Allowed: `image/png`, `image/jpeg`, `image/gif`, `image/webp` + +**Frontend limits** (see `frontend/src/utils/fileAttachments.ts`): +- Same limits with user-friendly error messages +- Toast notifications for validation feedback + +--- + +## Project-Specific: Full Endpoint Implementation + +```csharp +app.MapPost("/api/chat/stream", async ( + ChatRequest request, + AgentFrameworkService agentService, + HttpContext httpContext, + IHostEnvironment env, + CancellationToken cancellationToken) => +{ + httpContext.Response.Headers.Append("Content-Type", "text/event-stream"); + httpContext.Response.Headers.Append("Cache-Control", "no-cache"); + + var conversationId = request.ConversationId + ?? await agentService.CreateConversationAsync(request.Message, cancellationToken); + + await httpContext.Response.WriteAsync( + $"data: {{\"type\":\"conversationId\",\"conversationId\":\"{conversationId}\"}}\n\n", + cancellationToken); + await httpContext.Response.Body.FlushAsync(cancellationToken); + + await foreach (var chunk in agentService.StreamMessageAsync( + conversationId, request.Message, request.ImageDataUris, cancellationToken)) + { + var json = System.Text.Json.JsonSerializer.Serialize(new { type = "chunk", content = chunk }); + await httpContext.Response.WriteAsync($"data: {json}\n\n", cancellationToken); + await httpContext.Response.Body.FlushAsync(cancellationToken); + } + + await httpContext.Response.WriteAsync("data: {\"type\":\"done\"}\n\n", cancellationToken); +}) +.RequireAuthorization("RequireChatScope") +.WithName("StreamChatMessage"); +``` + +## Project-Specific: Service Implementation + +**See**: `backend/WebApp.Api/Services/AgentFrameworkService.cs` + +**Key patterns in `StreamMessageAsync`**: +- Disposal guard before processing +- Multi-modal message support (text + image data URIs) +- `IAsyncEnumerable` with `[EnumeratorCancellation]` +- `StreamingResponseOutputTextDeltaUpdate` for text content +- `StreamingResponseOutputItemDoneUpdate` for annotations +- Collects file search quotes via `FileSearchCallResponseItem` for citation context +- Usage captured from `StreamingResponseCompletedUpdate` + +## Project-Specific: Frontend State Flow + +```text +CHAT_SEND_MESSAGE + → CHAT_ADD_ASSISTANT_MESSAGE + → CHAT_START_STREAM + → (repeat CHAT_STREAM_CHUNK) + → CHAT_STREAM_ANNOTATIONS (optional, for citations) + → CHAT_STREAM_COMPLETE (with usage: promptTokens, completionTokens, totalTokens, duration) +``` + +**Cancel**: `CHAT_CANCEL_STREAM` sets status to `idle` and re-enables input. + +**Error**: `CHAT_ERROR` with `AppError` containing message, optional retry action, timestamp. + +## SSE Event Types + +| Event Type | Payload | Description | +|------------|---------|-------------| +| `conversationId` | `{ conversationId: string }` | Sent first for new conversations | +| `chunk` | `{ content: string }` | Text delta from agent response | +| `annotations` | `{ annotations: [...] }` | Citations (uri_citation, file_citation, etc.) | +| `usage` | `{ duration, promptTokens, completionTokens, totalTokens }` | Token metrics | +| `done` | `{}` | Stream complete | +| `error` | `{ message: string }` | Error occurred | + +## Project-Specific: Dev Logging + +Each state change prints (dev only): + +```text +🔄 [HH:MM:SS] ACTION_TYPE +Action: { … } +Changes: { field: before → after } +``` + +## Related Skills + +- **writing-csharp-code** - Backend coding standards and AgentFrameworkService patterns +- **writing-typescript-code** - Frontend React patterns and ChatService implementation +- **troubleshooting-authentication** - Token acquisition for authenticated streaming diff --git a/.github/skills/planning-features/SKILL.md b/.github/skills/planning-features/SKILL.md new file mode 100644 index 0000000..cda77a0 --- /dev/null +++ b/.github/skills/planning-features/SKILL.md @@ -0,0 +1,72 @@ +--- +name: planning-features +description: > + Provides structured plan template for feature implementation. + Use when planning new features, multi-file changes, or + creating implementation roadmaps before coding. +--- + +# Feature Planning + +> **Tip:** For read-only planning mode, press **Shift+Tab** in VS Code or Copilot CLI to enter built-in plan mode. + +## Research-First Workflow + +1. **Understand the request** — clarify scope and success criteria +2. **Search the codebase** for existing patterns and relevant files +3. **Load relevant skills** from `.github/skills/` for project conventions +4. **Generate a structured plan** using the template below + +### Research Guidelines + +- Search existing patterns before proposing new ones — reuse what the codebase already does +- Read relevant skills (e.g., `writing-csharp-code`, `writing-typescript-code`, `writing-bicep-templates`) to follow project conventions +- Check current implementations in files you plan to modify so the plan reflects actual code structure + +## Required Plan Structure + +Every plan must include these sections: + +### 1. Overview +2-3 sentence description of the feature or change. + +### 2. Requirements +```markdown +- [ ] Requirement one +- [ ] Requirement two +``` + +### 3. Files to Modify + +| File Path | Changes | +|-----------|---------| +| `path/to/file` | Description of modifications | + +### 4. Files to Create + +| File Path | Purpose | +|-----------|---------| +| `path/to/new/file` | What this file does | + +### 5. Implementation Steps +Numbered steps with sub-steps. Reference skill patterns where applicable. + +```markdown +1. Step one + - Sub-step referencing `writing-csharp-code` skill patterns + - Sub-step with specific code location +2. Step two + - Sub-step referencing `writing-typescript-code` skill patterns +``` + +### 6. Testing Checklist +```markdown +- [ ] Manual verification step one +- [ ] Manual verification step two +``` + +### 7. Edge Cases +Document each edge case and how the implementation should handle it. + +### 8. Documentation Updates +If the plan affects state machines, SSE events, or API endpoints, include updates to `ARCHITECTURE-FLOW.md`. Load the `understanding-architecture` skill for validation rules. diff --git a/.github/skills/researching-azure-ai-sdk/SKILL.md b/.github/skills/researching-azure-ai-sdk/SKILL.md new file mode 100644 index 0000000..ccb9b66 --- /dev/null +++ b/.github/skills/researching-azure-ai-sdk/SKILL.md @@ -0,0 +1,413 @@ +--- +name: researching-azure-ai-sdk +description: Provides research patterns for Foundry Agent Service SDK. Use when implementing agent features, looking up SDK methods, finding code samples, or troubleshooting Azure.AI.Projects API usage. +--- + +# Researching Azure AI SDK + +**CRITICAL**: Don't guess SDK usage. Follow this research workflow. + +## Subagent Delegation for Research + +**Multi-repo research blows up context** (1000+ tokens per file). Delegate to subagent for: +- Searching across 3+ repositories +- Reading 5+ files for patterns +- Comprehensive API surface exploration +- Finding all usages of a method/type + +### Delegation Pattern + +```text +runSubagent( + prompt: "RESEARCH task - do NOT write code. + + **Question**: [specific SDK question] + + **Search these sources in order**: + 1. Azure.AI.Projects SDK: github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Projects + 2. Azure.AI.Agents.Persistent samples: .../Azure.AI.Agents.Persistent/samples + 3. Microsoft Foundry Samples: github.com/microsoft-foundry/foundry-samples + + **Find**: + - Method signatures for [specific API] + - Usage examples (pseudocode only) + - Any gotchas or edge cases + + **Return** (max 20 lines): + - Key method name and signature + - Code pattern (pseudocode) + - File path where found (for later reference) + + Do NOT include full file contents.", + description: "SDK research: [topic]" +) +``` + +### When to Delegate vs Inline + +| Delegate to Subagent | Keep Inline | +|----------------------|-------------| +| Multi-repo code search | Local codebase grep | +| Finding all usages | Known method lookup | +| API surface exploration | Single file read | +| Pattern comparison | Quick signature check | +| Sample discovery | Using known pattern | + +## SDK Architecture Overview + +The Foundry Agent Service SDK has **two API surfaces** for agents: + +| API | Endpoint | ID Format | SDK Access | +|-----|----------|-----------|------------| +| **v2 Agents API** | `/agents/` | Human-readable (e.g., `dadjokes`) | `AIProjectClient.AgentAdministrationClient` | +| **OpenAI Assistants API** | `/assistants/` | OpenAI format (e.g., `asst_xxx`) | `PersistentAgentsClient` | + +**This project uses v2 Agents API** for human-readable agent IDs. + +```text +Azure.AI.Projects (Main Entry Point) +├── AIProjectClient +│ ├── .AgentAdministrationClient.GetAgentVersionAsync() → ProjectsAgentVersion (v2 Agents API) +│ ├── .GetPersistentAgentsClient() → PersistentAgentsClient (Assistants API) +│ └── .ProjectOpenAIClient.GetProjectResponsesClientForAgent() → ProjectResponsesClient (Responses API) +└── Companion packages: + ├── Azure.AI.Projects.Agents (ProjectsAgentVersion, DeclarativeAgentDefinition, …) + ├── Azure.AI.Extensions.OpenAI (ProjectConversationsClient, ProjectOpenAIClient, …) + └── OpenAI.Responses (streaming types) +``` + +## 1. Primary SDK Repository (Start Here) + +**Azure.AI.Projects SDK**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Projects + +- README: Core client patterns, authentication, basic operations +- Samples: `tests/Samples/` folder with full examples + +**Azure.AI.Agents.Persistent SDK**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Agents.Persistent + +- 33+ samples covering streaming, file search, Bing grounding, MCP, Azure Functions +- Key samples: + - `Sample9_PersistentAgents_Streaming.md` - Basic streaming pattern + - `Sample8_PersistentAgents_FunctionsWithStreaming.md` - Tool calls with streaming + - `Sample27_PersistentAgents_MCP_Streaming.md` - MCP server integration + +## 2. Official Quickstart Samples + +**Microsoft Foundry Samples**: https://github.com/microsoft-foundry/foundry-samples + +- `samples/csharp/quickstart/quickstart-chat-with-agent.cs` - **Responses API pattern** +- `samples/csharp/quickstart/` - Multiple quickstart examples + +**Key pattern from official quickstart**: +```csharp +AIProjectClient projectClient = new(new Uri(projectEndpoint), new AzureCliCredential()); +ProjectConversation conversation = projectClient.ProjectOpenAIClient.GetProjectConversationsClient().CreateProjectConversation(); +ProjectResponsesClient responsesClient = projectClient.ProjectOpenAIClient.GetProjectResponsesClientForAgent( + defaultAgent: agentName, + defaultConversationId: conversation.Id); +ResponseResult response = responsesClient.CreateResponse("Your prompt"); +``` + +## 3. Azure Architecture Center Samples + +**Baseline Chat App**: https://github.com/Azure-Samples/microsoft-foundry-baseline + +- Full production architecture with Entra ID auth +- `website/chatui/Controllers/ChatController.cs` - SSE streaming pattern + +**Basic Chat Example**: https://github.com/Azure-Samples/microsoft-foundry-basic + +- Simpler example of Foundry agent chat integration + +**Semantic Kernel + Foundry**: https://github.com/Azure-Samples/app-service-agentic-semantic-kernel-ai-foundry-agent + +- Integration pattern for Semantic Kernel with Foundry Agents + +## 4. UI Reference Samples (React Patterns) + +### Primary UI Reference + +**Azure AI Agents React Sample**: https://github.com/Azure-Samples/get-started-with-ai-agents + +This is the **primary UI reference** for this project. Many UI patterns were borrowed from here: +- Chat interface components +- Message rendering with citations/annotations +- Streaming text display +- Responsive layout patterns + +### Agent Framework DevUI (Python) + +**Agent Framework DevUI**: https://github.com/microsoft/agent-framework/tree/main/python/packages/devui + +Alternative UI patterns for agent development: +- Development-focused chat interface +- Multi-agent visualization +- Tool call debugging UI + +### UI Component Inspiration + +When implementing new UI features, check these sources in order: +1. `get-started-with-ai-agents` - React + TypeScript patterns for chat UI +2. `agent-framework/devui` - Development UI patterns +3. Fluent UI Copilot Components - Base component library (already used) + +## 5. Semantic Kernel Integration + +**Repository**: https://github.com/microsoft/semantic-kernel + +**Relevant paths**: +- `dotnet/src/Agents/OpenAI/` - OpenAI Responses API integration +- `dotnet/samples/GettingStartedWithAgents/AzureAIAgent/` +- `dotnet/samples/Concepts/Agents/` (Step##_*.cs files) + +## 6. OpenAI .NET SDK (Streaming Types) + +**Repository**: https://github.com/openai/openai-dotnet + +- `docs/guides/streaming-responses/` - Streaming patterns +- Source of `StreamingResponseOutputTextDeltaUpdate` and related types + +## 7. GitHub Code Search (For Specific Patterns) + +Use GitHub search to find usage examples: + +```text +# Find streaming patterns +"StreamingResponseOutputTextDeltaUpdate language:csharp" + +# Find Responses API usage +"ProjectResponsesClient CreateResponseStreamingAsync language:csharp" + +# Find conversation patterns +"ProjectConversation GetProjectResponsesClientForAgent language:csharp" +``` + +## Current SDK Packages + +| Package | Purpose | +|---------|---------| +| `Azure.AI.Projects` | Main entry point, `AIProjectClient`, v2 Agents API, Responses API | +| `Azure.Identity` | Authentication (`AzureDeveloperCliCredential`, `ManagedIdentityCredential`) | +| `Microsoft.Identity.Web` | JWT Bearer authentication for API | + +**Note**: Check `WebApp.Api.csproj` for current versions. This project requires `Azure.AI.Projects` GA with v2 Agents API support (`AIProjectClient.AgentAdministrationClient`). + +**Companion packages used**: +- `Azure.AI.Projects.Agents` — `ProjectsAgentVersion`, `ProjectsAgentRecord`, `DeclarativeAgentDefinition` / `HostedAgentDefinition` / `WorkflowAgentDefinition`, `AgentAdministrationClient` +- `Azure.AI.Extensions.OpenAI` — `ProjectOpenAIClient`, `ProjectConversationsClient`, `ProjectResponsesClient`, `ProjectConversation` +- `OpenAI.Responses` — streaming types + +**Agent Framework (Microsoft.Agents.AI.AzureAI)**: Not referenced. As of rc5, incompatible with `Azure.AI.Projects 2.0.0` GA. See "Compatibility blocker" below. + +**Key Resources**: +- NuGet (Azure.AI.Projects): https://www.nuget.org/packages/Azure.AI.Projects +- SDK Source: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Projects +- v2 Migration Guide: https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/migrate +- API Reference: https://learn.microsoft.com/en-us/dotnet/api/azure.ai.projects +- Product Docs: https://learn.microsoft.com/azure/ai-studio/ +- Infrastructure Bicep Templates: https://github.com/microsoft-foundry/foundry-samples/tree/main/infrastructure/infrastructure-setup-bicep + +## Official Azure AI Foundry Agent Service Documentation + +**Start here** when researching agent capabilities, limits, or new features: + +| Topic | URL | +|-------|-----| +| **Agent Service overview** | https://learn.microsoft.com/azure/ai-foundry/agents/overview | +| **Quickstart: Create an agent** | https://learn.microsoft.com/azure/ai-foundry/agents/quickstart | +| **Agent concepts & architecture** | https://learn.microsoft.com/azure/ai-foundry/agents/concepts | +| **Supported models** | https://learn.microsoft.com/azure/ai-foundry/agents/concepts/supported-models | +| **Tools: File Search** | https://learn.microsoft.com/azure/ai-foundry/agents/how-to/tools/file-search | +| **Tools: Code Interpreter** | https://learn.microsoft.com/azure/ai-foundry/agents/how-to/tools/code-interpreter | +| **Tools: Bing Grounding** | https://learn.microsoft.com/azure/ai-foundry/agents/how-to/tools/bing-grounding | +| **Tools: Azure AI Search** | https://learn.microsoft.com/azure/ai-foundry/agents/how-to/tools/azure-ai-search | +| **Tools: Azure Functions** | https://learn.microsoft.com/azure/ai-foundry/agents/how-to/tools/azure-functions | +| **MCP server tools** | https://learn.microsoft.com/azure/ai-foundry/agents/how-to/tools/mcp-servers | +| **v2 Agents API migration** | https://learn.microsoft.com/azure/ai-foundry/agents/how-to/migrate | +| **REST API reference** | https://learn.microsoft.com/rest/api/azureai/agents | +| **Quotas & limits** | https://learn.microsoft.com/azure/ai-foundry/agents/concepts/quotas-limits | + +**Agent Framework (Microsoft.Agents) docs**: + +| Topic | URL | +|-------|-----| +| **Agent Framework overview** | https://learn.microsoft.com/microsoft-agents/overview | +| **Agent Framework .NET SDK** | https://github.com/microsoft/Agents-for-net | +| **NuGet package** | https://www.nuget.org/packages/Microsoft.Agents.AI.AzureAI | +| **IChatClient abstraction** | https://learn.microsoft.com/dotnet/api/microsoft.extensions.ai.ichatclient | + +## Annotation Types in Responses + +The SDK provides several annotation types for citations (from `OpenAI.Responses` namespace): + +| Type | Class | Use Case | Key Properties | +|------|-------|----------|----------------| +| URI Citation | `UriCitationMessageAnnotation` | Bing, Azure AI Search, SharePoint | `Uri`, `Title`, `StartIndex`, `EndIndex` | +| File Citation | `FileCitationMessageAnnotation` | File search (vector stores) | `FileId`, `Filename`, `Index` | +| File Path | `FilePathMessageAnnotation` | Code interpreter output | `FileId`, `Index` | +| Container Citation | `ContainerFileCitationMessageAnnotation` | Container file citations | `FileId`, `Filename`, `ContainerId`, `StartIndex`, `EndIndex` | + +**Note**: `FileCitationMessageAnnotation` uses `Index` (not `StartIndex`/`EndIndex`) per the SDK. See `ExtractAnnotations()` in `AgentFrameworkService.cs` for mapping to `AnnotationInfo`. + +### Container File Download + +The C# SDK does not yet have a typed client for container file downloads. Use the REST API directly with a bearer token scoped to `https://ai.azure.com/.default`: + +``` +GET {projectEndpoint}/openai/v1/containers/{containerId}/files/{fileId}/content +Authorization: Bearer {token} +``` + +For standard (non-container) files (`cfile_` prefix absent), use `OpenAI.Files.FileClient` instead. The backend endpoint `GET /api/files/{fileId}?containerId={id}` abstracts this: it routes `cfile_`-prefixed files through the REST API and standard files through `FileClient`. + +## Streaming Response Types (from OpenAI.Responses namespace) + +| Type | Purpose | +|------|---------| +| `StreamingResponseOutputTextDeltaUpdate` | Text content delta chunks | +| `StreamingResponseOutputItemDoneUpdate` | Item completion signals | +| `StreamingResponseCompletedUpdate` | Response completion with usage | +| `ResponseItem` | Base type for response items | + +**Pattern used in this project**: +```csharp +await foreach (var update in responsesClient.CreateResponseStreamingAsync(...)) +{ + if (update is StreamingResponseOutputTextDeltaUpdate textUpdate) + yield return new StreamChunk { Text = textUpdate.Delta }; + if (update is StreamingResponseOutputItemDoneUpdate itemDone) + // Extract annotations from itemDone.Item +} +``` + +## Microsoft Agent Framework (NOT used — see rationale) + +**Package**: `Microsoft.Agents.AI.AzureAI` (prerelease, not referenced) + +**Status**: ❌ **Not installed.** Blocked on compatibility with `Azure.AI.Projects 2.0.0` GA. + +### Compatibility blocker (as of rc5) + +`Microsoft.Agents.AI.AzureAI 1.0.0-rc5` pins `Azure.AI.Projects 2.0.0-beta.2` and references types removed in the GA release. Attempting to use rc5 with `Azure.AI.Projects 2.0.0` throws `TypeLoadException` at runtime. Re-evaluate when rc6+ ships. + +### Why we use the direct SDK anyway + +Even when the compat blocker is lifted, this project's streaming path needs direct access to `ProjectResponsesClient` and typed response items that are not surfaced by the `IChatClient` abstraction: + +- `McpToolCallApprovalRequestItem` for MCP approval flows +- `FileSearchCallResponseItem` for file search quotes +- `MessageResponseItem.OutputTextAnnotations` for citations +- `ResponseItem.CreateMcpApprovalResponseItem()` to respond to MCP approvals + +Routing through `ChatClientAgent.RunStreamingAsync()` would require casting `RawRepresentation` for each of these, which defeats the abstraction benefit. + +### Current pattern (direct SDK only) + +```csharp +// Agent metadata — no "latest" keyword in the REST spec; enumerate versions descending. +ProjectsAgentVersion? agentVersion = null; +await foreach (var v in projectClient.AgentAdministrationClient.GetAgentVersionsAsync( + agentName: agentName, + limit: 1, + order: AgentListOrder.Descending, + after: null, + before: null, + cancellationToken: ct)) +{ + agentVersion = v; + break; +} +var definition = agentVersion?.Definition as DeclarativeAgentDefinition; +string model = definition?.Model ?? ""; +string instructions = definition?.Instructions ?? ""; + +// Streaming — pin the resolved version so streaming hits the same version as metadata. +ProjectResponsesClient responsesClient = projectClient.ProjectOpenAIClient + .GetProjectResponsesClientForAgent( + new AgentReference(agentId, agentVersion?.Version), + conversationId); +await foreach (var update in responsesClient.CreateResponseStreamingAsync(...)) { } +``` + +## Migration Notes + +`AIProjectClient` requires a project endpoint URI (not a connection string): + +```csharp +var projectClient = new AIProjectClient(new Uri(projectEndpoint), new DefaultAzureCredential()); +``` + +Connection-string constructors are deprecated. See: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/ai/Azure.AI.Projects/AGENTS_MIGRATION_GUIDE.md + +## Additional SDK Resources + +### Fetch SDK Source from GitHub (Authoritative) + +Type definitions live in these repos—read them directly: + +- **Azure.AI.Projects source**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Projects/src +- **Azure.AI.Agents.Persistent samples**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Agents.Persistent/samples +- **OpenAI.Responses types**: https://github.com/openai/openai-dotnet/tree/main/src + + + +### GitHub Code Search + +Search across all .NET codebases for real-world usage: + +```text +"ProjectResponsesClient CreateResponseStreamingAsync" language:csharp +"StreamingResponseOutputTextDeltaUpdate" language:csharp +``` + +This finds how other projects use these APIs, revealing patterns and edge cases. + +### PowerShell Reflection (When Docs Lag Behind) + +Use when SDK docs are outdated or incomplete — the DLLs are the ground truth. + +Works even when `dotnet build` fails (loads from NuGet cache): + +```powershell +cd backend/WebApp.Api; dotnet restore + +# Option A: Load from build output (requires successful build) +$asm = [Reflection.Assembly]::LoadFrom((Resolve-Path "bin/Debug/net10.0/Azure.AI.Projects.dll")) + +# Option B: Load from NuGet cache (works even if build fails — use for pre-release migrations) +$dll = Get-ChildItem "$env:USERPROFILE\.nuget\packages\azure.ai.projects" -Recurse -Filter "Azure.AI.Projects.dll" | Select-Object -Last 1 +$asm = [Reflection.Assembly]::LoadFrom($dll.FullName) + +# Find types matching a pattern +$asm.GetExportedTypes() | Where-Object { $_.Name -like "*Streaming*" } | ForEach-Object { $_.FullName } + +# Get method signatures with parameter details +$type = $asm.GetType("Azure.AI.Extensions.OpenAI.ProjectResponsesClient") +$type.GetMethods() | Where-Object { $_.Name -like "*Async*" } | Select-Object Name, ReturnType, @{N='Params';E={($_.GetParameters() | ForEach-Object { "$($_.ParameterType.Name) $($_.Name)" }) -join ', '}} +``` + +**Agent Framework assemblies** (for Microsoft.Agents.AI.AzureAI migrations): + +```powershell +# Load Agent Framework DLL from NuGet cache +$pkg = Get-ChildItem "$env:USERPROFILE\.nuget\packages\microsoft.agents.ai.azureai" -Recurse -Filter "Microsoft.Agents.AI.AzureAI.dll" | Select-Object -Last 1 +$asm = [Reflection.Assembly]::LoadFrom($pkg.FullName) + +# Dump all exported types to see what changed between versions +$asm.GetExportedTypes() | ForEach-Object { $_.FullName } | Sort-Object + +# Check if types you depend on still exist +@("ChatClientAgent", "AgentVersion", "PromptAgentDefinition", "AgentReference") | ForEach-Object { + $match = $asm.GetExportedTypes() | Where-Object { $_.Name -eq $_ } + if ($match) { Write-Host "FOUND: $($match.FullName)" } else { Write-Host "MISSING: $_" -ForegroundColor Red } +} + +# Inspect extension methods (GetAIAgentAsync, etc.) +$asm.GetExportedTypes() | Where-Object { $_.GetMethods([Reflection.BindingFlags]::Static -bor [Reflection.BindingFlags]::Public) | Where-Object { $_.IsDefined([Runtime.CompilerServices.ExtensionAttribute], $false) } } | ForEach-Object { + $_.GetMethods() | Where-Object { $_.IsDefined([Runtime.CompilerServices.ExtensionAttribute], $false) } | ForEach-Object { Write-Host "$($_.DeclaringType.Name).$($_.Name)" } +} +``` + +**When to use**: SDK upgrade with breaking changes, pre-release packages where docs lag, verifying actual API surface before writing migration code. + +**Key insight**: Load from NuGet cache (`$env:USERPROFILE\.nuget\packages\`) to inspect the *new* version's types even when the build is broken. diff --git a/.github/skills/reviewing-documentation/SKILL.md b/.github/skills/reviewing-documentation/SKILL.md new file mode 100644 index 0000000..2715cf2 --- /dev/null +++ b/.github/skills/reviewing-documentation/SKILL.md @@ -0,0 +1,102 @@ +--- +name: reviewing-documentation +description: > + Provides documentation audit checklists and quality standards. + Use when reviewing README files, SKILL.md files, agent definitions, + copilot-instructions.md, or ARCHITECTURE-FLOW.md for quality and consistency. +--- + +# Documentation Review Guidance + +Audit, improve, and maintain documentation quality across the repository. Search for doc files — don't rely on hardcoded paths. + +## Architecture Maintenance + +For `ARCHITECTURE-FLOW.md` updates, load `.github/skills/understanding-architecture/SKILL.md` first — it has validation commands and source-of-truth mappings. + +## SKILL.md Quality Gates + +**Naming rules** (agentskills.io spec): +- ✅ `writing-csharp-code` (lowercase, hyphens, max 64 chars) +- ❌ `WritingCSharpCode` | `writing--csharp` | `-writing-code` + +**Required frontmatter**: `name` (must match directory name), `description` (must explain WHAT and WHEN, max 1024 chars) + +**Body must include**: goal statement, practical code examples, common mistakes, related skill cross-references. + +## .agent.md Quality Gates + +**Required frontmatter**: `name`, `description`, `argument-hint`, `tools`, `model` + +**Verify**: `handoffs` reference valid agent `name` values from other agent files. + +## copilot-instructions.md + +Loads on every request — keep it lean. Must have: architecture quick reference, dev commands, agents table, skills table. + +## Audit Checklists + +### ARCHITECTURE-FLOW.md +- [ ] Mermaid diagrams render without syntax errors +- [ ] State machines match `appState.ts` type definitions +- [ ] SSE events match `Program.cs` Write*Event methods +- [ ] Actions match `AppAction` type union + +### README.md +- [ ] Quick start works for new users +- [ ] Commands table is accurate +- [ ] Links to sub-READMEs resolve + +### Skills & Agents +- [ ] Valid YAML frontmatter on all files +- [ ] SDK versions in examples match `*.csproj` / `package.json` +- [ ] Cross-references between skills are valid + +### Cross-Document Consistency +- [ ] Architecture tables match across all docs +- [ ] Port numbers consistent (5173, 8080) +- [ ] SDK versions match actual dependencies + +## Review Output Format + +For each document reviewed: + +**Document**: `path/to/file.md` — ✅ Good | ⚠️ Needs Improvement | ❌ Broken + +**Issues**: numbered list with suggested fixes + +**Cross-Reference Issues**: broken links, version mismatches + +## Content Quality Rules + +All documentation must state **how things work now**, not history or rationale for past changes. + +### Must NOT contain +- **Version numbers in prose** — reference `*.csproj` or `package.json` instead. Hardcoded versions go stale silently. +- **Issue/PR tracker links** — these are PR artifacts, not reference docs. Exception: links to external tracking if explicitly requested. +- **Duplicate information** — if a fact is stated in one section, don't restate it in another. Cross-reference instead. +- **Narrative or history** — "we changed X because Y happened" belongs in commit messages, not docs. State the current behavior. +- **"Fallback" or "automatic" language** for mutually exclusive paths — if two modes are mutually exclusive, say so explicitly. Don't imply graceful degradation that doesn't exist. +- **Stale comments** — `// TODO (if added)` when the thing exists, `// backward compat` without explaining what it's compatible with. + +### Must contain (for AI agent effectiveness) +- **Non-obvious constraints** — things an AI agent would get wrong by reading only the code (e.g., "consent to Azure ML Services, not Cognitive Services") +- **Mutually exclusive choices** — state which modes exist and that they don't mix +- **Error signatures** — what error an AI agent will see if it does the wrong thing (e.g., `AADSTS65001`, `AADSTS500131`) +- **File-to-concept mapping** — which file controls which behavior, so agents don't search blindly + +### Package/SDK version references +- Tables with versions: remove the version column, point to source of truth +- Prose references: say "pre-release" or "beta" without specific version numbers +- If a version matters for API compatibility, state the minimum version and why + +## Style Rules + +- Code blocks: always include language identifier (` ```typescript ` not ` ``` `) +- Links: relative paths for internal, absolute for external +- Tables: consistent column widths + +## Constraints + +- ❌ Don't change code (only docs) +- ❌ Don't invent features not in codebase diff --git a/.github/skills/syncing-mcp-servers/SKILL.md b/.github/skills/syncing-mcp-servers/SKILL.md new file mode 100644 index 0000000..b7a8147 --- /dev/null +++ b/.github/skills/syncing-mcp-servers/SKILL.md @@ -0,0 +1,125 @@ +--- +name: syncing-mcp-servers +description: Synchronize MCP server configuration between VS Code (.vscode/mcp.json) and Copilot CLI (~/.copilot/mcp-config.json). Use when setting up Copilot CLI for the first time, when MCP servers are added or changed in the repo, or when a user reports missing MCP tools in CLI. +--- + +# Syncing MCP Servers + +This skill ensures MCP server configurations stay in sync between VS Code (repo-level) and Copilot CLI (user-level). + +## Why This Exists + +VS Code and Copilot CLI use **different config files with different JSON formats** for MCP servers. Neither reads the other's config. This skill bridges that gap. + +| Platform | Config File | Top-Level Key | Scope | +|----------|------------|---------------|-------| +| **VS Code** | `.vscode/mcp.json` | `"servers"` | Per-workspace (repo, checked into git) | +| **Copilot CLI** | `~/.copilot/mcp-config.json` | `"mcpServers"` | Per-user (global, all repos) | + +## Format Differences + +### VS Code format (`.vscode/mcp.json`) +```json +{ + "servers": { + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest", "--viewport-size=1024,768"] + }, + "microsoftdocs": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + } + } +} +``` + +### CLI format (`~/.copilot/mcp-config.json`) +```json +{ + "mcpServers": { + "playwright": { + "type": "local", + "command": "npx", + "tools": ["*"], + "args": ["@playwright/mcp@latest", "--viewport-size=1024,768"] + }, + "microsoftdocs": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + } + } +} +``` + +### Key differences: +1. **Top-level key**: `"servers"` (VS Code) vs `"mcpServers"` (CLI) +2. **Local servers**: CLI requires `"type": "local"` — VS Code infers it from `"command"` +3. **Tool filtering**: CLI supports `"tools": ["*"]` (or specific tool names) — VS Code uses `"tools"` differently in its config +4. **Comments**: VS Code's `mcp.json` supports JSON with comments (JSONC) — CLI does not + +## Workflow + +### Step 1 — Read the source of truth + +Read `.vscode/mcp.json` from the repo. This is the canonical config, maintained by the team and checked into git. + +### Step 2 — Read the user's CLI config + +Read `~/.copilot/mcp-config.json`. This may contain servers from other projects — **do not overwrite them**. + +On Windows: `$env:USERPROFILE\.copilot\mcp-config.json` +On macOS/Linux: `~/.copilot/mcp-config.json` + +The location can be overridden by the user via `--config-dir` flag or `XDG_CONFIG_HOME` env var. Check if either is set. + +### Step 3 — Convert and merge + +For each server in `.vscode/mcp.json`: + +1. **Local servers** (have `"command"`): Add `"type": "local"` and `"tools": ["*"]` for CLI format +2. **HTTP servers** (have `"type": "http"`): Copy as-is — format is identical +3. **Skip** any server that already exists in the CLI config with matching `"command"` and `"args"` (or matching `"url"` for HTTP types) +4. **Warn** if an existing CLI server has the same name but different config — ask the user whether to overwrite + +### Step 4 — Write the updated CLI config + +Write the merged config back to `~/.copilot/mcp-config.json`. Preserve JSON formatting (2-space indent). + +### Step 5 — Verify + +Run `copilot --prompt "list available MCP tools" --allow-all-tools -p "run /mcp show and exit"` or instruct the user to run `/mcp show` in their next CLI session to confirm the servers are available. + +## Important Rules + +1. **Never delete** servers from the CLI config that aren't in the repo's VS Code config — the user may have servers from other projects +2. **Always back up** the CLI config before writing: copy to `mcp-config.json.bak` +3. **Strip comments** when reading VS Code's JSONC file — parse it as JSONC, not strict JSON +4. **Preserve user's existing servers** — merge, don't replace +5. **Report what changed** — list servers added, updated, or skipped + +## Reverse Sync (CLI → VS Code) + +If the user added new MCP servers via CLI (`/mcp add`) that should also be available in VS Code: + +1. Read `~/.copilot/mcp-config.json` for any servers not in `.vscode/mcp.json` +2. Convert: remove `"type": "local"` and `"tools"` fields (VS Code infers these) +3. Add to `.vscode/mcp.json` under `"servers"` key +4. Preserve existing comments in the JSONC file — add new servers at the end of the `"servers"` block + +## Future-Proofing + +The CLI and VS Code MCP config formats may converge in the future. Before syncing: + +1. **Check if CLI now reads `.vscode/mcp.json`** — look for a `.vscode/mcp.json` or `.github/copilot/mcp.json` entry in CLI docs or `copilot help` output +2. **Check if the top-level key has changed** — CLI may adopt `"servers"` or VS Code may adopt `"mcpServers"` +3. **Check for a unified config path** — a `.github/mcp.json` or similar repo-level config that both platforms read + +If any of these converge, update this skill and simplify the sync process. + +## Reference + +- [VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) +- [CLI MCP docs](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli#add-an-mcp-server) +- [Custom agents config (tools + MCP)](https://docs.github.com/en/copilot/reference/custom-agents-configuration) +- CLI flag: `--additional-mcp-config @` — loads extra MCP config for one session without modifying `~/.copilot/mcp-config.json` diff --git a/.github/skills/testing-cli-compatibility/SKILL.md b/.github/skills/testing-cli-compatibility/SKILL.md new file mode 100644 index 0000000..46241d4 --- /dev/null +++ b/.github/skills/testing-cli-compatibility/SKILL.md @@ -0,0 +1,79 @@ +--- +name: testing-cli-compatibility +description: Validate that Copilot CLI can see all repo skills, MCP servers, and custom instructions. Use when setting up CLI for the first time, after adding skills, or to diagnose CLI issues. +--- + +# Testing CLI Compatibility + +Validates that the standalone [Copilot CLI](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli) works correctly with this repo's skills and MCP servers. + +## Quick Start + +```powershell +# Run all tests (including MCP) +./deployment/scripts/test-cli-compatibility.ps1 + +# Skip MCP server tests (faster, no tool approval needed) +./deployment/scripts/test-cli-compatibility.ps1 -SkipMcp +``` + +## What It Tests + +| # | Test | What It Checks | +|---|------|----------------| +| 1 | **CLI Version** | `copilot` command is installed and responds | +| 2 | **Custom Instructions** | `.github/copilot-instructions.md` is loaded (identifies project name) | +| 3 | **Skills** | All `.github/skills/*/SKILL.md` files are visible (count auto-detected) | +| 4 | **MCP Servers** | `playwright` and `microsoftdocs` servers are connected | + +## Prerequisites + +1. **Copilot CLI installed**: `npm install -g @anthropic-ai/copilot-cli` or via GitHub +2. **Authenticated**: Run `copilot login` if not already logged in +3. **MCP servers synced**: Run the `syncing-mcp-servers` skill first if MCP tests fail + +## How It Works + +The script uses `copilot -p "" -s --no-auto-update` to invoke the CLI in non-interactive mode: + +- `-p` — Single prompt, exits after response +- `-s` — Silent mode, output only the agent response (no stats) +- `--no-auto-update` — Skip update check for faster execution +- `--allow-all-tools` — Used only for MCP test (needs tool access) + +Each test sends a prompt asking the CLI to list what it can see, then validates the response against expected values. + +## When to Run + +- After adding or modifying skills in `.github/skills/` +- After syncing MCP servers with the `syncing-mcp-servers` skill +- When a user reports CLI issues (missing agents, skills, or tools) +- As a smoke test after CLI updates (`copilot update`) + +## Manual Testing + +If the script isn't available, you can test manually: + +```powershell +# Check version +copilot --version + +# Test custom instructions +copilot -p "What project is this repo for?" -s + +# Test skills +copilot -p "List all custom skills" -s + +# Test MCP (needs --allow-all-tools) +copilot -p "List all MCP servers" -s --allow-all-tools +``` + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| `copilot` not found | Install CLI or add to PATH | +| Custom instructions not loaded | Ensure you're in the repo directory | +| Agents missing | No custom agents expected — this repo uses skills + copilot-instructions only | +| Skills missing | Check `.github/skills/*/SKILL.md` files exist with valid frontmatter | +| MCP servers not connected | Run `syncing-mcp-servers` skill to sync `.vscode/mcp.json` → `~/.copilot/mcp-config.json` | diff --git a/.github/skills/testing-with-playwright/SKILL.md b/.github/skills/testing-with-playwright/SKILL.md new file mode 100644 index 0000000..0e66d62 --- /dev/null +++ b/.github/skills/testing-with-playwright/SKILL.md @@ -0,0 +1,145 @@ +--- +name: testing-with-playwright +description: Provides Playwright MCP testing workflow for the web application. Use when testing UI changes, verifying chat functionality, debugging frontend issues, or validating state transitions in the browser. +--- + +# Testing with Playwright MCP + +**CRITICAL**: Always test changes before completion. + +## Subagent Delegation for Testing + +**Screenshot operations blow up context fast** (~2000-5000 tokens each). Delegate to subagent for: +- Multi-step UI test scenarios +- Visual regression verification +- Accessibility audits +- Screenshot-heavy debugging + +### Delegation Pattern + +```text +runSubagent( + prompt: "TESTING task with Playwright MCP. + + **Servers**: Frontend at localhost:5173, Backend at localhost:8080 + + **Test Scenario**: [describe what to verify] + + **Steps**: + 1. Navigate to http://localhost:5173 + 2. [specific actions to perform] + + **Check in priority order** (stop when answer found): + 1. Browser console logs - look for errors, state transitions (🔄) + 2. Network requests - verify API calls and status codes + 3. Accessibility snapshot - check element presence + 4. Screenshot - ONLY if visual verification essential + + **Return**: + - Pass/Fail result + - Specific evidence (error messages, status codes, element presence) + - Console action log if relevant (🔄 ACTION_TYPE entries) + + Do NOT include raw screenshots or full accessibility dumps.", + description: "Test: [what being verified]" +) +``` + +### When to Delegate vs Inline + +| Delegate to Subagent | Keep Inline | +|----------------------|-------------| +| Multi-page flows | Single console check | +| Visual verification needed | Network status check | +| Accessibility audit | Element presence (single) | +| Error reproduction | Quick state verification | +| Screenshot required | Reading console logs | + +## Testing Priority (Token efficiency) + +1. **Browser console logs** - Check console messages for state transitions and errors +2. **VS Code terminal logs** - Check terminal output for backend/frontend server logs +3. **Network requests** - Inspect API calls and status codes +4. **Accessibility snapshot** - Get DOM structure for element verification +5. **Screenshots** - Visual verification (use sparingly, high token cost) + +**Key insight**: Browser console shows React state changes (🔄 ACTION_TYPE), errors, and warnings. VS Code terminals show server logs and compilation output. + +## Workflow + +**Start servers in VS Code terminals** (preferred - logs visible to AI agent): +```powershell +# Run these VS Code tasks: +# - "Backend: ASP.NET Core API" (dotnet watch, port 8080) +# - "Frontend: React Vite" (npm run dev, port 5173) +# Or use compound task: "Start Dev (VS Code Terminals)" +``` + +**Then test with Playwright MCP**: +1. Navigate to http://localhost:5173 +2. Check browser console for state transitions and errors +3. Verify network requests for API calls +4. Take accessibility snapshot for DOM validation + +**Check server logs**: +- Check VS Code terminal output for backend compilation and request logs +- Backend terminal shows: request handling, errors, recompilation status +- Frontend terminal shows: HMR updates, build warnings, Vite output + +## When to Test + +- After UI component or API endpoint changes +- Before committing multi-step implementations +- When user reports issues + +## Validation Checklist + +- [ ] Console shows expected actions (🔄 [timestamp] ACTION_TYPE) +- [ ] No console errors/warnings +- [ ] Network tab shows correct status codes (200/400/401/500) +- [ ] DOM elements present in accessibility snapshot + +## State Logging (Dev Mode) + +Each state change prints: +```text +🔄 [HH:MM:SS] ACTION_TYPE +Action: { … } +Changes: { field: before → after } +``` + +## Project-Specific: Key Test Scenarios + +| Scenario | What to Verify | +|----------|----------------| +| Initial load | Auth redirect, agent metadata loads | +| Send message | User message appears, streaming starts | +| Streaming | Text chunks append, no flicker | +| Annotations | Citations render with links | +| Cancel stream | Input re-enabled, status → idle | +| Error recovery | Retry button works, error clears | + +## Project-Specific: Network Verification + +| Endpoint | Success | Failure | +|----------|---------|---------| +| `POST /api/chat/stream` | 200 + SSE events | 401 (auth), 400 (validation) | +| `GET /api/agent/info` | 200 + JSON metadata | 500 (agent not found) | + +## Playwright MCP + +| Capability | Token Cost | +|------------|------------| +| Navigate | Low | +| Console logs | Low | +| Click / Type | Low | +| Accessibility snapshot | Medium | +| Screenshot | High | + +Use console logs for state verification. Use snapshots for element presence. Avoid screenshots unless visual check required. + +## Related Skills + +- **validating-ui-features** - Detailed test procedures for specific features +- **writing-typescript-code** - Frontend patterns and state management +- **implementing-chat-streaming** - SSE flow verification diff --git a/.github/skills/triaging-issues/SKILL.md b/.github/skills/triaging-issues/SKILL.md new file mode 100644 index 0000000..0158e1a --- /dev/null +++ b/.github/skills/triaging-issues/SKILL.md @@ -0,0 +1,97 @@ +--- +name: triaging-issues +description: > + Provides issue triage workflow, priority definitions, and report format. + Use when analyzing GitHub issues and PRs, assessing priority, + or generating triage reports. +--- + +# Issue Triage Guidance + +Gather open issues and PRs, analyze them, and recommend next steps. Search the codebase to identify affected components — don't rely on hardcoded file mappings. + +## Priority Definitions + +| Priority | Criteria | +|----------|----------| +| 🔴 **Critical** | Production broken, security issue, data loss | +| 🟠 **High** | Major feature broken, blocking users | +| 🟡 **Medium** | Feature degraded, workaround exists | +| 🟢 **Low** | Minor issue, cosmetic, nice-to-have | + +## Complexity Scale + +| Size | Estimate | +|------|----------| +| **S** | Hours | +| **M** | 1-2 days | +| **L** | 3-5 days | +| **XL** | 1+ week | + +## Label Taxonomy + +| Prefix | Values | +|--------|--------| +| `area:` | `backend`, `frontend`, `auth`, `streaming`, `infra`, `docs` | +| `type:` | `bug`, `feature`, `enhancement`, `chore`, `docs` | +| `priority:` | `critical`, `high`, `medium`, `low` | + +## Triage Report Format + +### Open Issues Summary + +| # | Title | Priority | Type | Complexity | +|---|-------|----------|------|------------| + +### Per Issue — #[number]: [title] + +- **Priority** / **Type** / **Complexity** +- **Affected Areas**: checklist of areas +- **Relevant Files**: table with file + reason +- **Suggested Labels**: using taxonomy above +- **Recommended Next Steps**: numbered list + +### Open PRs Summary + +| # | Title | Author | Status | Files Changed | +|---|-------|--------|--------|---------------| + +### Per PR — #[number]: [title] + +- **Status**: Draft | Ready for review | Changes requested | Approved +- **Review Notes**: concerns or things to check + +## gh CLI Commands for Read-Only Analysis + +```bash +# List open issues +gh issue list --state open + +# Search for potential duplicates +gh issue list --search "keyword" + +# View issue details +gh issue view + +# List open PRs +gh pr list --state open + +# View PR details and diff +gh pr view +gh pr diff +``` + +## Duplicate Detection + +Before deep-diving into an issue, search for duplicates: + +1. Use `gh issue list --search ""` with key terms from the issue title and body. +2. Check closed issues too: `gh issue list --state closed --search ""`. +3. If a duplicate exists, note it in the triage report with a cross-reference. + +## Constraints + +- ❌ **Read-only** — do NOT modify issues or PRs +- ❌ No `gh issue edit`, `gh issue close`, `gh issue label` +- ❌ No `gh pr merge`, `gh pr close`, `gh pr review --approve` +- ✅ Only use read commands: `list`, `view`, `search`, `diff` diff --git a/.github/skills/troubleshooting-authentication/SKILL.md b/.github/skills/troubleshooting-authentication/SKILL.md new file mode 100644 index 0000000..d17e532 --- /dev/null +++ b/.github/skills/troubleshooting-authentication/SKILL.md @@ -0,0 +1,93 @@ +--- +name: troubleshooting-authentication +description: Provides authentication troubleshooting for MSAL, JWT, and Entra ID. Use when debugging 401 errors, token issues, MSAL configuration problems, or credential failures in this repository. +--- + +# Authentication Troubleshooting + +## Architecture + +1. Browser → MSAL.js (PKCE flow) → JWT with `Chat.ReadWrite` scope +2. Frontend → Backend (JWT Bearer token) +3. Backend → Foundry Agent Service (ManagedIdentityCredential) + +## Common Issues + +| Issue | Cause | Fix | +|-------|-------|-----| +| 401 on `/api/*` | Token missing scope | Verify `Chat.ReadWrite` scope in token | +| `ManagedIdentityCredential` error locally | Wrong environment | Set `ASPNETCORE_ENVIRONMENT=Development` | +| Token popup blocked | Browser settings | Allow popups for localhost | +| Silent token fails | No cached token | Fallback to popup (handled by useAuth) | + +## Backend: JWT Validation + +Accepts both audience formats: + +```csharp +options.TokenValidationParameters.ValidAudiences = new[] +{ + builder.Configuration["AzureAd:ClientId"], + $"api://{builder.Configuration["AzureAd:ClientId"]}" +}; +``` + +## Backend: Credential Strategy + +```csharp +TokenCredential credential = env.IsDevelopment() + ? new ChainedTokenCredential( + new AzureCliCredential(), + new AzureDeveloperCliCredential()) // Supports 'azd auth login' + : new ManagedIdentityCredential(); +``` + +**Local development**: Requires `az login` or `azd auth login` to work. + +**Why ChainedTokenCredential**: Avoids `DefaultAzureCredential`'s unpredictable "fail fast" mode. Provides explicit, debuggable credential chain. + +## Frontend: MSAL Pattern + +```typescript +// Always try silent first +try { + const { accessToken } = await instance.acquireTokenSilent({ + ...tokenRequest, + account: accounts[0] + }); + return accessToken; +} catch { + // Fallback to popup + const { accessToken } = await instance.acquireTokenPopup(tokenRequest); + return accessToken; +} +``` + +## Debugging Steps + +1. **Check token contents**: https://jwt.ms +2. **Verify scope**: Token should have `Chat.ReadWrite` +3. **Check audience**: Should match client ID or `api://{clientId}` +4. **Verify Entra app**: Check redirect URIs in Azure Portal + +## Environment Variables + +**Frontend** (`.env.local`): +```ini +VITE_ENTRA_SPA_CLIENT_ID=... +VITE_ENTRA_TENANT_ID=... +``` + +**Backend** (`.env`): +```ini +AzureAd__ClientId=... +AzureAd__TenantId=... +``` + +**Regenerate**: Run `azd up` to recreate Entra app and `.env` files. + +## Related Skills + +- **writing-csharp-code** - Backend JWT validation and credential patterns +- **writing-typescript-code** - Frontend MSAL integration and useAuth hook +- **deploying-to-azure** - Entra app provisioning and RBAC configuration diff --git a/.github/skills/understanding-architecture/SKILL.md b/.github/skills/understanding-architecture/SKILL.md new file mode 100644 index 0000000..339ff40 --- /dev/null +++ b/.github/skills/understanding-architecture/SKILL.md @@ -0,0 +1,230 @@ +--- +name: understanding-architecture +description: Provides architecture overview with state machines, SSE event flow, and file mappings. Use when understanding system design, debugging state issues, or maintaining ARCHITECTURE-FLOW.md. +--- + +# Understanding Architecture + +**Load this skill when**: Understanding system design, debugging state transitions, tracing SSE events, or updating architecture documentation. + +## Quick Reference + +### System Overview + +| Layer | Tech | Port | Entry Point | +|-------|------|------|-------------| +| Frontend | React 19 + Vite | 5173 | `frontend/src/App.tsx` | +| Backend | ASP.NET Core 9 | 8080 | `backend/WebApp.Api/Program.cs` | +| Auth | MSAL.js → JWT Bearer | — | `frontend/src/config/authConfig.ts` | +| AI SDK | Azure.AI.Projects (GA) + Azure.AI.Extensions.OpenAI | — | `backend/.../AgentFrameworkService.cs` | + +### Data Flow + +```text +User → ChatInput → CHAT_SEND_MESSAGE → ChatService.sendMessage() + → POST /api/chat/stream (JWT) → AgentFrameworkService.StreamMessageAsync() + → AI Foundry → SSE chunks → parseSseLine() → Reducer actions → UI update +``` + +--- + +## State Machines + +### Chat States + +```text +idle ──CHAT_SEND_MESSAGE──► sending ──CHAT_START_STREAM──► streaming + ▲ │ │ + │ ▼ ▼ + └──CHAT_CLEAR_ERROR─── error ◄──CHAT_ERROR──────────────────┤ + │ │ + └──CHAT_STREAM_COMPLETE / CHAT_CANCEL_STREAM / CHAT_MCP_APPROVAL_REQUEST +``` + +| State | Input Enabled | streamingMessageId | +|-------|---------------|-------------------| +| `idle` | ✅ Yes | `undefined` | +| `sending` | ❌ No | `undefined` | +| `streaming` | ❌ No | Message ID | +| `error` | If recoverable | `undefined` | + +### Auth States + +```text +initializing ──AUTH_INITIALIZED──► authenticated ──AUTH_TOKEN_EXPIRED──► unauthenticated + │ │ + └───────────AUTH_INITIALIZED──────────┘ +``` + +--- + +## SSE Event Flow + +### Backend → Frontend Mapping + +| SSE Event | Backend Method | Frontend Action | Reducer Effect | +|-----------|----------------|-----------------|----------------| +| `conversationId` | `WriteConversationIdEvent` | `CHAT_START_STREAM` | Set conversationId | +| `chunk` | `WriteChunkEvent` | `CHAT_STREAM_CHUNK` | Append content | +| `annotations` | `WriteAnnotationsEvent` | `CHAT_STREAM_ANNOTATIONS` | Add citations | +| `mcpApprovalRequest` | `WriteMcpApprovalRequestEvent` | `CHAT_MCP_APPROVAL_REQUEST` | Show approval UI | +| `usage` | `WriteUsageEvent` | `CHAT_STREAM_COMPLETE` | Add token counts | +| `done` | `WriteDoneEvent` | `CHAT_STREAM_COMPLETE` | Finalize | +| `error` | `WriteErrorEvent` | `CHAT_ERROR` | Set error state | + +### Event Sequence + +```text +1. conversationId (always first) +2. chunk (0-N times) +3. annotations (0-N times, after item complete) +4. mcpApprovalRequest (0-1 times, pauses stream) +5. usage (always before done) +6. done (always last) +``` + +--- + +## Key Files by Domain + +### State Management +| File | Purpose | +|------|---------| +| `frontend/src/types/appState.ts` | State & action type definitions | +| `frontend/src/reducers/appReducer.ts` | All state transitions | +| `frontend/src/contexts/AppContext.tsx` | Provider + dev logging | + +### SSE Streaming +| File | Purpose | +|------|---------| +| `backend/WebApp.Api/Program.cs` | SSE endpoints + Write*Event helpers | +| `frontend/src/services/chatService.ts` | SSE client + action dispatch | +| `frontend/src/utils/sseParser.ts` | Line parsing + event types | + +### AI Integration +| File | Purpose | +|------|---------| +| `backend/.../AgentFrameworkService.cs` | Agent loading + streaming | +| `backend/.../Models/StreamChunk.cs` | Chunk types (text, annotations, MCP) | +| `backend/.../Models/ChatRequest.cs` | Request payload structure | + +--- + +## Full Documentation + +For complete diagrams and detailed flows, see: +- **[ARCHITECTURE-FLOW.md](../../../ARCHITECTURE-FLOW.md)** - Full Mermaid diagrams +- **Part 1**: Backend flow (request pipeline, credential resolution, agent loading) +- **Part 2**: Frontend state (auth, chat, UI state machines) +- **Part 3**: Performance patterns (reducer optimizations) +- **Part 4**: Extending the state (adding new actions) +- **Part 5**: Backend patterns (validation, error format, async) +- **Part 6**: File reference (all key files) + +--- + +## Maintaining ARCHITECTURE-FLOW.md + +### When to Update + +Update the architecture document when: + +| Change Type | What to Update | +|-------------|----------------| +| New SSE event type | Section 1.5 (Backend SSE Event Types), Section 2.8 (SSE → Action Mapping) | +| New reducer action | Section 2.7 (Action Reference), state machine diagrams | +| New API endpoint | Section 1.1 (Request Pipeline flowchart) | +| New auth state | Section 2.1 (Authentication State Machine) | +| New chat state | Section 2.2 (Chat State Machine) | +| File moved/renamed | Part 6 (File Reference tables) | +| Validation rules changed | Section 5.1 (Attachment Validation) | + +### Validation Checklist + +Before committing architecture doc changes: + +```text +□ Mermaid Diagrams + □ All states match code (appState.ts types) + □ All transitions match reducer (appReducer.ts cases) + □ Diagram syntax renders without errors + +□ Tables + □ SSE events match Program.cs Write*Event methods + □ Actions match AppAction type union + □ File paths are lowercase (case-sensitive filesystems) + +□ Code Snippets + □ Patterns match actual code + □ Variable names correct + □ Examples would compile/run + +□ File Links + □ All referenced files exist + □ Paths use correct case (chatService.ts not ChatService.ts) +``` + +### Source of Truth Mapping + +| Document Section | Source Code | +|------------------|-------------| +| Request Pipeline (1.1) | `Program.cs` middleware + endpoints | +| Credential Resolution (1.2) | `AgentFrameworkService.cs` constructor | +| Agent Loading (1.3) | `AgentFrameworkService.GetAgentAsync()` | +| SSE Events (1.5) | `Program.cs` static Write*Event methods | +| Auth States (2.1) | `appState.ts` auth.status type | +| Chat States (2.2) | `appState.ts` chat.status type | +| Action Reference (2.7) | `appState.ts` AppAction type | +| SSE → Action (2.8) | `chatService.ts` processStream switch | +| Attachment Limits (5.1) | `AgentFrameworkService.cs` Max* constants | + +### Quick Sync Commands + +```powershell +# Find all SSE event types in backend +Select-String -Path "backend/WebApp.Api/Program.cs" -Pattern "type.*=" + +# Find all reducer actions +Select-String -Path "frontend/src/types/appState.ts" -Pattern "type:" + +# Find SSE parsing +Select-String -Path "frontend/src/services/chatService.ts" -Pattern "case '" + +# Verify file links exist +Get-ChildItem -Recurse -Include "chatService.ts","appReducer.ts","appState.ts" +``` + +### Cross-Reference with DeepWiki + +DeepWiki (https://deepwiki.com/microsoft-foundry/foundry-agent-webapp) indexes the repo automatically. After major architecture changes: + +1. Check DeepWiki re-indexes (usually within 24 hours) +2. Verify diagrams match between ARCHITECTURE-FLOW.md and DeepWiki +3. Note: DeepWiki may show older commit - check "Last indexed" date + +--- + +## Common Architecture Questions + +### "How does a message flow end-to-end?" +See [ARCHITECTURE-FLOW.md#2.3](../../../ARCHITECTURE-FLOW.md) - End-to-End Message Flow sequence diagram. + +### "What happens when streaming is cancelled?" +1. User clicks Stop button or presses Escape +2. `ChatService.cancelStream()` sets `streamCancelled = true` and calls `abort()` +3. `CHAT_CANCEL_STREAM` action dispatched +4. Reducer sets `status: idle`, clears `streamingMessageId`, enables input + +### "How does MCP tool approval work?" +1. Backend yields `StreamChunk.McpApproval` when `McpToolCallApprovalRequestItem` received +2. Frontend dispatches `CHAT_MCP_APPROVAL_REQUEST` with approval details +3. Reducer adds approval message, sets status to `idle` (but input stays disabled) +4. User clicks Approve/Deny +5. `ChatService.sendMcpApproval()` resumes with approval response + +### "Where is the JWT validated?" +`Program.cs` → `AddMicrosoftIdentityWebApi()` + `RequireAuthorization(ScopePolicyName)` on each endpoint. + +### "How are credentials resolved in production vs development?" +- **Development**: `ChainedTokenCredential(AzureCliCredential, AzureDeveloperCliCredential)` +- **Production**: `ManagedIdentityCredential(miClientId)` (user-assigned MI with `MANAGED_IDENTITY_CLIENT_ID`) diff --git a/.github/skills/validating-deployments/SKILL.md b/.github/skills/validating-deployments/SKILL.md new file mode 100644 index 0000000..10fea72 --- /dev/null +++ b/.github/skills/validating-deployments/SKILL.md @@ -0,0 +1,130 @@ +--- +name: validating-deployments +description: > + End-of-session validation for MI and OBO deployment paths. + Use after code changes to verify deploy → test → teardown works end-to-end. +--- + +# Validating Deployments + +Run this at the end of any session that changes backend, frontend, infra, or auth code. + +## Prerequisites + +- All unit tests pass (`dotnet test` + `npm test`) +- Frontend builds (`npm run build`) +- Bicep validates (`az bicep build --file infra/main.bicep`) +- Playwright installed (`npx playwright install chromium`) + +## Security Checks + +Run before every deployment: + +```powershell +# Backend: check for vulnerable NuGet packages +cd backend && dotnet list package --vulnerable + +# Frontend: check for vulnerable npm packages +cd frontend && npm audit + +# Docker: lint Dockerfile for security issues (if hadolint installed) +hadolint deployment/docker/frontend.Dockerfile +``` + +The Dockerfile runs as non-root (`USER app`). Verify this hasn't been removed after changes. + +## MI Path (Default) + +### 1. Deploy + +```powershell +azd env new mi-test --no-prompt +azd env set ENTRA_SERVICE_MANAGEMENT_REFERENCE "" # Required by some orgs +azd env set AZURE_LOCATION eastus2 --no-prompt +azd up --no-prompt +``` + +### 2. Test Remote + +```powershell +$endpoint = azd env get-value WEB_ENDPOINT +node deployment/scripts/smoke-test.js $endpoint +``` + +Verify: health 200, agent name displayed, chat streaming works, token usage visible. + +### 3. Test Local Dev + +```powershell +cd backend/WebApp.Api +$env:ASPNETCORE_ENVIRONMENT = "Development" # CRITICAL — without this, uses ManagedIdentityCredential which fails locally +$env:ASPNETCORE_URLS = "http://localhost:8080" +dotnet watch run --no-launch-profile & + +cd ../../frontend +npm run dev & + +# Wait for both servers, then: +node deployment/scripts/smoke-test.js http://localhost:5173 +``` + +### 4. Teardown + +```powershell +azd down --force --purge --environment mi-test --no-prompt +# Verify: +# - frontend/.env.local deleted +# - backend/WebApp.Api/.env deleted +# - .azure/mi-test/ deleted +# - Entra app deleted (check azd logs) +``` + +## OBO Path + +### 1. Deploy + +```powershell +azd env new obo-test --no-prompt +azd env set ENABLE_OBO true --no-prompt +azd env set AZURE_LOCATION eastus2 --no-prompt +# No SMR needed if using a non-MSFT tenant +azd up --no-prompt +``` + +### 2. Verify OBO Wiring + +```powershell +# Check backend logs for OBO mode +az containerapp logs show --name --resource-group --type console --tail 20 | Select-String "OBO" +# Expected: "OBO mode enabled: backendClientId=..." +# Expected: "Created OBO credential for request" (after first chat) +``` + +### 3. Test + +Run smoke test. OBO requires interactive MSAL login (user must sign in with test tenant credentials). The smoke test will wait at auth if no cached session exists. + +### 4. Teardown + +Same as MI path. Additionally verify: +- Backend API app registration deleted (postdown.ps1 handles this) +- FIC removed with the app + +## Gotchas + +| Gotcha | Detail | +|--------|--------| +| `ASPNETCORE_ENVIRONMENT=Development` | Required for local dev — without it, backend tries ManagedIdentityCredential which fails on dev machines | +| SMR required | Set `ENTRA_SERVICE_MANAGEMENT_REFERENCE` or Entra app creation fails | +| Bicep FIC creation may fail | Graph API eventual consistency issue. Workaround: create FIC via `az ad app federated-credential create` | +| v2 agent API requires `kind: "prompt"` | When creating agents via REST, use `definition: { kind: "prompt", model: "...", instructions: "..." }` | +| OBO scope is `api://{BACKEND}/Chat.ReadWrite` | NOT `api://{SPA}/Chat.ReadWrite`. Token audience mismatch = `AADSTS500131` | +| AI scope resolves to Azure ML Services | `https://ai.azure.com/.default` → appId `18a66f5f-...` (Azure Machine Learning Services), NOT `7d312290-...` (Cognitive Services) | +| Conversation history not user-scoped in MI mode | MI uses shared identity — all users see all conversations | + +## Related Skills + +- **testing-with-playwright** — Playwright MCP patterns for interactive testing +- **validating-ui-features** — Step-by-step UI feature validation +- **deploying-to-azure** — azd commands and troubleshooting +- **troubleshooting-authentication** — MSAL/JWT debugging diff --git a/.github/skills/validating-local-setup/SKILL.md b/.github/skills/validating-local-setup/SKILL.md new file mode 100644 index 0000000..2ed4d4e --- /dev/null +++ b/.github/skills/validating-local-setup/SKILL.md @@ -0,0 +1,122 @@ +--- +name: validating-local-setup +description: > + Diagnose and fix incomplete local development setup. + Use when dev servers fail to start, env vars are missing, + authentication errors occur, or before running any dev commands + for the first time. +--- + +# Local Setup Validation + +## Portal vs `azd up` — What Each Provides + +The AI Foundry portal's "View sample app code" gives you AI resource variables (`AI_AGENT_ENDPOINT`, `AI_AGENT_ID`), which identify your agent. However, this app also needs an **Entra ID app registration** for user authentication — which only `azd up` creates. Both sets of config are required. + +| Source | What It Provides | Config It Generates | +|--------|-----------------|-------------------| +| **AI Foundry portal** | Agent endpoint + agent ID | Root `.env` (or manual `azd env set`) | +| **`azd up`** | Entra app registration, RBAC, infrastructure | `frontend/.env.local` + `backend/WebApp.Api/.env` | + +If you came from the portal, paste those values and run `azd up` — it detects them and incorporates them automatically. + +## Quick Diagnostic + +Run the validation script to check all configuration: + +```powershell +pwsh -File deployment/scripts/validate-config.ps1 +``` + +This checks for `frontend/.env.local` and `backend/WebApp.Api/.env` with the core authentication variables. For a full list of required variables, see the tables below. + +## What `azd up` Creates + +This app requires `azd up` before local development works. Here's what it provisions: + +| What | Why | Generated File | +|------|-----|----------------| +| Entra ID app registration | SPA authentication (MSAL.js) | `frontend/.env.local` | +| Backend auth config | JWT token validation | `backend/WebApp.Api/.env` | +| Azure infrastructure | Container Apps, ACR, RBAC | `.azure//.env` | +| Redirect URIs | Login callback URLs | Entra app registration | + +**Even if AI Foundry resources already exist** (e.g., from the portal "View sample app code" flow), `azd up` is still required to create the Entra app registration. + +## Required Environment Variables + +### Frontend (`frontend/.env.local`) + +| Variable | Source | Required | +|----------|--------|----------| +| `VITE_ENTRA_SPA_CLIENT_ID` | Entra app registration (created by `azd up`) | Yes | +| `VITE_ENTRA_TENANT_ID` | Azure AD tenant | Yes | +| `VITE_ENTRA_BACKEND_CLIENT_ID` | Backend app registration (OBO mode only) | No | + +### Backend (`backend/WebApp.Api/.env`) + +| Variable | Source | Required | +|----------|--------|----------| +| `AzureAd__TenantId` | Azure AD tenant | Yes | +| `AzureAd__ClientId` | Entra app registration | Yes | +| `AzureAd__Audience` | `api://{ClientId}` | Yes | +| `AI_AGENT_ENDPOINT` | AI Foundry project endpoint | Yes | +| `AI_AGENT_ID` | Agent name in Foundry | Yes | + +## Common Error Patterns + +### "undefined" in login URL +``` +login.microsoftonline.com/undefined/oauth2/v2.0/authorize?client_id=undefined +``` +**Cause**: `VITE_ENTRA_SPA_CLIENT_ID` and `VITE_ENTRA_TENANT_ID` not set. +**Fix**: Run `azd up` — this creates the Entra app and generates `frontend/.env.local`. + +### AADSTS900023: Specified tenant identifier is neither a valid DNS name +**Cause**: Same as above — tenant ID is `undefined` or empty. +**Fix**: Run `azd up`. + +### Frontend shows "Setup Required" error page +**Cause**: The Vite env check plugin detected missing environment variables. +**Fix**: Run `azd up`, then restart the dev server. + +### 401 Unauthorized on /api/* endpoints +**Cause**: Backend JWT validation failing — check `backend/WebApp.Api/.env`. +**Fix**: Ensure `AzureAd__ClientId` matches the Entra app registration. Run `azd provision` to regenerate. + +### ManagedIdentityCredential error in local dev +**Cause**: Backend trying to use managed identity locally. +**Fix**: Set `ASPNETCORE_ENVIRONMENT=Development` — local dev uses `ChainedTokenCredential(AzureCliCredential, AzureDeveloperCliCredential)` instead. + +## Step-by-Step Fix for Incomplete Setup + +1. **Check current state**: + ```powershell + pwsh -File deployment/scripts/validate-config.ps1 + ``` + +2. **If no `.azure/` directory** (never ran azd): + ```powershell + azd up + ``` + +3. **If `.azure/` exists but env files missing** (partial setup): + ```powershell + azd provision # Re-creates Entra app + generates .env files + ``` + +4. **If env files exist but vars are wrong** (stale config): + ```powershell + azd provision # Regenerates everything + ``` + +5. **Restart dev servers** after any fix — env vars are read at startup. + +## For Agents: Before Running Dev Commands + +Before executing `dotnet watch run`, `npm run dev`, or `start-local-dev.ps1`: + +1. Check if `frontend/.env.local` exists +2. Check if `backend/WebApp.Api/.env` exists +3. If either is missing, tell the user to run `azd up` first +4. Do NOT try to create these files manually — they contain auto-generated Entra app registration IDs diff --git a/.github/skills/validating-ui-features/SKILL.md b/.github/skills/validating-ui-features/SKILL.md new file mode 100644 index 0000000..fafb166 --- /dev/null +++ b/.github/skills/validating-ui-features/SKILL.md @@ -0,0 +1,394 @@ +--- +name: validating-ui-features +description: Provides step-by-step procedures for validating UI features - theme toggle, new chat, cancel stream, markdown rendering, and token usage info. +--- + +# Validating UI Features + +**CRITICAL**: Load this skill before running any UI validation tests. + +## Prerequisites + +- [ ] Local dev servers running (Backend: 8080, Frontend: 5173) +- [ ] Playwright MCP tools available +- [ ] Authenticated user session (MSAL popup completed) + +## Quick Test Commands + +| Test | Command | +|------|---------| +| Start servers | VS Code task: `Start Dev (VS Code Terminals)` | +| Navigate | `browser_navigate` to `http://localhost:5173` | +| Check state | Look for `🔄` entries in browser console | + +--- + +## Test 1: Theme Toggle (Settings Panel) + +### Purpose +Verify the Settings panel opens and theme switching works correctly. + +### UI Flow +```text +ChatInput toolbar → Settings button (gear) → SettingsPanel drawer → ThemePicker dropdown +``` + +### Steps + +1. **Navigate** to `http://localhost:5173` +2. **Wait** for authentication and agent metadata load +3. **Find** Settings button in ChatInput toolbar (gear icon, aria-label="Settings") +4. **Click** Settings button +5. **Verify** SettingsPanel drawer opens from right side +6. **Verify** "Appearance" section visible with ThemePicker +7. **Click** ThemePicker dropdown (currently shows "Light" or saved preference) +8. **Select** "Dark" option +9. **Verify** Theme changes: + - Background becomes dark (≈ `rgb(32, 31, 30)`) + - Text becomes light + - No console errors +10. **Select** "Light" option +11. **Verify** Theme changes back: + - Background becomes light (≈ `rgb(255, 255, 255)`) + - Text becomes dark +12. **Select** "System" option +13. **Verify** Theme matches OS preference +14. **Close** Settings panel (X button or click outside) + +### Console Evidence +```javascript +// No errors should appear +// LocalStorage updated: +localStorage.getItem('ai-foundry-theme') // "Dark", "Light", or "System" +``` + +### DOM Changes +- `` FluentProvider styles update +- CSS variables change: `--colorNeutralBackground1`, `--colorNeutralForeground1` + +### Pass Criteria +- [ ] Settings panel opens/closes without errors +- [ ] All three theme options selectable +- [ ] Visual theme changes immediately on selection +- [ ] Theme persists after closing panel + +--- + +## Test 2: New Chat Button + +### Purpose +Verify the New Chat button clears messages and resets conversation state. + +### UI Flow +```text +Send message → Wait for response → Click New Chat button → Verify reset +``` + +### Prerequisites +- At least one message exchange completed + +### Steps + +1. **Send** a test message: `"Hello, testing new chat button"` +2. **Wait** for assistant response to complete (status: `idle`) +3. **Verify** New Chat button is **enabled** (not grayed out) +4. **Click** New Chat button (ChatAdd icon, aria-label="New chat") +5. **Verify** immediate changes: + - Messages array cleared (empty chat area) + - StarterMessages component visible (agent intro + prompts) + - Input field focused and empty + - New Chat button now **disabled** (no messages to clear) + +### Console Evidence +```javascript +🔄 [timestamp] CHAT_CLEAR +Action: {type: CHAT_CLEAR} +Changes: { + chat.messages.length: N → 0 +} +``` + +### Pass Criteria +- [ ] Button disabled when no messages +- [ ] Button enabled after first message +- [ ] Click clears all messages instantly +- [ ] StarterMessages reappear +- [ ] conversationId reset to null +- [ ] Input field receives focus +- [ ] No console errors + +--- + +## Test 3: Cancel Stream (Stop Button) + +### Purpose +Verify streaming can be cancelled mid-response. + +### UI Flow +```text +Send long prompt → While streaming → Click Stop button → Verify cancellation +``` + +### Steps + +1. **Start** a new chat (or use existing) +2. **Send** a prompt that triggers a long code response: + ```text + Write a comprehensive Python script that calculates Fibonacci numbers using 5 different methods: recursive, memoized, iterative, matrix exponentiation, and Binet's formula. Include detailed docstrings, type hints, performance benchmarks, and unit tests for each method. + ``` +3. **Immediately observe**: + - Status changes to `streaming` + - Stop button becomes **enabled** (aria-label="Cancel response") + - Send button becomes **disabled** + - Text chunks appearing in assistant message +4. **Click** Stop button while streaming is active +5. **Verify** cancellation: + - Streaming stops immediately + - Partial response preserved (not deleted) + - Status returns to `idle` + - Send button re-enabled + - Stop button disabled again + +### Keyboard Shortcut +- Press `Escape` key during streaming → should also cancel + +### Console Evidence +```javascript +🔄 [timestamp] CHAT_CANCEL_STREAM +Action: {type: CHAT_CANCEL_STREAM} +Changes: { + chat.status: streaming → idle, + chat.streamingMessageId: "xxx" → undefined +} +``` + +### Edge Cases +- Very fast response may complete before cancel → OK, not an error +- Multiple rapid clicks → should be idempotent + +### Pass Criteria +- [ ] Stop button disabled when not streaming +- [ ] Stop button enabled during streaming +- [ ] Click stops stream immediately +- [ ] Partial response text preserved +- [ ] Status returns to idle +- [ ] Escape key works as shortcut +- [ ] No errors in console + +--- + +## Test 4: Markdown Code Block Rendering + +### Purpose +Verify code blocks render with syntax highlighting, line numbers, and copy button. + +### UI Flow +```text +Send code request → Wait for response → Verify code block UI +``` + +### Steps + +1. **Start** a new chat +2. **Send** a code generation prompt: + ```text + Write a Python function to calculate fibonacci numbers with proper type hints + ``` +3. **Wait** for response to complete +4. **Verify** code block structure: + - Container with dark background + - Header bar showing "python" language label + - "Copy" button in header + - Line numbers on left side + - Syntax highlighting (keywords, strings, comments in different colors) +5. **Click** "Copy" button +6. **Verify** code copied (paste somewhere to confirm) + +### Additional Test Prompts +From `test-files/test-prompts.json`: +```json +[ + "Create a TypeScript interface for a user profile with nested preferences", + "Show me a bash script to backup a PostgreSQL database", + "Write a SQL query with a CTE to find duplicate customer records" +] +``` + +### Expected Code Block DOM +```html +
+
+ python + +
+
+ +
+
+``` + +### Syntax Highlighting Colors (vscDarkPlus theme) +| Element | Color | +|---------|-------| +| Keywords (`def`, `return`, `if`) | Purple/Blue | +| Strings | Orange | +| Comments | Green | +| Function names | Yellow | +| Types | Cyan | + +### Pass Criteria +- [ ] Language label displayed correctly +- [ ] Copy button present and functional +- [ ] Syntax highlighting applied +- [ ] Line numbers visible +- [ ] Long lines wrap (no horizontal overflow) +- [ ] Multiple code blocks render independently + +--- + +## Test 5: Complex Markdown Rendering + +### Purpose +Verify tables, lists, headings, and text formatting render correctly. + +### Steps + +1. **Send** a complex markdown prompt: + ```text + Create a comprehensive guide with: + - A comparison table of React, Vue, and Angular + - Numbered installation steps + - A code example + - Bold and italic text formatting + - A blockquote with a tip + ``` +2. **Wait** for response to complete +3. **Verify** each element: + +### Expected Elements + +| Element | Verification | +|---------|--------------| +| Table | Borders visible, headers bold, rows alternate | +| Ordered list | Numbers 1, 2, 3... with proper indentation | +| Unordered list | Bullets with proper indentation | +| Nested list | Sub-items indented further | +| Bold text | **text** renders with heavier weight | +| Italic text | *text* renders with slant | +| Inline code | `code` has background highlight | +| Blockquote | Left border, indented, lighter text | +| Links | Underlined, opens in new tab | + +### Pass Criteria +- [ ] Tables render with visible structure +- [ ] Lists have proper indentation +- [ ] Text formatting (bold/italic) applied +- [ ] Inline code visually distinct +- [ ] Links functional and styled +- [ ] No raw markdown visible + +--- + +## Test 6: Token Usage Info + +### Purpose +Verify response footer shows timing, token counts, and expandable usage details. + +### UI Flow +```text +Send message → Wait for response → Verify footer → Click expand → Verify breakdown +``` + +### Steps + +1. **Send** any message and wait for response to complete +2. **Verify** response footer displays: + - Response time (e.g., `3575ms`) + - Total token count (e.g., `848 tokens`) + - Info icon button (aria-label="Show token usage details") +3. **Hover** over info icon +4. **Verify** usage details panel shows: + - "Usage Information" header + - Input tokens (e.g., `Input: 799 tokens`) + - Output tokens (e.g., `Output: 49 tokens`) + +### Console Evidence +```javascript +Action: {type: CHAT_STREAM_COMPLETE, usage: Object} +// usage object: { promptTokens, completionTokens, totalTokens } +``` + +### Pass Criteria +- [ ] Response time displayed after completion +- [ ] Total token count displayed +- [ ] Info icon button present +- [ ] Input/Output breakdown shows on hover +- [ ] Values are non-zero numbers + +--- + +## Test Files Reference + +| File | Purpose | +|------|---------| +| `test-files/test-prompts.json` | Prompts for each test scenario | +| `test-files/code-sample.md` | Expected code block rendering | +| `test-files/complex-response.md` | Expected complex markdown | +| `test-files/test.txt` | Plain text upload test | +| `test-files/test.png` | Image upload test | + +--- + +## Troubleshooting + +| Issue | Likely Cause | Solution | +|-------|--------------|----------| +| Theme not changing | ThemeContext not receiving update | Check FluentProvider wrapping | +| New chat button always disabled | `hasMessages` prop false | Check messages array binding | +| Cancel not working | AbortController not set | Check `currentStreamAbort` in ChatService | +| Code not highlighted | Language not detected | Check regex pattern in Markdown.tsx | +| Copy button not working | `copy-to-clipboard` import | Check package installed | +| Table not rendering | GFM plugin missing | Check `remarkGfm` in Markdown.tsx | + +--- + +## Quick Validation Checklist + +```text +□ Theme Toggle + □ Settings opens + □ Dark theme works + □ Light theme works + □ System theme works + □ Persists on refresh + +□ New Chat + □ Button disabled initially + □ Enabled after message + □ Clears messages + □ Resets conversation + +□ Cancel Stream + □ Stop enabled during stream + □ Cancels immediately + □ Preserves partial response + □ Escape key works + +□ Code Blocks + □ Language label + □ Copy button works + □ Syntax highlighting + □ Line numbers + +□ Complex Markdown + □ Tables + □ Lists + □ Formatting + □ Links + +□ Token Usage + □ Response time shown + □ Token count shown + □ Info icon present + □ Input/Output breakdown on hover +``` diff --git a/.github/skills/validating-ui-features/test-files/code-sample.md b/.github/skills/validating-ui-features/test-files/code-sample.md new file mode 100644 index 0000000..cd3fcb8 --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/code-sample.md @@ -0,0 +1,135 @@ +# Expected Code Block Output + +When the agent returns code, verify this structure renders correctly. + +## Python Example + +The following should render with: +- Language label: "python" +- Copy button in header +- Line numbers 1-10 +- Syntax highlighting + +```python +def fibonacci(n: int) -> int: + """Calculate the nth Fibonacci number. + + Args: + n: The position in the Fibonacci sequence (0-indexed) + + Returns: + The nth Fibonacci number + """ + if n <= 1: + return n + return fibonacci(n - 1) + fibonacci(n - 2) + + +# Test the function +for i in range(10): + print(f"F({i}) = {fibonacci(i)}") +``` + +### Expected Syntax Highlighting + +| Element | Color | Examples | +|---------|-------|----------| +| Keywords | Purple/Blue | `def`, `return`, `if`, `for`, `in` | +| Strings | Orange | `"Calculate..."`, `f"F({i})..."` | +| Comments | Green | `# Test the function` | +| Function names | Yellow | `fibonacci`, `print` | +| Types | Cyan | `int` | +| Numbers | Light green | `1`, `10` | + +--- + +## TypeScript Example + +```typescript +interface UserProfile { + id: string; + name: string; + email: string; + createdAt: Date; + preferences: { + theme: 'light' | 'dark' | 'system'; + notifications: boolean; + language: string; + }; + roles: string[]; +} + +async function fetchUser(id: string): Promise { + try { + const response = await fetch(`/api/users/${id}`); + if (!response.ok) return null; + return await response.json(); + } catch (error) { + console.error('Failed to fetch user:', error); + return null; + } +} +``` + +--- + +## Bash Example + +```bash +#!/bin/bash +# PostgreSQL backup script with timestamp + +DB_NAME="myapp_production" +BACKUP_DIR="/var/backups/postgres" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +FILENAME="${DB_NAME}_${TIMESTAMP}.sql.gz" + +echo "Starting backup of ${DB_NAME}..." +pg_dump -U postgres -h localhost "${DB_NAME}" | gzip > "${BACKUP_DIR}/${FILENAME}" + +if [ $? -eq 0 ]; then + echo "Backup completed: ${FILENAME}" +else + echo "Backup failed!" >&2 + exit 1 +fi +``` + +--- + +## SQL Example + +```sql +-- Find customers with duplicate email addresses +WITH duplicate_emails AS ( + SELECT + email, + COUNT(*) as occurrence_count + FROM customers + GROUP BY email + HAVING COUNT(*) > 1 +) +SELECT + c.id, + c.name, + c.email, + c.created_at, + de.occurrence_count +FROM customers c +INNER JOIN duplicate_emails de ON c.email = de.email +ORDER BY c.email, c.created_at; +``` + +--- + +## Verification Checklist + +For each code block above, verify: + +- [ ] Dark background container +- [ ] Language label in top-left of header +- [ ] "Copy" button in top-right of header +- [ ] Line numbers visible on left +- [ ] Syntax highlighting applied (colors match theme) +- [ ] Long lines wrap without horizontal scroll +- [ ] Click "Copy" → code copied to clipboard diff --git a/.github/skills/validating-ui-features/test-files/complex-response.md b/.github/skills/validating-ui-features/test-files/complex-response.md new file mode 100644 index 0000000..725c5fc --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/complex-response.md @@ -0,0 +1,205 @@ +# Expected Complex Markdown Output + +This file shows expected rendering for various markdown elements. + +--- + +## Table Example + +The following table should render with visible borders, bold headers, and proper alignment: + +| Framework | Language | Stars | Initial Release | Virtual DOM | +|-----------|----------|------:|-----------------|:-----------:| +| React | JavaScript/TypeScript | 220k+ | 2013 | Yes | +| Vue | JavaScript/TypeScript | 205k+ | 2014 | Yes | +| Angular | TypeScript | 95k+ | 2016 | No (Incremental DOM) | +| Svelte | JavaScript/TypeScript | 75k+ | 2016 | No (Compiler) | + +### Table Verification +- [ ] Headers bold +- [ ] Borders visible +- [ ] Right-aligned "Stars" column +- [ ] Center-aligned "Virtual DOM" column +- [ ] Left-aligned other columns + +--- + +## List Examples + +### Unordered List (Bullets) + +- First top-level item +- Second top-level item + - Nested item A + - Nested item B + - Deeply nested item + - Another deeply nested + - Nested item C +- Third top-level item + +### Ordered List (Numbers) + +1. First step: Install dependencies +2. Second step: Configure environment + - Create `.env` file + - Add required variables +3. Third step: Run the application +4. Fourth step: Verify functionality + +### Mixed Lists + +1. Prerequisites + - Node.js 18+ + - npm or yarn + - Git +2. Installation + - Clone the repository + - Install dependencies +3. Configuration + - Copy `.env.example` to `.env` + - Update values + +### List Verification +- [ ] Bullets for unordered lists +- [ ] Numbers for ordered lists +- [ ] Proper indentation for nesting +- [ ] Consistent spacing + +--- + +## Text Formatting + +This paragraph contains **bold text**, *italic text*, and ***bold italic text***. + +Here is some `inline code` that should have a background. + +Here is ~~strikethrough text~~ that should have a line through it. + +### Formatting Verification +- [ ] Bold text heavier weight +- [ ] Italic text slanted +- [ ] Inline code has background highlight +- [ ] Strikethrough has line + +--- + +## Blockquotes + +> This is a simple blockquote that should render with a left border and slight indentation. + +> **💡 Tip:** You can use blockquotes for tips, warnings, or important notes. +> They can span multiple lines and contain **formatting**. + +> ⚠️ **Warning:** This is a warning blockquote. +> +> It contains multiple paragraphs and should maintain the left border throughout. + +### Blockquote Verification +- [ ] Left border visible +- [ ] Text indented from border +- [ ] Formatting works inside quotes +- [ ] Multi-paragraph quotes connected + +--- + +## Links + +External links should open in a new tab: +- [Microsoft Learn](https://learn.microsoft.com) +- [Azure Portal](https://portal.azure.com) +- [GitHub](https://github.com) + +### Link Verification +- [ ] Links styled (underline or color) +- [ ] Hover state visible +- [ ] Opens in new tab (`target="_blank"`) + +--- + +## Headings + +The page should have a clear heading hierarchy: + +# Heading 1 (Largest) +## Heading 2 +### Heading 3 +#### Heading 4 +##### Heading 5 +###### Heading 6 (Smallest) + +### Heading Verification +- [ ] Size decreases from H1 to H6 +- [ ] Proper spacing above/below +- [ ] Bold weight on all headings + +--- + +## Combined Example + +Here's what a real documentation response might look like: + +### Quick Start Guide + +> **Prerequisites:** Make sure you have Node.js 18+ installed. + +1. **Clone the repository** + ```bash + git clone https://github.com/example/repo.git + cd repo + ``` + +2. **Install dependencies** + ```bash + npm install + ``` + +3. **Configure environment** + - Copy the example file: `cp .env.example .env` + - Edit `.env` with your values + +4. **Start the application** + ```bash + npm run dev + ``` + +| Command | Description | +|---------|-------------| +| `npm run dev` | Start development server | +| `npm run build` | Build for production | +| `npm run test` | Run test suite | + +> **Note:** For production deployment, see the [deployment guide](https://docs.example.com/deploy). + +--- + +## Full Verification Checklist + +``` +□ Tables + □ Visible borders/structure + □ Bold headers + □ Column alignment + +□ Lists + □ Bullets render + □ Numbers render + □ Nesting indented + +□ Formatting + □ Bold works + □ Italic works + □ Inline code styled + +□ Blockquotes + □ Left border + □ Indentation + □ Multi-line works + +□ Links + □ Styled + □ New tab + +□ Headings + □ Size hierarchy + □ Proper spacing +``` diff --git a/.github/skills/validating-ui-features/test-files/test-prompts.json b/.github/skills/validating-ui-features/test-files/test-prompts.json new file mode 100644 index 0000000..1000966 --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/test-prompts.json @@ -0,0 +1,104 @@ +{ + "theme_toggle": { + "description": "No prompt needed - UI interaction only", + "steps": [ + "Click Settings button (gear icon)", + "Click ThemePicker dropdown", + "Select Dark/Light/System" + ], + "verification": "Check body background color and localStorage" + }, + "new_chat": { + "setup_prompt": "Hello, this is a test message to populate the chat history.", + "expected_action": "CHAT_CLEAR", + "verification": [ + "Messages array cleared", + "StarterMessages visible", + "conversationId is null" + ] + }, + "cancel_stream": { + "long_response_prompts": [ + "Write a detailed 500-word essay about the history of software testing methodologies, including unit testing, integration testing, and end-to-end testing.", + "Explain the principles of quantum computing in great detail, covering qubits, superposition, entanglement, and quantum gates with examples.", + "Create a comprehensive tutorial on building RESTful APIs with proper authentication, error handling, and documentation." + ], + "expected_action": "CHAT_CANCEL_STREAM", + "keyboard_shortcut": "Escape" + }, + "code_blocks": { + "prompts": [ + { + "language": "python", + "prompt": "Write a Python function to calculate fibonacci numbers with proper type hints and docstring" + }, + { + "language": "typescript", + "prompt": "Create a TypeScript interface for a blog post with author, comments, and tags" + }, + { + "language": "bash", + "prompt": "Show me a bash script to backup a PostgreSQL database with timestamp in filename" + }, + { + "language": "sql", + "prompt": "Write a SQL query with a CTE to find customers with duplicate email addresses" + }, + { + "language": "csharp", + "prompt": "Write a C# async method to fetch data from an API with retry logic" + }, + { + "language": "javascript", + "prompt": "Create a JavaScript function to debounce API calls with configurable delay" + } + ], + "expected_elements": [ + "codeHeader with language label", + "Copy button (functional)", + "SyntaxHighlighter with vscDarkPlus theme", + "Line numbers on left", + "Word wrap for long lines" + ] + }, + "complex_markdown": { + "prompts": [ + { + "description": "Table comparison", + "prompt": "Create a markdown comparison table of React, Vue, and Angular including columns for: Framework, Language, Virtual DOM, State Management, and Learning Curve" + }, + { + "description": "Documentation style", + "prompt": "Write a quick start guide with: h2 headings, numbered steps, bullet point prerequisites, inline code for commands, and a tip in a blockquote" + }, + { + "description": "Mixed content", + "prompt": "Create documentation that includes: a warning blockquote, a table, nested bullet lists, bold and italic formatting, and a code example" + } + ], + "expected_elements": [ + "Table with visible borders and headers", + "Ordered list with numbers", + "Unordered list with bullets", + "Nested lists with indentation", + "Bold text (**text**)", + "Italic text (*text*)", + "Inline code (`code`)", + "Blockquote with left border", + "Links that open in new tab" + ] + }, + "file_upload": { + "description": "Test files for upload validation", + "files": [ + { "name": "test.txt", "type": "text/plain", "description": "Plain text document" }, + { "name": "test.md", "type": "text/markdown", "description": "Markdown document" }, + { "name": "test.csv", "type": "text/csv", "description": "CSV data file" }, + { "name": "test.json", "type": "application/json", "description": "JSON data file" }, + { "name": "test.html", "type": "text/html", "description": "HTML document" }, + { "name": "test.xml", "type": "application/xml", "description": "XML document" }, + { "name": "test.png", "type": "image/png", "description": "PNG image with test graphic" } + ], + "prompt_with_files": "Please describe all the files I've attached. List each file type and summarize its contents." + } +} diff --git a/.github/skills/validating-ui-features/test-files/test.csv b/.github/skills/validating-ui-features/test-files/test.csv new file mode 100644 index 0000000..d532727 --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/test.csv @@ -0,0 +1,4 @@ +name,age,department +Alice,30,Engineering +Bob,25,Marketing +Carol,35,HR diff --git a/.github/skills/validating-ui-features/test-files/test.html b/.github/skills/validating-ui-features/test-files/test.html new file mode 100644 index 0000000..2278191 --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/test.html @@ -0,0 +1 @@ +Test

Test HTML

This is a test HTML file.

diff --git a/.github/skills/validating-ui-features/test-files/test.json b/.github/skills/validating-ui-features/test-files/test.json new file mode 100644 index 0000000..a2deca1 --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/test.json @@ -0,0 +1 @@ +{"name": "Test JSON", "type": "test", "items": [1, 2, 3]} diff --git a/.github/skills/validating-ui-features/test-files/test.md b/.github/skills/validating-ui-features/test-files/test.md new file mode 100644 index 0000000..83023b2 --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/test.md @@ -0,0 +1,6 @@ +# Test Markdown + +This is a **markdown** test file with *formatting*. + +- Item 1 +- Item 2 diff --git a/.github/skills/validating-ui-features/test-files/test.png b/.github/skills/validating-ui-features/test-files/test.png new file mode 100644 index 0000000..aadcc0c Binary files /dev/null and b/.github/skills/validating-ui-features/test-files/test.png differ diff --git a/.github/skills/validating-ui-features/test-files/test.txt b/.github/skills/validating-ui-features/test-files/test.txt new file mode 100644 index 0000000..058fde5 --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/test.txt @@ -0,0 +1,2 @@ +This is a plain text test file. +It contains sample content for testing text/plain uploads. diff --git a/.github/skills/validating-ui-features/test-files/test.xml b/.github/skills/validating-ui-features/test-files/test.xml new file mode 100644 index 0000000..7f8d14f --- /dev/null +++ b/.github/skills/validating-ui-features/test-files/test.xml @@ -0,0 +1 @@ +Test XML contentMore content diff --git a/.github/skills/writing-bicep-templates/SKILL.md b/.github/skills/writing-bicep-templates/SKILL.md new file mode 100644 index 0000000..c9cdcee --- /dev/null +++ b/.github/skills/writing-bicep-templates/SKILL.md @@ -0,0 +1,168 @@ +--- +name: writing-bicep-templates +description: Provides Bicep coding standards for Azure infrastructure in this repository. Use when writing or modifying Bicep files, configuring Container Apps, setting up RBAC, or working with Azure resources. +--- + +# Bicep Coding Standards + +**Goal**: Create consistent, secure Azure infrastructure + +## Naming Convention + +Use `resourceToken` from `uniqueString()`: + +```bicep +var token = toLower(uniqueString(subscription().id, environmentName, location)) +name: '${abbrs.appContainerApps}web-${token}' // ca-web-abc123 +``` + +**Exception**: ACR requires alphanumeric only: `cr${resourceToken}` + +## Parameters + +Always add `@description()` and use `@allowed()` for constrained values: + +```bicep +@description('Environment (dev, prod)') +param environmentName string + +@description('Azure region') +@allowed(['eastus2', 'westus2']) +param location string = 'eastus2' +``` + +## Outputs + +Expose key identifiers for `azd` and other modules: + +```bicep +output containerAppName string = containerApp.name +output webEndpoint string = 'https://${containerApp.properties.configuration.ingress.fqdn}' +output identityPrincipalId string = containerApp.identity.principalId +``` + +## Managed Identity + +Use a user-assigned MI for ACR pull and OBO (avoids circular dependencies). Create it in the infrastructure module so its `principalId` is available before the Container App: + +```bicep +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2024-11-30' = { + name: '${abbrs.managedIdentityUserAssignedIdentities}web-${resourceToken}' + location: location + properties: { isolationScope: 'Regional' } +} +output managedIdentityPrincipalId string = managedIdentity.properties.principalId +``` + +Attach to Container App with `identity: { type: 'UserAssigned', userAssignedIdentities: { '${miId}': {} } }`. Use MI for ACR pull via `registries: [{ server: acr.loginServer, identity: miId }]`. + +## RBAC Assignments + +Use `guid()` for names + specify `principalType`: + +```bicep +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resource.id, principalId, roleId) + properties: { + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId) + principalId: principalId + principalType: 'ServicePrincipal' + } +} +``` + +## Container Apps + +Key settings: System identity + scale-to-zero + HTTPS only: + +```bicep +resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { + identity: { type: 'UserAssigned', userAssignedIdentities: { '${userAssignedIdentityId}': {} } } + properties: { + configuration: { + ingress: { + external: true + targetPort: 8080 + allowInsecure: false + } + } + template: { + scale: { minReplicas: 0, maxReplicas: 3 } + } + } +} +``` + +## ACR Pull Pattern + +Use user-assigned MI for ACR pull (no admin credentials or secrets): + +```bicep +registries: [{ + server: containerRegistry.properties.loginServer + identity: userAssignedIdentityId // MI with AcrPull role +}] +``` + +## Validation + +```powershell +az bicep build --file main.bicep +az deployment group what-if --template-file main.bicep +``` + +--- + +## Project-Specific: Module Hierarchy + +```text +main.bicep (subscription scope) +├─ Resource group +├─ main-infrastructure.bicep (ACR + Container Apps Env + Log Analytics + User-Assigned MI) +├─ entra-app.bicep (SPA app + conditional OBO backend app with FIC + admin consent) +├─ main-app.bicep (Container App with MI-based ACR pull) +└─ RBAC (Cognitive Services User role via postprovision CLI) +``` + +## Project-Specific: Container App Configuration + +```bicep +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + identity: { + type: 'UserAssigned' + userAssignedIdentities: { '${userAssignedIdentityId}': {} } + } + properties: { + managedEnvironmentId: containerAppsEnvironmentId + configuration: { + ingress: { + external: true + targetPort: 8080 + allowInsecure: false + } + registries: [{ + server: containerRegistry.properties.loginServer + identity: userAssignedIdentityId // MI-based pull, no secrets + }] + } + template: { + containers: [{ + name: 'web' + image: containerImage + env: containerEnv // Base env + conditional OBO env + resources: { cpu: json('0.5'), memory: '1Gi' } + }] + scale: { minReplicas: 0, maxReplicas: 3 } + } + } +} + +output fqdn string = containerApp.properties.configuration.ingress.fqdn +output identityPrincipalId string = containerApp.identity.principalId +``` + +## Related Skills + +- **deploying-to-azure** - Deployment commands and hook workflow +- **writing-csharp-code** - Backend configuration for Container Apps +- **troubleshooting-authentication** - RBAC and managed identity debugging diff --git a/.github/skills/writing-csharp-code/SKILL.md b/.github/skills/writing-csharp-code/SKILL.md new file mode 100644 index 0000000..4a498f2 --- /dev/null +++ b/.github/skills/writing-csharp-code/SKILL.md @@ -0,0 +1,347 @@ +--- +name: writing-csharp-code +description: Provides C# and ASP.NET Core coding standards for this repository. Use when writing or modifying C# code, implementing API endpoints, configuring middleware, or working with authentication in the backend. +--- + +# C# Coding Standards + +**Goal**: Write clean, secure ASP.NET Core code with proper authentication + +## Hot Reload Development Workflow + +**The backend runs in watch mode** (`dotnet watch run`). When you edit C# code: + +1. **Save the file** - .NET automatically recompiles +2. **Check the terminal** - Look for compilation output in the "Backend: ASP.NET Core API" terminal +3. **Verify via console logs** - New requests will use updated code immediately + +**VS Code Tasks** (use `Run Task` command or check terminal panel): +- `Backend: ASP.NET Core API` - Runs `dotnet watch run` with live recompilation +- Logs are visible directly in VS Code terminal + +**No restart needed** - Just edit, save, and test. Watch for compilation errors in the terminal. + +**Testing changes**: Use Playwright browser tools to make requests and check browser console logs, or call endpoints directly. + +## Minimal API Patterns + +Use typed request models, CancellationToken, and IHostEnvironment: + +```csharp +app.MapPost("/api/endpoint", async ( + RequestModel request, + MyService service, + IHostEnvironment env, + CancellationToken cancellationToken) => +{ + try + { + var result = await service.ProcessAsync(request, cancellationToken); + return Results.Ok(result); + } + catch (Exception ex) + { + return ErrorResponseFactory.CreateFromException(ex, env); + } +}) +.RequireAuthorization("RequireChatScope") +.WithName("EndpointName"); +``` + +## Authentication Setup + +**JWT Bearer with Entra ID**: + +```csharp +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(options => + { + builder.Configuration.Bind("AzureAd", options); + options.TokenValidationParameters.ValidAudiences = new[] + { + builder.Configuration["AzureAd:ClientId"], + $"api://{builder.Configuration["AzureAd:ClientId"]}" + }; + }, options => builder.Configuration.Bind("AzureAd", options)); +``` + +## Async Best Practices + +```csharp +// ✅ Use async/await with CancellationToken +public async Task ProcessAsync(Request req, CancellationToken ct) +{ + return await _service.ExecuteAsync(req, ct); +} + +// ❌ Never block on async +var result = _service.ExecuteAsync(req).Result; // WRONG +``` + +## IAsyncEnumerable for Streaming + +```csharp +public async IAsyncEnumerable StreamAsync( + string input, + [EnumeratorCancellation] CancellationToken cancellationToken = default) +{ + await foreach (var chunk in source.WithCancellation(cancellationToken)) + { + yield return chunk; + } +} +``` + +## Credential Strategy + +```csharp +TokenCredential credential = env.IsDevelopment() + ? new ChainedTokenCredential( + new AzureCliCredential(), + new AzureDeveloperCliCredential()) // Supports 'azd auth login' + : new ManagedIdentityCredential(miClientId); // User-assigned MI in production +``` + +**Why ChainedTokenCredential**: Avoids `DefaultAzureCredential`'s "fail fast" mode issues. Explicit, predictable credential chain. + +## IDisposable Pattern + +```csharp +public class MyService : IDisposable +{ + private readonly SemaphoreSlim _lock = new(1, 1); + private readonly CancellationTokenSource _disposeCts = new(); + private bool _disposed; + + public void DoWork() + { + ObjectDisposedException.ThrowIf(_disposed, this); + // ... + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + // Cancel pending operations first + try { _disposeCts.Cancel(); } + catch (ObjectDisposedException) { } + + _disposeCts.Dispose(); + _lock.Dispose(); + } +} +``` + +## Error Responses (RFC 7807) + +Use `ErrorResponseFactory.CreateFromException()` for consistent error responses. + +See: `backend/WebApp.Api/Models/ErrorResponse.cs` + +## Common Mistakes + +- ❌ Using `.Result` or `.Wait()` on async methods +- ❌ Forgetting `CancellationToken` parameter +- ❌ Missing `.RequireAuthorization()` on endpoints +- ❌ Exposing internal errors in production +- ❌ Forgetting disposal guards in `IDisposable` + +--- + +## Project-Specific: Middleware Pipeline + +**Goal**: Serve static files → validate auth → route APIs → SPA fallback + +```csharp +app.UseDefaultFiles(); // index.html for / +app.UseStaticFiles(); // wwwroot/* assets +app.UseCors(); // Dev only +app.UseAuthentication(); // Validate JWT +app.UseAuthorization(); // Enforce scope +// Map endpoints here +app.MapFallbackToFile("index.html"); // MUST BE LAST +``` + +## Project-Specific: AgentFrameworkService + +**See**: `backend/WebApp.Api/Services/AgentFrameworkService.cs` + +**SDK Packages**: +- `Azure.AI.Projects` — Main entry point, v2 Agents API (see `*.csproj` for version) +- `Azure.AI.Projects.Agents` — `ProjectsAgentVersion`, `DeclarativeAgentDefinition`, `AgentAdministrationClient` +- `Azure.AI.Extensions.OpenAI` — `ProjectOpenAIClient`, `ProjectConversationsClient`, `ProjectResponsesClient` + +**Sub-namespaces**: `Azure.AI.Projects.Agents`, `Azure.AI.Extensions.OpenAI`, `OpenAI.Responses` + +**Key patterns**: +- `IDisposable` implementation +- Disposal guards (`ObjectDisposedException.ThrowIf`) in all public methods +- Environment-aware credential selection (ChainedTokenCredential vs ManagedIdentityCredential vs OnBehalfOfCredential) +- Static-cached `ProjectsAgentVersion` resolved once per process via `SemaphoreSlim` +- Configuration validation (`AI_AGENT_ENDPOINT`, `AI_AGENT_ID`, optional `AI_AGENT_VERSION`) + +**Agent Loading** (direct SDK): +```csharp +// Load agent metadata directly from v2 Agents API. +// NOTE: the REST spec has no "latest" keyword — the agent_version path parameter is a +// plain string. To resolve the newest version, enumerate versions in descending order +// and take the first. Pin a specific version by passing its id to GetAgentVersionAsync. +ProjectsAgentVersion? agentVersion = null; +await foreach (var v in projectClient.AgentAdministrationClient.GetAgentVersionsAsync( + agentName: agentId, + limit: 1, + order: AgentListOrder.Descending, + after: null, + before: null, + cancellationToken: ct)) +{ + agentVersion = v; + break; +} + +// Access definition for model/instructions/structured inputs +var definition = agentVersion?.Definition as DeclarativeAgentDefinition; +``` + +**Streaming** (direct ProjectResponsesClient — required for specialized types): +```csharp +// Direct SDK for streaming — IChatClient doesn't expose MCP/annotations. +// Pin to the resolved agentVersion.Version so streaming and metadata stay in sync. +ProjectResponsesClient responsesClient = projectClient.ProjectOpenAIClient.GetProjectResponsesClientForAgent( + new AgentReference(agentId, agentVersion.Version), conversationId); +``` + +**Why direct streaming?** The `IChatClient` abstraction doesn't expose: +- `McpToolCallApprovalRequestItem` for MCP approval flows +- `FileSearchCallResponseItem` for file search quotes +- `MessageResponseItem.OutputTextAnnotations` for citations + +**Streaming Pattern**: Returns `IAsyncEnumerable` where `StreamChunk` contains either: +- Text delta (`chunk.IsText`, `chunk.TextDelta`) +- Annotations/citations (`chunk.HasAnnotations`, `chunk.Annotations`) + +**Streaming Response Types** (from `OpenAI.Responses`): +- `StreamingResponseOutputTextDeltaUpdate` - Text content delta +- `StreamingResponseOutputItemDoneUpdate` - Item completion (has annotations) +- `StreamingResponseCompletedUpdate` - Response completion with usage stats + +**Image Validation** (in `BuildUserMessage()`): +- Maximum 5 images per request +- Maximum 5MB per image (decoded size) +- Allowed: `image/png`, `image/jpeg`, `image/gif`, `image/webp` +- Returns HTTP 400 with validation details if constraints violated + +**Annotation Types** (from `OpenAI.Responses`): +- `UriCitationMessageAnnotation` - Bing, Azure AI Search, SharePoint +- `FileCitationMessageAnnotation` - File search (vector stores) +- `FilePathMessageAnnotation` - Code interpreter output +- `ContainerFileCitationMessageAnnotation` - Container file citations + +**Starter Prompts**: Parsed from agent metadata (`starterPrompts` key, newline-separated). + +## Project-Specific: Configuration Loading + +Auto-load `.env` file before building configuration: + +```csharp +var envFile = Path.Combine(Directory.GetCurrentDirectory(), ".env"); +if (File.Exists(envFile)) +{ + foreach (var line in File.ReadAllLines(envFile) + .Where(l => !string.IsNullOrWhiteSpace(l) && !l.StartsWith("#"))) + { + var parts = line.Split('=', 2); + if (parts.Length == 2) + Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1].Trim()); + } +} +``` + +## Troubleshooting SDK Issues + +**When things break**: SDK type mismatches and missing methods almost always happen after a package upgrade. Check `backend/WebApp.Api/WebApp.Api.csproj` for current versions: + +- `Azure.AI.Projects` - check `WebApp.Api.csproj` for current version +- `Azure.Identity` - check `WebApp.Api.csproj` for current version + +If types don't match documentation or samples, verify you're looking at docs for the **same version** installed in the project. + +### GitHub SDK Source (For Deep Dives) + +When you need to understand SDK internals, fetch the actual source: + +- **Azure.AI.Projects**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Projects/src +- **SDK samples**: https://github.com/Azure/azure-sdk-for-net/tree/main/sdk/ai/Azure.AI.Agents.Persistent/samples +- **OpenAI.Responses**: https://github.com/openai/openai-dotnet/tree/main/src + +### Quick Structure Checks (CLI) + +For project-level overviews only—use sparingly: + +```powershell +# List public types in backend (overview, not deep exploration) +Get-ChildItem -Path backend -Recurse -Include *.cs | + Select-String -Pattern "^\s*(public|internal)\s+(class|record|interface)\s+(\w+)" | + ForEach-Object { $_.Matches.Groups[3].Value } | Sort-Object -Unique + +# Find IDisposable implementations +Get-ChildItem -Path backend -Recurse -Include *.cs | + Select-String -Pattern ":\s*.*IDisposable" +``` + +**Use for**: Quick inventory of what exists. Follow up with pattern search or IDE navigation for understanding. + +### PowerShell Reflection for .NET Assemblies + +Use when you need to discover exact type members on any .NET assembly (especially beta SDKs): + +```powershell +# 1. Build first to ensure DLLs are current +cd backend/WebApp.Api; dotnet build --no-restore + +# 2. Find and load any assembly by name +$dll = Get-ChildItem -Path "bin/Debug" -Recurse -Filter "SomePackage.dll" | Select-Object -First 1 +$asm = [System.Reflection.Assembly]::LoadFrom($dll.FullName) + +# 3. Inspect a specific type's properties +$type = $asm.GetType("SomeNamespace.SomeClass") +Write-Host "Type: $($type.FullName)" +Write-Host "Assembly: $($asm.GetName().Name) v$($asm.GetName().Version)" +$type.GetProperties() | ForEach-Object { Write-Host " $($_.PropertyType.Name) $($_.Name)" } + +# 4. Check base type for inherited members +Write-Host "Base: $($type.BaseType.Name)" +$type.BaseType.GetProperties() | ForEach-Object { Write-Host " $($_.PropertyType.Name) $($_.Name)" } +``` + +**Finding types by pattern** (when you don't know exact namespace): + +```powershell +# Search for types matching a pattern +$asm.GetTypes() | Where-Object { $_.Name -like "*Response*" } | ForEach-Object { Write-Host $_.FullName } + +# Find methods on a type +$type.GetMethods() | Where-Object { $_.Name -like "*Async*" } | Select-Object Name, ReturnType +``` + +**Common assemblies to inspect** (after `dotnet build`): + +| Assembly | Path | Contains | +|----------|------|----------| +| `Azure.AI.Projects.dll` | bin/Debug/net10.0/ | AIProjectClient, AgentReference | +| `Azure.AI.Projects.Agents.dll` | bin/Debug/net10.0/ | AgentAdministrationClient, ProjectsAgentVersion, DeclarativeAgentDefinition | +| `Azure.AI.Extensions.OpenAI.dll` | bin/Debug/net10.0/ | ProjectOpenAIClient, ProjectConversationsClient, ProjectResponsesClient | +| `OpenAI.dll` | bin/Debug/net10.0/ | ResponseItem, StreamingResponse*, annotations | +| `Azure.Identity.dll` | bin/Debug/net10.0/ | Credential types | + +**When to use**: Beta SDK properties aren't in docs, IDE tooltips are incomplete, or you need to verify a type's actual API surface. + +**Limitation**: Returns raw API surface without intent or usage guidance. Combine with GitHub source for context. + +## Related Skills + +- **implementing-chat-streaming** - SSE streaming patterns and backend endpoint implementation +- **troubleshooting-authentication** - MSAL/JWT debugging for 401 errors +- **researching-azure-ai-sdk** - SDK research workflow and sample repositories diff --git a/.github/skills/writing-typescript-code/SKILL.md b/.github/skills/writing-typescript-code/SKILL.md new file mode 100644 index 0000000..ef453a4 --- /dev/null +++ b/.github/skills/writing-typescript-code/SKILL.md @@ -0,0 +1,254 @@ +--- +name: writing-typescript-code +description: Provides TypeScript and React coding standards for this repository. Use when writing or modifying TypeScript code, creating React components, implementing MSAL authentication, or working with the frontend. +--- + +# TypeScript Coding Standards + +**Goal**: Write type-safe React components with proper MSAL integration + +## Hot Module Replacement (HMR) Workflow + +**The frontend runs with Vite HMR**. When you edit TypeScript/React code: + +1. **Save the file** - Vite instantly updates the browser (no refresh needed) +2. **Check the terminal** - Look for HMR updates in the "Frontend: React Vite" terminal +3. **State is preserved** - React state persists through most edits + +**VS Code Tasks** (use `Run Task` command or check terminal panel): +- `Frontend: React Vite` - Runs `npm run dev` with HMR enabled +- Logs are visible directly in VS Code terminal + +**No restart needed** - Just edit, save, and see changes instantly in the browser. + +**Testing changes**: Use Playwright browser tools to: +- Navigate to http://localhost:5173 +- Check browser console logs for state transitions and errors +- Inspect network requests for API validation + +## TypeScript Config + +Enable strict mode + explicit types (avoid `any`): + +```json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true + } +} +``` + +## React Components + +Use functional components + hooks + typed props: + +```typescript +interface MessageProps { + message: string; + sender: 'user' | 'agent'; +} + +function Message({ message, sender }: MessageProps) { + return
{message}
; +} +``` + +## MSAL Pattern + +**Always**: Try silent first, fallback to popup: + +```typescript +try { + const { accessToken } = await instance.acquireTokenSilent({ + ...tokenRequest, + account: accounts[0] + }); + return accessToken; +} catch { + const { accessToken } = await instance.acquireTokenPopup(tokenRequest); + return accessToken; +} +``` + +## Environment Variables + +**CRITICAL**: Access at module level only (build-time replacement): + +```typescript +// ✅ Correct - module level +const clientId = import.meta.env.VITE_ENTRA_SPA_CLIENT_ID; + +// ❌ Wrong - inside function (won't work after build) +function getClientId() { + return import.meta.env.VITE_ENTRA_SPA_CLIENT_ID; +} +``` + +**Available variables**: +- `VITE_ENTRA_SPA_CLIENT_ID` - Entra app client ID +- `VITE_ENTRA_TENANT_ID` - Azure tenant ID + +## State Management + +Use `useState` (local) or Context API (shared): + +```typescript +const [messages, setMessages] = useState([]); +const [loading, setLoading] = useState(false); +const [error, setError] = useState(null); +``` + +## Memoization Patterns + +Use `useMemo` and `useCallback` for expensive computations and stable references: + +```typescript +// Memoize computed values +const isAuthenticated = useMemo( + () => accounts.length > 0, + [accounts.length] +); + +// Memoize callbacks to prevent child re-renders +const getAccessToken = useCallback(async () => { + // ... token acquisition logic +}, [instance, accounts]); + +// Return memoized object for stable reference +return useMemo( + () => ({ getAccessToken, isAuthenticated, user }), + [getAccessToken, isAuthenticated, user] +); +``` + +## API Calls + +Include `Authorization` header + use `async/await`: + +```typescript +const response = await fetch('/api/endpoint', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) +}); + +if (!response.ok) throw new Error(`API error: ${response.status}`); +``` + +## npm Dependencies + +`frontend/.npmrc` sets `legacy-peer-deps=true` automatically — no flag needed when running from `frontend/`. + +**Gotcha**: `legacy-peer-deps` skips automatic peer dependency installation. If a package requires peer deps, add them explicitly to `package.json`. + +**Example**: `@lexical/yjs` requires `yjs` as a peer dependency. Since peer deps aren't auto-installed, `yjs` must be in `package.json` directly. + +**Before committing package changes**, verify with: +```bash +npm ci # Fails if lock file is out of sync with package.json +``` + +## Common Mistakes + +- ❌ Accessing `import.meta.env.*` in functions +- ❌ Calling hooks conditionally or in loops +- ❌ Using `any` type +- ❌ Storing tokens in component state +- ❌ Running `npm install` outside `frontend/` (misses `.npmrc` config) +- ❌ Missing memoization in custom hooks (causes infinite re-renders) +- ❌ Returning new objects from hooks without `useMemo` + +--- + +## Project-Specific: Architecture + +| Concern | Implementation | +|---------|----------------| +| **State Management** | Centralized Context + `useReducer` (`AppContext`) with discriminated action union | +| **Authentication** | MSAL redirect flow; silent token refresh; `useAuth` hook | +| **Chat Streaming** | SSE in `ChatService` with abort controllers for cancellation | +| **Accessibility** | Live region (`aria-live`), aria labels, focus management | +| **Logging** | Dev-only diff-based logger | + +## Project-Specific: Key Components + +| Component | Purpose | +|-----------|---------| +| `AgentChat.tsx` | Container wiring chat state to controlled `ChatInterface` | +| `ChatInterface.tsx` | Stateless controlled UI; renders messages, input, errors, BuiltWithBadge | +| `chat/AssistantMessage.tsx` | Memoized assistant message with streaming + citation footnotes | +| `chat/UserMessage.tsx` | Memoized user message with image thumbnail previews | +| `chat/ChatInput.tsx` | File uploads, character counter, cancel streaming button | +| `chat/CitationMarker.tsx` | Inline superscript citation badge with tooltip + click handler | +| `core/Markdown.tsx` | Renders markdown with inline citation markers via `ContentWithCitations` | +| `core/BuiltWithBadge.tsx` | \"Built with Microsoft Foundry\" link badge (centered under input) | + +## Project-Specific: Citation System + +**Parser**: `frontend/src/utils/citationParser.ts` + +Handles Azure AI Agent citation formats: +- Assistants/Responses API: `【4:0†source】`, `【13†myfile.pdf】` +- Azure OpenAI On Your Data: `[doc1]`, `[doc2]` + +**Flow**: +1. `parseContentWithCitations()` replaces placeholders with `[N]` markers +2. `Markdown.tsx` renders `CitationMarker` components for each `[N]` +3. Clicking inline marker scrolls to footnote (with highlight animation) or opens URL +4. `AssistantMessage.tsx` renders footnote list with icons by type (URI/file/document) + +**Key Types** (`frontend/src/types/chat.ts`): +- `IAnnotation` - Citation metadata (type, label, url, fileId, quote, textToReplace) +- `IndexedCitation` - Parsed citation with display index + +## Project-Specific: File Upload Validation + +**Limits**: 5MB per file, max 5 files total + +**See**: `frontend/src/utils/fileAttachments.ts` for `validateImageFile()` and `validateFileCount()` + +## Project-Specific: ChatService + +**File**: `frontend/src/services/chatService.ts` + +Key patterns: +- Class-based service with `Dispatch` for state updates +- `AbortController` for stream cancellation (`cancelStream()`) +- `retryWithBackoff()` for resilient API calls (3 retries, 1s initial delay) +- SSE parsing via `parseSseLine()` and `splitSseBuffer()` utilities +- Duplicate chunk suppression to prevent UI flicker + +**Methods**: +| Method | Purpose | +|--------|---------| +| `sendMessage()` | Orchestrates auth, file conversion, streaming | +| `cancelStream()` | Aborts active stream, dispatches `CHAT_CANCEL_STREAM` | +| `clearChat()` | Resets conversation state | +| `clearError()` | Clears error without affecting chat | + +## Project-Specific: Adding Features + +1. **Extend state**: Add discriminated action to `AppAction` union in `frontend/src/types/appState.ts` +2. **Handle in reducer**: Update `frontend/src/reducers/appReducer.ts` (keep pure, no side effects) +3. **Create service method**: Add to `ChatService` if network interaction needed +4. **Wire container**: Update `AgentChat.tsx` to dispatch actions +5. **Update UI**: Pass callbacks to controlled component + +## Project-Specific: Accessibility Checklist + +- ✅ Live region announces latest assistant message +- ✅ `aria-busy` attribute on messages container during streaming +- ✅ Buttons have `aria-label` when icon-only +- ✅ Focus returns to input after sending +- ✅ Character counter linked via `aria-describedby` + +## Related Skills + +- **implementing-chat-streaming** - SSE streaming patterns and frontend state flow +- **troubleshooting-authentication** - MSAL popup issues and token debugging +- **testing-with-playwright** - Browser testing and accessibility validation diff --git a/.github/skills/writing-unit-tests-csharp/SKILL.md b/.github/skills/writing-unit-tests-csharp/SKILL.md new file mode 100644 index 0000000..284cd52 --- /dev/null +++ b/.github/skills/writing-unit-tests-csharp/SKILL.md @@ -0,0 +1,154 @@ +--- +name: writing-unit-tests-csharp +description: Guidelines and patterns for writing unit tests in C# using MSTest SDK. +--- +# Skill: Writing C# Unit Tests + +## Overview + +This skill covers writing unit tests for the backend using **MSTest SDK**. The project uses a lean, zero-config approach that leverages Microsoft's official SDK-style testing. + +## Project Structure + +```text +backend/ +├── WebApp.sln +├── WebApp.Api/ +│ ├── Models/ # DTOs and request/response models +│ ├── Services/ # Business logic and agent integration +│ └── Program.cs # Minimal API endpoints +└── WebApp.Api.Tests/ + ├── WebApp.Api.Tests.csproj + └── [TestClass].cs # Test files organized by class under test +``` + +## Test Project Configuration + +The test project uses MSTest SDK which eliminates the need for explicit package references: + +See `backend/WebApp.Api.Tests/WebApp.Api.Tests.csproj` for current configuration. The project uses MSTest SDK which eliminates the need for explicit package references — just set `Sdk="MSTest.Sdk/{version}"` in the project element. + +## Test Anatomy + +```csharp +using WebApp.Api.Models; + +namespace WebApp.Api.Tests; + +[TestClass] +public class ErrorResponseFactoryTests +{ + [TestMethod] + public void CreateFromException_ReturnsCorrectStatusCode() + { + // Arrange + var exception = new InvalidOperationException("Test"); + + // Act + var result = ErrorResponseFactory.CreateFromException(exception, 500, isDevelopment: false); + + // Assert + Assert.AreEqual(500, result.Status); + } + + [TestMethod] + [DataRow(400, "Bad Request")] + [DataRow(401, "Unauthorized")] + [DataRow(500, "Internal Server Error")] + public void CreateFromException_MapsStatusToTitle(int status, string expectedTitle) + { + // Parameterized test using DataRow + } +} +``` + +## Running Tests + +```powershell +# Run all backend tests +cd backend +dotnet test + +# Run with verbose output +dotnet test --verbosity normal + +# Run specific test class +dotnet test --filter "FullyQualifiedName~ErrorResponseFactoryTests" + +# Run with coverage (requires coverlet) +dotnet test --collect:"XPlat Code Coverage" +``` + +## Testable Units in This Project + +### Models (Pure, Easy to Test) + +| Class | What to Test | +|-------|--------------| +| `ErrorResponseFactory` | Status code mapping, dev vs prod mode, exception detail hiding | +| `ChatRequest` | Validation logic if any | +| `StreamChunk` | Serialization/deserialization | +| `AnnotationInfo` | Property mapping | + +### Services (Require Integration Testing) + +| Class | Testing Approach | +|-------|------------------| +| `AgentFrameworkService` | Use `validating-ui-features` skill with Playwright for integration tests | + +## Test Naming Convention + +Use descriptive names following: `MethodName_Scenario_ExpectedBehavior` + +```csharp +[TestMethod] +public void CreateFromException_WhenDevelopmentMode_IncludesStackTrace() { } + +[TestMethod] +public void CreateFromException_WhenProductionMode_HidesStackTrace() { } +``` + +## Assertions Reference + +MSTest provides these assertion methods: + +```csharp +// Equality +Assert.AreEqual(expected, actual); +Assert.AreNotEqual(notExpected, actual); + +// Nullability +Assert.IsNull(value); +Assert.IsNotNull(value); + +// Boolean +Assert.IsTrue(condition); +Assert.IsFalse(condition); + +// Type +Assert.IsInstanceOfType(obj, typeof(ExpectedType)); + +// Collections +CollectionAssert.Contains(collection, element); +CollectionAssert.AreEqual(expected, actual); + +// Exceptions +Assert.ThrowsException(() => MethodThatThrows()); +``` + +## When Unit Tests Aren't Enough + +Use the `validating-ui-features` skill and Playwright when: +- Testing requires a running backend server +- Testing SSE streaming behavior +- Testing authentication flows +- Testing the full chat request/response cycle + +## Quick Reference + +| Command | Purpose | +|---------|---------| +| `dotnet test` | Run all tests | +| `dotnet test --filter "Name~Test"` | Run filtered tests | +| `dotnet build` | Build without running | +| `dotnet test --list-tests` | List all tests | diff --git a/.github/skills/writing-unit-tests-typescript/SKILL.md b/.github/skills/writing-unit-tests-typescript/SKILL.md new file mode 100644 index 0000000..ee8d938 --- /dev/null +++ b/.github/skills/writing-unit-tests-typescript/SKILL.md @@ -0,0 +1,219 @@ +--- +name: writing-unit-tests-typescript +description: Guidelines and patterns for writing unit tests in TypeScript using Vitest. +--- +# Skill: Writing TypeScript Unit Tests + +## Overview + +This skill covers writing unit tests for the frontend using **Vitest**. + +## Project Structure + +```text +frontend/ +├── package.json # vitest + jsdom devDependencies +├── vite.config.ts # Vitest config inline +└── src/ + ├── config/ + │ └── __tests__/ + │ └── authConfig.test.ts + ├── utils/ + │ ├── citationParser.ts + │ ├── sseParser.ts + │ ├── fileAttachments.ts + │ └── __tests__/ + │ ├── citationParser.test.ts + │ ├── sseParser.test.ts + │ └── fileAttachments.test.ts + ├── services/ + │ └── __tests__/ + │ └── chatService.test.ts + └── reducers/ + ├── appReducer.ts + └── __tests__/ + └── appReducer.test.ts # Includes state shape snapshot +``` + +## Configuration + +The Vitest config lives inline in `vite.config.ts`: + +```typescript +export default defineConfig({ + // ... existing config + test: { + globals: true, + environment: "jsdom", + include: ["src/**/*.test.{ts,tsx}"], + }, +}); +``` + +## Test Anatomy + +```typescript +import { describe, it, expect } from "vitest"; +import { parseCitations, deduplicateAnnotations } from "../citationParser"; + +describe("citationParser", () => { + describe("parseCitations", () => { + it("returns empty array for empty input", () => { + expect(parseCitations("", [])).toEqual([]); + }); + + it("extracts citation markers from text", () => { + const text = "Hello [1] world [2]"; + const result = parseCitations(text, annotations); + expect(result).toHaveLength(2); + }); + }); + + describe("deduplicateAnnotations", () => { + it("removes duplicate annotations by URL", () => { + const annotations = [ + { url: "https://example.com", title: "Example" }, + { url: "https://example.com", title: "Example Duplicate" }, + ]; + expect(deduplicateAnnotations(annotations)).toHaveLength(1); + }); + }); +}); +``` + +## Running Tests + +```powershell +# Run tests in watch mode (interactive) +cd frontend +npm test + +# Run tests once (CI mode) +npm run test:run + +# Run with coverage +npm run test:coverage + +# Run specific file +npx vitest run src/utils/__tests__/citationParser.test.ts + +# Run tests matching pattern +npx vitest run --testNamePattern="parseCitations" +``` + +## Testable Units in This Project + +### Utils (Pure Functions - Easy to Test) + +| File | Functions to Test | +|------|-------------------| +| `citationParser.ts` | `parseContentWithCitations` | +| `sseParser.ts` | `parseSseLine`, `splitSseBuffer` | +| `fileAttachments.ts` | `validateFile`, `validateImageFile`, `validateDocumentFile`, `validateFileCount`, `getEffectiveMimeType`, `convertFilesToDataUris` | +| `errorHandler.ts` | `getUserFriendlyMessage`, `createAppError`, `getErrorCodeFromResponse`, `parseErrorFromResponse`, `getErrorCodeFromMessage`, `isTokenExpiredError`, `isNetworkError`, `retryWithBackoff` | + +### Reducers (Pure Functions - Easy to Test) + +| File | What to Test | +|------|--------------| +| `appReducer.ts` | All action types, state transitions, immutability | + +### Components (Require React Testing Library) + +For component testing, add `@testing-library/react` only when needed: + +```typescript +import { render, screen } from "@testing-library/react"; +import { ChatMessage } from "../ChatMessage"; + +it("renders message content", () => { + render(); + expect(screen.getByText("Hello")).toBeInTheDocument(); +}); +``` + +## Assertions Reference + +Vitest uses Chai-style assertions via `expect`: + +```typescript +// Equality +expect(actual).toBe(expected); // strict equality (===) +expect(actual).toEqual(expected); // deep equality + +// Truthiness +expect(value).toBeTruthy(); +expect(value).toBeFalsy(); +expect(value).toBeNull(); +expect(value).toBeUndefined(); + +// Numbers +expect(num).toBeGreaterThan(5); +expect(num).toBeLessThanOrEqual(10); + +// Strings +expect(str).toContain("substring"); +expect(str).toMatch(/regex/); + +// Arrays +expect(arr).toHaveLength(3); +expect(arr).toContain(item); + +// Objects +expect(obj).toHaveProperty("key"); +expect(obj).toMatchObject({ partial: "match" }); + +// Exceptions +expect(() => throwingFn()).toThrow(); +expect(() => throwingFn()).toThrowError("message"); + +// Async +await expect(asyncFn()).resolves.toBe(value); +await expect(asyncFn()).rejects.toThrow(); +``` + +## Test Organization + +Use `describe` blocks to group related tests: + +```typescript +describe("moduleName", () => { + describe("functionName", () => { + it("handles normal case", () => {}); + it("handles edge case", () => {}); + it("throws on invalid input", () => {}); + }); +}); +``` + +## State Shape Snapshot Tests + +Use state shape snapshots to prevent accidental state changes from going unnoticed. If a new field is added to `AppState` without updating the test, it fails: + +```typescript +it('should have expected state shape (update this test when adding new state fields)', () => { + const shape = JSON.stringify(Object.keys(initialAppState).sort()); + expect(shape).toBe('["auth","chat","conversations","ui"]'); + const convShape = JSON.stringify(Object.keys(initialAppState.conversations).sort()); + expect(convShape).toBe('["hasMore","isLoading","list","sidebarOpen"]'); +}); +``` + +This forces anyone adding state fields to also add test coverage — the test file becomes the registry of all state. Apply this pattern to any new top-level state domain. + +## When Unit Tests Aren't Enough + +Use the `validating-ui-features` skill and Playwright when: +- Testing requires browser interaction (clicking, navigation) +- Testing authentication flows with MSAL +- Testing SSE streaming with real backend +- Visual regression testing + +## Quick Reference + +| Command | Purpose | +|---------|---------| +| `npm test` | Watch mode | +| `npm run test:run` | Run once | +| `npm run test:coverage` | With coverage | +| `npx vitest --ui` | Interactive UI | diff --git a/.gitignore b/.gitignore index 977d377..8db6499 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ dist/ .vscode/* !.vscode/settings.json !.vscode/tasks.json +!.vscode/launch.json +!.vscode/mcp.json .idea/ *.swp *.swo diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..51072a9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,51 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Launch ASP.NET Core API with debugger (C# Dev Kit) + // Uses the "http" profile from launchSettings.json → localhost:8080 + "name": ".NET: Launch Backend", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/backend/WebApp.Api/WebApp.Api.csproj", + "preLaunchTask": "build-backend" + }, + { + // Attach to an already-running dotnet process (e.g. dotnet watch) + // Use the process picker to select the correct process + "name": ".NET: Attach to Backend", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + }, + { + // Debug React frontend in Chrome + // Start the Vite dev server first (preLaunchTask), then open Chrome + "name": "Chrome: Frontend", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}/frontend/src", + "sourceMaps": true, + "preLaunchTask": "Frontend: React Vite" + }, + { + // Debug React frontend in Edge (alternative) + "name": "Edge: Frontend", + "type": "msedge", + "request": "launch", + "url": "http://localhost:5173", + "webRoot": "${workspaceFolder}/frontend/src", + "sourceMaps": true, + "preLaunchTask": "Frontend: React Vite" + } + ], + "compounds": [ + { + // Launch backend API + Chrome frontend together + "name": "Full Stack Debug", + "configurations": [".NET: Launch Backend", "Chrome: Frontend"], + "stopAll": true + } + ] +} diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..e93637c --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,47 @@ +{ + // ============================================ + // MCP Server Configuration + // ============================================ + // + // This file configures Model Context Protocol (MCP) servers for VS Code. + // MCP servers extend Copilot's capabilities with external tools. + // + // See: https://code.visualstudio.com/docs/copilot/chat/mcp-servers + // ============================================ + + "servers": { + // ============================================ + // Playwright Browser Automation + // ============================================ + // + // Provides browser automation tools for testing and web interaction. + // Uses smaller viewport (1024x768) to reduce screenshot token costs. + // Delegate multi-page testing to subagents to preserve parent context. + // + // Tools: browser_navigate, browser_snapshot, browser_click, etc. + // See: https://github.com/microsoft/playwright-mcp + // ============================================ + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--viewport-size=1024,768" + ] + }, + + // ============================================ + // Microsoft Learn Documentation + // ============================================ + // + // Search and fetch official Microsoft/Azure documentation. + // Provides code samples, tutorials, and API references. + // + // Tools: microsoft_docs_search, microsoft_docs_fetch, microsoft_code_sample_search + // See: https://learn.microsoft.com/en-us/training/support/mcp + // ============================================ + "microsoftdocs": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + } + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index b5fa788..f2bd944 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,124 @@ { + // ============================================ // GitHub Copilot Configuration + // ============================================ "github.copilot.enable": { "*": true }, - "github.copilot.chat.codeGeneration.useInstructionFiles": true, - - // Disable all markdown validation + + // ============================================ + // Agent & Chat Features (VS Code 1.106-1.109) + // ============================================ + + // Enable custom agents as subagents (allows agents to delegate to other agents) + // See: https://code.visualstudio.com/updates/v1_109#_control-how-custom-agents-are-invoked + "chat.customAgentInSubagent.enabled": true, + + // Use custom agents with background agents + "github.copilot.chat.cli.customAgents.enabled": true, + + // Enable built-in GitHub MCP Server for seamless GitHub integration + "github.copilot.chat.githubMcpServer.enabled": true, + + // ============================================ + // Agent Optimizations (1.109) + // ============================================ + + // Copilot Memory - stores and recalls context across sessions (Preview) + // See: https://code.visualstudio.com/updates/v1_109#_copilot-memory-preview + "github.copilot.chat.copilotMemory.enabled": true, + + // Search subagent - isolated agent loop for iterative codebase searches (Experimental) + // See: https://code.visualstudio.com/updates/v1_109#_search-subagent-experimental + "github.copilot.chat.searchSubagent.enabled": true, + + // Context editing for Anthropic models - manages longer conversations efficiently (Experimental) + // See: https://code.visualstudio.com/updates/v1_109#_anthropic-models + "github.copilot.chat.anthropic.contextEditing.enabled": true, + + // ============================================ + // Chat UX Features (1.109) + // ============================================ + + // Message steering and queueing - send follow-up messages while a request runs (Experimental) + // See: https://code.visualstudio.com/updates/v1_109#_message-steering-and-queueing-experimental + "chat.requestQueuing.enabled": true, + + // Ask Questions tool - agent asks clarifying questions instead of guessing (Experimental) + // See: https://code.visualstudio.com/updates/v1_109#_ask-questions-tool-experimental + "chat.askQuestions.enabled": true, + + // Agent status indicator in command center for session visibility + // See: https://code.visualstudio.com/updates/v1_109#_agent-status-indicator + "chat.agentsControl.enabled": true, + + // Show chat title for easy session identification + "chat.viewTitle.enabled": true, + + // Enable Agent Sessions view integration + "chat.viewSessions.enabled": true, + + // ============================================ + // Agent Customization (1.109) + // ============================================ + + // Agent hooks - run custom shell commands at agent lifecycle points (Preview) + // See: https://code.visualstudio.com/updates/v1_109#_agent-hooks-preview + "chat.hooks.enabled": true, + + // Agent customization skill - teaches agent to help set up agents/skills/instructions (Experimental) + // See: https://code.visualstudio.com/updates/v1_109#_agent-customization-skill-experimental + "chat.agentCustomizationSkill.enabled": true, + + // ============================================ + // Inline Chat (1.109) + // ============================================ + + // Inline chat affordance - show gutter icon to trigger inline chat (Preview) + // See: https://code.visualstudio.com/updates/v1_109#_inline-chat-ux-revamp-preview + "inlineChat.affordance": "gutter", + + // ============================================ + // Terminal Tool Auto-Approval (Security) + // ============================================ + + // Enable terminal auto-approve for common safe commands + "chat.tools.terminal.enableAutoApprove": true, + + // Auto-approve npm scripts defined in package.json (requires workspace trust) + "chat.tools.terminal.autoApproveWorkspaceNpmScripts": true, + + // Prevent terminal tool commands from being added to shell history + "chat.tools.terminal.preventShellHistory": true, + + // ============================================ + // MCP Configuration + // ============================================ + + // Auto-start MCP servers when configuration changes + "chat.mcp.autostart": "newAndOutdated", + + // ============================================ + // Terminal + // ============================================ + + // Enable terminal IntelliSense (stable in VS Code 1.106+) + "terminal.integrated.suggest.enabled": true, + + // Limit terminal scrollback to prevent overwhelming AI context + "terminal.integrated.scrollback": 500, + + // ============================================ + // Markdown Settings + // ============================================ + + // Disable all markdown validation (for instruction files) "markdown.validate.enabled": false, "markdown.validate.ignoredLinks": ["**"], - - // Disable specific language features that might cause problems + "[markdown]": { "editor.defaultFormatter": null, "editor.formatOnSave": false - }, - - /// Enable nested agents markdown files for chat - "chat.useNestedAgentsMdFiles": true + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ceaee1d..83673e4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,6 +12,120 @@ "panel": "dedicated", "focus": false } + }, + { + "label": "Backend: ASP.NET Core API", + "type": "shell", + "command": "dotnet", + "args": ["watch", "run", "--no-hot-reload"], + "options": { + "cwd": "${workspaceFolder}/backend/WebApp.Api" + }, + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^(.*)$", + "file": 1, + "line": 2, + "message": 3 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^dotnet watch", + "endsPattern": "Now listening on|Application started" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev-servers", + "echo": true, + "showReuseMessage": false + } + }, + { + "label": "Frontend: React Vite", + "type": "shell", + "command": "npm", + "args": ["run", "dev"], + "options": { + "cwd": "${workspaceFolder}/frontend" + }, + "dependsOn": ["Install Frontend Dependencies"], + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^(.*)$", + "file": 1, + "line": 2, + "message": 3 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^\\s*VITE", + "endsPattern": "Local:|ready in" + } + }, + "presentation": { + "reveal": "always", + "panel": "dedicated", + "group": "dev-servers", + "echo": true, + "showReuseMessage": false + } + }, + { + "label": "Validate Configuration", + "type": "shell", + "command": "pwsh", + "args": ["-File", "${workspaceFolder}/deployment/scripts/validate-config.ps1"], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared", + "showReuseMessage": false + } + }, + { + "label": "Start Dev (VS Code Terminals)", + "dependsOn": ["Backend: ASP.NET Core API", "Frontend: React Vite"], + "dependsOrder": "parallel", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "build-backend", + "type": "process", + "command": "dotnet", + "args": ["build", "${workspaceFolder}/backend/WebApp.Api/WebApp.Api.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary;ForceNoAlign"], + "problemMatcher": "$msCompile", + "presentation": { + "reveal": "silent", + "panel": "shared", + "showReuseMessage": false + } + }, + { + "label": "Install Frontend Dependencies", + "type": "shell", + "command": "npm", + "args": ["install", "--legacy-peer-deps"], + "options": { + "cwd": "${workspaceFolder}/frontend" + }, + "problemMatcher": [], + "presentation": { + "reveal": "silent", + "panel": "shared", + "showReuseMessage": false + } } ] } diff --git a/ARCHITECTURE-FLOW.md b/ARCHITECTURE-FLOW.md new file mode 100644 index 0000000..d134470 --- /dev/null +++ b/ARCHITECTURE-FLOW.md @@ -0,0 +1,681 @@ +# Architecture Flow + +State transitions and data flow diagrams for the foundry-agent-webapp application. + +## Overview + +The app has two distinct state domains: + +| Domain | Location | Pattern | +|--------|----------|---------| +| **Frontend** | React (AppContext) | useReducer with discriminated actions | +| **Backend** | ASP.NET Core | Stateless request handling + lazy-cached agent | + +--- + +## Part 1: Backend Flow + +### 1.1 Request Pipeline + +```mermaid +flowchart TB + subgraph Middleware["ASP.NET Core Pipeline"] + direction TB + A[Incoming Request] --> B{Static File?} + B -->|Yes| C[UseStaticFiles] + B -->|No| D[UseCors] + D --> E[UseAuthentication] + E --> F[UseAuthorization] + F --> G{Has Valid JWT?} + G -->|No| H[401 Unauthorized] + G -->|Yes| I{Has Chat.ReadWrite Scope?} + I -->|No| J[403 Forbidden] + I -->|Yes| K[Route Handler] + end + + K --> L{Which Endpoint?} + L -->|/api/chat/stream| M[StreamChatMessage] + L -->|/api/agent| N[GetAgentMetadata] + L -->|/api/agent/info| Q[GetAgentInfo] + L -->|/api/health| O[GetHealth] + L -->|/api/conversations| R[ListConversations] + L -->|/api/conversations/*/messages| S[GetConversationMessages] + L -->|DELETE /api/conversations/*| T[DeleteConversation ⚠️ 501] + L -->|/api/files/*| U[DownloadFile] + L -->|/*| P[Fallback: index.html] +``` + +### 1.2 Credential Resolution + +```mermaid +stateDiagram-v2 + [*] --> CheckEnvironment + + CheckEnvironment --> Development: ASPNETCORE_ENVIRONMENT = Development + CheckEnvironment --> Production: Otherwise + + state Development { + [*] --> ChainedCredential + ChainedCredential --> TryAzureCli: First + TryAzureCli --> TryAzdCli: Fallback + TryAzdCli --> CredentialReady: Success + TryAzdCli --> CredentialFailed: Both failed + } + + state Production { + [*] --> CheckOBO + CheckOBO --> OBOMode: ENTRA_BACKEND_CLIENT_ID AND TenantId set + CheckOBO --> MIOnly: Not set + + state OBOMode { + [*] --> ExtractJWT: Per-request + ExtractJWT --> GetFICAssertion: JWT found + ExtractJWT --> Error: No JWT (throws InvalidOperationException) + GetFICAssertion --> CreateOBO: ManagedIdentityClientAssertion + CreateOBO --> CredentialReady: OnBehalfOfCredential + } + + note right of OBOMode + FIC created in postprovision.ps1 (not Bicep) + — Graph API eventual consistency prevents + creating FIC in same deployment as parent app. + User-assigned MI provides client assertion via FIC. + end note + + MIOnly --> CredentialReady: User-assigned MI + } + + CredentialReady --> CreateProjectClient + CredentialFailed --> StartupError +``` + +### 1.3 Agent Loading (Lazy Singleton) + +```mermaid +stateDiagram-v2 + [*] --> NotLoaded: Service instantiated + + NotLoaded --> Loading: First request calls GetAgentAsync + Loading --> Loading: SemaphoreSlim acquired + + Loading --> Loaded: GetAgentVersionAsync (pinned via AI_AGENT_VERSION) or GetAgentVersionsAsync (latest, first of descending list) + Loading --> Failed: Exception thrown + + Loaded --> Loaded: Subsequent requests use s_cachedAgentVersion + Failed --> Loading: Next request retries + + note right of Loaded + s_cachedAgentVersion: ProjectsAgentVersion (static) + s_cachedMetadata: AgentMetadataResponse (static) + Cached across requests (not per-instance) + end note +``` + +### 1.4 SSE Streaming Pipeline + +```mermaid +sequenceDiagram + participant Client + participant Handler as /api/chat/stream + participant Service as AgentFrameworkService + participant SDK as Azure.AI.Projects SDK + participant Agent as AI Foundry + + Client->>Handler: POST ChatRequest + Handler->>Handler: Set SSE headers + + alt New conversation + Handler->>Service: CreateConversationAsync + Service->>SDK: CreateProjectConversationAsync + SDK-->>Service: conversation.Id + end + + Handler-->>Client: data: {type: conversationId} + + Handler->>Service: StreamMessageAsync + Note over Service,SDK: ResponsesClient bound to conversationId —
conversation tracks MCP approval state + Service->>SDK: CreateResponseStreamingAsync + + loop StreamingResponseUpdate + SDK-->>Service: update + + alt TextDeltaUpdate + Service-->>Handler: StreamChunk.Text + Handler-->>Client: data: {type: chunk} + else ItemDoneUpdate (MessageItem) + Service->>Service: ExtractAnnotations + Service-->>Handler: StreamChunk.WithAnnotations + Handler-->>Client: data: {type: annotations} + else ItemDoneUpdate (McpApprovalItem) + Service-->>Handler: StreamChunk.McpApproval + Handler-->>Client: data: {type: mcpApprovalRequest} + Note over Client: Stream pauses for user decision + else CompletedUpdate + Service->>Service: Store _lastUsage + else ErrorUpdate + Service-->>Handler: throw Exception + end + end + + Handler->>Service: GetLastUsage + Handler-->>Client: data: {type: usage} + Handler-->>Client: data: {type: done} +``` + +### 1.5 Backend SSE Event Types + +| Event Type | When Sent | Payload | +|------------|-----------|---------| +| `conversationId` | First, always | `{conversationId: string}` | +| `chunk` | Per text delta | `{content: string}` | +| `annotations` | After item complete | `{annotations: AnnotationInfo[]}` — each annotation may include `containerId` for container file citations | +| `mcpApprovalRequest` | MCP tool needs approval | `{approvalRequest: {...}}` | +| `toolUse` | When agent starts using a tool | `{toolName: string}` | +| `usage` | Before done | `{duration, promptTokens, completionTokens, totalTokens}` | +| `done` | Last, always | `{}` | +| `error` | On exception | `{message: string}` | + +--- + +## Part 2: Frontend State + +The frontend manages three state domains: +- **Auth State**: User authentication lifecycle +- **Chat State**: Message and streaming lifecycle +- **UI State**: Input enablement (derived from chat state) + +### 2.1 Authentication State Machine + +```mermaid +stateDiagram-v2 + [*] --> initializing + + initializing --> authenticated: AUTH_INITIALIZED + initializing --> unauthenticated: No cached session + + authenticated --> unauthenticated: AUTH_TOKEN_EXPIRED + unauthenticated --> authenticated: AUTH_INITIALIZED + + authenticated --> error: Token acquisition fails + error --> unauthenticated: User dismisses +``` + +### Auth States + +| State | Description | User Object | +|-------|-------------|-------------| +| `initializing` | App startup, checking MSAL cache | `null` | +| `authenticated` | Valid session, user info available | `AccountInfo` | +| `unauthenticated` | No session, login required | `null` | +| `error` | Auth failure (rare) | `null` | + +--- + +### 2.2 Chat State Machine + +```mermaid +stateDiagram-v2 + [*] --> idle + + idle --> sending: CHAT_SEND_MESSAGE + + sending --> streaming: CHAT_START_STREAM + sending --> error: CHAT_ERROR + + streaming --> streaming: CHAT_STREAM_CHUNK + streaming --> streaming: CHAT_STREAM_ANNOTATIONS + streaming --> streaming: CHAT_STREAM_RETRY + streaming --> streaming: CHAT_STREAM_TOOL_USE + streaming --> idle: CHAT_STREAM_COMPLETE + streaming --> idle: CHAT_CANCEL_STREAM + streaming --> idle: CHAT_MCP_APPROVAL_REQUEST + streaming --> error: CHAT_ERROR + streaming --> error: CHAT_RECOVER_MESSAGE + + error --> idle: CHAT_CLEAR_ERROR + + idle --> idle: CHAT_MCP_APPROVAL_RESOLVED + idle --> idle: CHAT_CLEAR +``` + +### Chat States + +| State | Description | Input Enabled | streamingMessageId | +|-------|-------------|---------------|-------------------| +| `idle` | Ready for input | ✅ Yes (except during MCP approval) | `undefined` | +| `sending` | Request in flight | ❌ No | `undefined` | +| `streaming` | Receiving chunks (or retrying) | ✅ Yes (messages queue) | Message ID | +| `error` | Failure occurred | If recoverable | `undefined` | + +### Message Queue + +When the AI is streaming, the input stays enabled. Messages sent during streaming are queued in `pendingMessages[]` (with optional file attachments) and shown as dismissible chips below the input. When the stream completes and status returns to `idle`, queued messages are combined (newline-separated) into a single message and auto-sent. Files from all queued messages are merged. + +### Message Actions + +Assistant messages display a hover action bar with Copy, Regenerate, and Feedback (👍👎) buttons. User messages show an Edit button on the last message. +- **Regenerate**: Removes the last assistant response and auto-resends the user's message +- **Edit**: Removes the target message and everything after it, then auto-resends with the edited text +- **Feedback**: Tracks 👍👎 ratings to Application Insights via `trackEvent` + +### Tool-Use Visualization + +When the AI agent uses tools (file search, code interpreter, function calls), the backend streams `toolUse` SSE events. The UI shows an inline indicator (e.g., "Searching files...") on the assistant message during tool execution. + +### Input Enhancements + +- **Voice Input**: Web Speech API microphone button with feature detection +- **Drag-and-Drop**: File drop zone overlay on the chat area +- **Keyboard Shortcuts**: ⌨️ toolbar button opens shortcuts dialog; `Ctrl+N` for new chat +- **Toolbar Layout**: Primary actions (attach, cancel, voice, new chat) are always visible; secondary actions (history, export, shortcuts, settings) are in a ⋯ overflow menu to keep the UI clean + +### Conversation Management + +- **Search**: Client-side filtering in the conversation sidebar +- **Export**: Download conversation as Markdown +- **Smart Scroll**: Auto-scroll only when near bottom; "↓ New messages" pill when scrolled up + +### Stream Retry & Message Recovery + +When a stream fails, the system automatically retries up to 3 times with exponential backoff. During retries, the assistant message shows a "Retrying (2/3)..." indicator via the `CHAT_STREAM_RETRY` action. + +If all retries are exhausted, `CHAT_RECOVER_MESSAGE` removes the failed user message and assistant placeholder from the chat, restores the original message text to the input via `recoveredInput`, and shows an error banner. The user can simply press Send again. + +--- + +### 2.3 End-to-End Message Flow + +```mermaid +sequenceDiagram + participant U as User + participant UI as ChatInterface + participant R as Reducer + participant S as ChatService + participant API as /api/chat/stream + participant Agent as AI Agent + + U->>UI: Type message + Submit + UI->>R: CHAT_SEND_MESSAGE + Note over R: status: sending
messages += userMsg + + UI->>S: streamChat(request) + S->>API: POST (SSE) + API->>Agent: CreateConversationAsync + + API-->>S: data: {conversationId} + S->>R: CHAT_ADD_ASSISTANT_MESSAGE + S->>R: CHAT_START_STREAM + Note over R: status: streaming
streamingMessageId = id + + loop Each chunk + Agent-->>API: StreamingResponse + API-->>S: data: {type: chunk} + S->>R: CHAT_STREAM_CHUNK + Note over R: Append to message content + end + + alt Annotations received + API-->>S: data: {type: annotations} + S->>R: CHAT_STREAM_ANNOTATIONS + end + + alt MCP Tool Approval needed + API-->>S: data: {type: mcpApprovalRequest} + S->>R: CHAT_MCP_APPROVAL_REQUEST + Note over R: status: idle
Show approval UI + U->>UI: Approve/Deny + UI->>R: CHAT_MCP_APPROVAL_RESOLVED + Note over R: Mark card approved/rejected + UI->>S: sendMcpApproval(id, approved, prevResponseId, convId) + Note over S: Resume via /api/chat/stream with mcpApproval + end + + API-->>S: data: {type: usage} + S->>R: CHAT_STREAM_COMPLETE + Note over R: status: idle
Input enabled + + API-->>S: data: {type: done} + Note over S: Exits stream reader +``` + +--- + +### 2.4 MCP Tool Approval Flow + +```mermaid +stateDiagram-v2 + [*] --> streaming: Normal streaming + + streaming --> awaiting_approval: CHAT_MCP_APPROVAL_REQUEST + + state awaiting_approval { + [*] --> show_card: Display approval UI + show_card --> user_decides: User sees tool request + } + + awaiting_approval --> sending: User approves + awaiting_approval --> idle: User denies + + sending --> streaming: Resume with approval response + + note right of awaiting_approval + mcpApproval: { + id, toolName, serverLabel, + arguments, previousResponseId + } + Conversation-bound client + maintains pending MCP state + end note +``` + +--- + +### 2.5 Error Recovery Flow + +```mermaid +stateDiagram-v2 + [*] --> active: Normal operation + + active --> error: CHAT_ERROR + + state error { + [*] --> check_type + check_type --> recoverable: error.recoverable = true + check_type --> fatal: error.recoverable = false + } + + recoverable --> active: CHAT_CLEAR_ERROR + fatal --> [*]: Page refresh required + + note right of recoverable + Examples: + - 401 (token expired) + - 429 (rate limit) + - Network timeout + end note + + note right of fatal + Examples: + - Invalid agent config + - Server 500 + end note +``` + +--- + +### 2.6 UI State Derivation + +The UI state (`chatInputEnabled`) is derived from chat state: + +```mermaid +flowchart LR + subgraph ChatStatus + idle[idle] + sending[sending] + streaming[streaming] + error[error] + end + + subgraph InputEnabled + yes[✅ Enabled] + no[❌ Disabled] + maybe[⚠️ Conditional] + end + + idle --> yes + sending --> no + streaming --> yes + error --> maybe + + maybe --> yes + maybe --> no + + note1[/"recoverable = true"/] --> yes + note2[/"recoverable = false"/] --> no +``` + +--- + +### 2.7 Action Reference + +| Action | From State(s) | To State | Side Effects | +|--------|--------------|----------|--------------| +| `AUTH_INITIALIZED` | initializing, unauthenticated | authenticated | Set user object | +| `AUTH_TOKEN_EXPIRED` | authenticated | unauthenticated | Clear user | +| `CHAT_SEND_MESSAGE` | idle | sending | Append user message | +| `CHAT_ADD_ASSISTANT_MESSAGE` | sending | sending | Create empty assistant msg | +| `CHAT_START_STREAM` | sending | streaming | Set conversationId, messageId | +| `CHAT_STREAM_CHUNK` | streaming | streaming | Append content to msg | +| `CHAT_STREAM_ANNOTATIONS` | streaming | streaming | Add citations to msg | +| `CHAT_MCP_APPROVAL_REQUEST` | streaming | idle | Add approval message, keep input disabled | +| `CHAT_MCP_APPROVAL_RESOLVED` | idle | idle | Mark approval as approved/rejected | +| `CHAT_STREAM_COMPLETE` | streaming | idle | Add usage, enable input | +| `CHAT_CANCEL_STREAM` | streaming | idle | Enable input | +| `CHAT_STREAM_RETRY` | streaming | streaming | Reset assistant msg content, show retry indicator | +| `CHAT_RECOVER_MESSAGE` | streaming | error | Remove failed msgs, restore input text, show error | +| `CHAT_REGENERATE` | idle | idle | Remove last assistant msg, store user text as regenerateText | +| `CHAT_EDIT_MESSAGE` | idle | idle | Remove target msg + after, store new text as regenerateText | +| `CHAT_CONSUMED_REGENERATE` | any | (unchanged) | Clear regenerateText after auto-send | +| `CHAT_STREAM_TOOL_USE` | streaming | streaming | Set activeToolUse on streaming message | +| `CHAT_CONSUMED_RECOVERED_INPUT` | any | (unchanged) | Clear recoveredInput after input pre-fill | +| `CHAT_QUEUE_MESSAGE` | any | (unchanged) | Append text to pendingMessages | +| `CHAT_DEQUEUE_MESSAGE` | any | (unchanged) | Remove message at index from pendingMessages | +| `CHAT_CLEAR_QUEUE` | any | (unchanged) | Clear pendingMessages array | +| `CHAT_ERROR` | any | error | Set error, conditional input | +| `CHAT_CLEAR_ERROR` | error | idle | Clear error + recoveredInput, enable input | +| `CHAT_CLEAR` | any | idle | Reset all chat state | +| `CHAT_LOAD_CONVERSATION` | any | idle | Replace messages, set conversationId, clear pendingMessages | +| `CHAT_LOAD_MESSAGES` | any | (unchanged) | Append historical messages without changing status | +| `CONVERSATIONS_LOADING` | any | (loading) | Set conversations loading state | +| `CONVERSATIONS_SET_LIST` | any | (list updated) | Populate conversation list | +| `CONVERSATIONS_TOGGLE_SIDEBAR` | any | (sidebar toggled) | Open/close conversation sidebar | +| `CONVERSATIONS_REMOVE` | any | (list updated) | Remove conversation from list | +| `CHAT_LOAD_MESSAGES` | any | (unchanged) | Append historical messages without changing status | +| `CONVERSATIONS_LOADING_DONE` | any | (loading cleared) | Reset isLoading without clearing data | + +### 2.8 SSE Event → Action Mapping + +The `ChatService` translates backend SSE events into reducer actions: + +| SSE Event | Action Dispatched | Payload Transformation | +|-----------|-------------------|----------------------| +| `conversationId` | `CHAT_START_STREAM` | Extract `conversationId` from event | +| `chunk` | `CHAT_STREAM_CHUNK` | Extract `content` field | +| `annotations` | `CHAT_STREAM_ANNOTATIONS` | Map `AnnotationInfo[]` to `IAnnotation[]` | +| `mcpApprovalRequest` | `CHAT_MCP_APPROVAL_REQUEST` | Create approval message with `role: 'approval'` | +| `usage` | `CHAT_STREAM_COMPLETE` | Extract token counts and duration | +| `done` | No action — exits stream reader | `usage` is the sole trigger for CHAT_STREAM_COMPLETE | +| `toolUse` | `CHAT_STREAM_TOOL_USE` | `{toolName}` → `{messageId, toolName}` | +| `error` | `CHAT_ERROR` | Wrap message in `AppError` object | + +### Performance Optimizations + +The message list uses `useDeferredValue(messages)` to keep the input responsive during rapid streaming updates. The original `messages` array drives scroll behavior and accessibility announcements (immediate), while `deferredMessages` drives the heavy message list rendering (deferred). + +--- + +## Part 3: Performance Patterns + +### 3.1 Reducer Optimizations + +**Early Returns**: Return same state reference when no changes occur to prevent unnecessary re-renders: + +```typescript +case 'CHAT_STREAM_CHUNK': { + const messageIndex = state.chat.messages.findIndex( + msg => msg.id === action.messageId + ); + + if (messageIndex === -1) { + return state; // Reference equality preserved - no re-render + } + // ... +} +``` + +**Targeted Array Updates**: Only recreate the modified message object: + +```typescript +const updatedMessages = [...state.chat.messages]; +updatedMessages[messageIndex] = { + ...updatedMessages[messageIndex], + content: updatedMessages[messageIndex].content + action.content, +}; +``` + +This preserves reference equality for all other messages, preventing their components from re-rendering. + +### 3.2 Development Logging + +In development mode, the `AppContext` logs each action with state changes: + +``` +🔄 [14:32:01] CHAT_STREAM_CHUNK +Action: { type: 'CHAT_STREAM_CHUNK', messageId: '...', content: 'Hello' } +Changes: { 'chat.messages[2].content': 'He → Hello' } +``` + +Enable via: `import.meta.env.DEV` (automatic in Vite dev server). + +--- + +## Part 4: Extending the State + +### Adding a New Action + +**Step 1**: Define the action type in `frontend/src/types/appState.ts`: + +```typescript +export type AppAction = + // ... existing actions + | { type: 'MY_NEW_ACTION'; payload: MyPayloadType } +``` + +**Step 2**: Handle in reducer `frontend/src/reducers/appReducer.ts`: + +```typescript +case 'MY_NEW_ACTION': + return { + ...state, + targetDomain: { + ...state.targetDomain, + field: action.payload, + }, + }; +``` + +**Step 3**: Dispatch from service or component: + +```typescript +// In ChatService or component +dispatch({ type: 'MY_NEW_ACTION', payload: data }); +``` + +**Step 4**: Consume in UI (automatic re-render): + +```typescript +const { state } = useAppContext(); +const value = state.targetDomain.field; // Updates on action +``` + +--- + +## Part 5: Backend Patterns + +### 5.1 Attachment Validation + +The backend enforces strict validation on file attachments before sending to AI Foundry: + +**Image Limits**: + +| Rule | Limit | Error | +|------|-------|-------| +| Max images per request | 5 | HTTP 400 | +| Max size per image | 5 MB (decoded) | HTTP 400 | +| Allowed MIME types | `image/png`, `image/jpeg`, `image/gif`, `image/webp` | HTTP 400 | + +**Document Limits**: + +| Rule | Limit | Error | +|------|-------|-------| +| Max files per request | 10 | HTTP 400 | +| Max size per file | 20 MB (decoded) | HTTP 400 | +| Allowed types | PDF, plain text, markdown, CSV, JSON, HTML, XML | HTTP 400 | +| Unsupported | DOCX, PPTX, XLSX (Office documents) | HTTP 400 | + +Validation occurs in `BuildUserMessage()` before constructing the AI Foundry message payload. + +### 5.2 Error Response Format (RFC 7807) + +All API errors use standardized Problem Details format: + +```json +{ + "title": "Authentication Failed", + "status": 401, + "detail": "Token expired at 2026-01-20T14:30:00Z", + "traceId": "00-abc123..." +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `title` | string | Human-readable error summary | +| `status` | int | HTTP status code | +| `detail` | string | Specific error description | +| `traceId` | string? | Request correlation ID | +| `stackTrace` | string? | Exception stack (dev only, omitted in prod) | + +### 5.3 Async Patterns + +The backend follows strict async/await conventions: + +**✅ Do**: +- Use `async`/`await` with `CancellationToken` on all I/O +- Pass `CancellationToken` through the entire call chain +- Use `IAsyncEnumerable` for streaming responses +- Include `[EnumeratorCancellation]` attribute on streaming parameters + +**❌ Don't**: +- Call `.Result` or `.Wait()` on async methods (causes deadlocks) +- Ignore `CancellationToken` parameters +- Use synchronous I/O in async contexts +- Block on async code in constructors + +### 5.4 Configuration Keys + +| Key | Source | Purpose | Example | +|-----|--------|---------|---------| +| `AzureAd:ClientId` | .env | Entra app client ID | `abc123-...` | +| `AzureAd:TenantId` | .env | Entra tenant ID | `def456-...` | +| `AI_AGENT_ENDPOINT` | .env | AI Foundry project URL | `https://....api.azureml.ms` | +| `AI_AGENT_ID` | .env | Agent name (v2 API) | `my-agent` | +| `ASPNETCORE_ENVIRONMENT` | Environment | Development/Production | `Development` | +| `ENTRA_BACKEND_CLIENT_ID` | Container App env | Backend app ID for OBO | `59bc6af3-...` | +| `MANAGED_IDENTITY_CLIENT_ID` | Container App env | User-assigned MI client ID for OBO (`OBO_MANAGED_IDENTITY_CLIENT_ID` is a deprecated alias) | `abc123-...` | +| `APPLICATIONINSIGHTS_CONNECTION_STRING` | Container App env | Azure Monitor OpenTelemetry export (backend traces/metrics) | `InstrumentationKey=...` | +| `APPLICATIONINSIGHTS_FRONTEND_CONNECTION_STRING` | Docker build arg | Frontend browser telemetry (injected at build as `VITE_APPLICATIONINSIGHTS_CONNECTION_STRING`) | `InstrumentationKey=...` | + +The `.env` file is auto-generated by `postprovision.ps1` during `azd up` (after Bicep provisions the Entra app and infrastructure). + +--- + +## Part 6: File Reference + +### Backend + +| File | Purpose | +|------|---------| +| [backend/WebApp.Api/Program.cs](backend/WebApp.Api/Program.cs) | Request pipeline, JWT validation, SSE endpoints | +| [backend/WebApp.Api/Services/AgentFrameworkService.cs](backend/WebApp.Api/Services/AgentFrameworkService.cs) | Agent loading, streaming, credential management | +| [backend/WebApp.Api/Models/StreamChunk.cs](backend/WebApp.Api/Models/StreamChunk.cs) | SSE chunk types (text, annotations, MCP) | +| [backend/WebApp.Api/Models/ChatRequest.cs](backend/WebApp.Api/Models/ChatRequest.cs) | Request payload with attachments | + +### Frontend + +| File | Purpose | +|------|---------| +| [frontend/src/types/appState.ts](frontend/src/types/appState.ts) | State & action type definitions | +| [frontend/src/reducers/appReducer.ts](frontend/src/reducers/appReducer.ts) | Pure reducer with all transitions | +| [frontend/src/contexts/AppContext.tsx](frontend/src/contexts/AppContext.tsx) | Provider with MSAL integration | +| [frontend/src/services/chatService.ts](frontend/src/services/chatService.ts) | SSE client dispatching actions | diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..98767ac --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index cbcc59c..1b67dcc 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,77 @@ # AI Agent Web App -AI-powered web application with Entra ID authentication and Azure AI Foundry Agent Service integration. Deploy to Azure Container Apps with a single command. +AI-powered web application with Entra ID authentication and Foundry Agent Service integration. Deploy to Azure Container Apps with a single command. + +> **⚠️ Coming from the AI Foundry portal?** The portal's "View sample app code" gives you AI resource variables, but this app also needs an **Entra ID app registration** for authentication — which is created by `azd up`. Even if your AI Foundry resources already exist, you must run `azd up` before the app will work. See the [Foundry portal setup](#coming-from-the-ai-foundry-portal) section below. ## Quick Start +### Using this Template + ```powershell +# Clone or initialize from GitHub template +azd init -t microsoft-foundry/foundry-agent-webapp + +# Deploy everything azd up # Full deployment: ~10-12 minutes ``` -This command: -1. Creates Microsoft Entra ID app registration (automated) -2. Deploys Azure infrastructure (ACR, Container Apps) +**Alternative**: Use GitHub's "Use this template" button or clone directly: +```powershell +git clone https://github.com/microsoft-foundry/foundry-agent-webapp.git +cd foundry-agent-webapp +azd up +``` + +The `azd up` command: +1. Discovers AI Foundry resources in your subscription +2. Creates Microsoft Entra ID app registration (via Bicep) and Azure infrastructure (ACR, Container Apps) 3. Builds and deploys your application 4. Opens browser to your deployed app **Local Development**: http://localhost:5173 (frontend), http://localhost:8080 (backend) **Production**: https://.azurecontainerapps.io +### GitHub Codespaces + +This repo includes a devcontainer configuration for Codespaces. The `azd` CLI, .NET 10 SDK, Node.js, and PowerShell are pre-installed. Open in Codespaces, then run `azd up` from the terminal to provision the Entra app and generate `.env` files. + +> **Corporate tenants**: Codespaces VMs are not managed by Intune, so organizations with device-compliance Conditional Access policies may block `az login` or token acquisition. The `az login --use-device-code` flow authenticates on your compliant browser, but some policies evaluate the device at token-use time — not just at login. If you hit authentication errors in Codespaces, use local development instead. + ## Prerequisites -- **Azure Subscription** with Contributor role -- **PowerShell 7+** - Cross-platform scripting (https://aka.ms/powershell) +### Windows +- **PowerShell 7+** - `winget install Microsoft.PowerShell` - **Azure Developer CLI (azd)** - `winget install microsoft.azd` -- **Bicep CLI** - Installed automatically with `azd`, or manually: `az bicep install` -- **.NET 9 SDK** - https://dot.net +- **Azure CLI** - `winget install Microsoft.AzureCLI` +- **Docker Desktop** (optional) - https://docs.docker.com/desktop/install/windows-install/ +- **.NET 10 SDK** - https://dot.net +- **Node.js 18+** - https://nodejs.org + +### macOS +- **PowerShell 7+** - `brew install powershell` or [download](https://github.com/PowerShell/PowerShell/releases) +- **Azure Developer CLI (azd)** - `brew tap azure/azd && brew install azd` or `curl -fsSL https://aka.ms/install-azd.sh | bash` +- **Azure CLI** - `brew install azure-cli` or `curl -L https://aka.ms/InstallAzureCli | bash` +- **Docker Desktop** (optional) - `brew install --cask docker` or [download](https://www.docker.com/products/docker-desktop/) +- **.NET 10 SDK** - https://dot.net +- **Node.js 18+** - `brew install node` or https://nodejs.org + +> **Homebrew not installed?** Commands work without Homebrew using direct installers. The deployment script (`azd up`) checks for Homebrew and provides appropriate installation instructions. + +### Linux +- **PowerShell 7+** - https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-linux +- **Azure Developer CLI (azd)** - `curl -fsSL https://aka.ms/install-azd.sh | bash` +- **Azure CLI** - https://learn.microsoft.com/cli/azure/install-azure-cli-linux +- **Docker Engine** (optional) - https://docs.docker.com/engine/install/ +- **.NET 10 SDK** - https://dot.net - **Node.js 18+** - https://nodejs.org -- **Azure AI Foundry Resource** - Create at https://ai.azure.com with at least one agent -- **Docker Desktop** (optional) - For local builds. If not installed, `azd` uses Azure Container Registry cloud build. + +### Azure Requirements +- **Azure Subscription** with Contributor role +- **Bicep CLI** - Installed automatically with `azd`, or manually: `az bicep install` +- **Microsoft Foundry Resource** with a project and at least one v2 agent — create via [ai.azure.com](https://ai.azure.com) or deploy infrastructure with [microsoft-foundry/foundry-samples](https://github.com/microsoft-foundry/foundry-samples/tree/main/infrastructure/infrastructure-setup-bicep) Bicep templates + +> **Note**: Docker is optional. If not installed, `azd` automatically uses Azure Container Registry cloud build for deployment. ### Custom npm Registries @@ -41,31 +86,88 @@ registry=https://your-registry.example.com/ ### Organization-Specific Requirements -If your organization has custom Entra ID policies, you may need to set environment variables before deployment. See [deployment/hooks/README.md](deployment/hooks/README.md#app-registration-policies) for details. +If your organization requires a Service Management Reference for Entra ID app registrations: + +```powershell +azd env set ENTRA_SERVICE_MANAGEMENT_REFERENCE "" +``` + +See [deployment/hooks/README.md](deployment/hooks/README.md#app-registration-policies) for more organization-specific configuration options. ## VS Code Configuration The workspace includes optimized VS Code configuration for AI-assisted development: ### Tasks (`.vscode/tasks.json`) -- **Start Local Development** - Runs both frontend and backend servers simultaneously -- **Start Backend (ASP.NET Core)** - `dotnet run` with watch mode (port 8080) -- **Start Frontend (Vite)** - `npm run dev` with HMR (port 5173) + +| Task | Description | Port | +|------|-------------|------| +| `Backend: ASP.NET Core API` | `dotnet watch run` with hot reload | 8080 | +| `Frontend: React Vite` | `npm run dev` with HMR (auto-installs deps) | 5173 | +| `Start Dev (VS Code Terminals)` | Starts both in parallel (default build task) | - | +| `Validate Configuration` | Checks `.env` files for required variables | - | +| `Install Frontend Dependencies` | `npm install --legacy-peer-deps` (runs automatically) | - | + +**Hot Reload Workflow**: +- **Backend**: Edit C# → Save → .NET auto-recompiles → Check terminal for errors +- **Frontend**: Edit TypeScript/React → Save → Browser updates instantly (HMR) +- **No restarts needed** - just edit, save, and test + +**AI Agent Benefits**: Server logs are visible in VS Code terminals, allowing AI agents to: +- See compilation errors and warnings +- Monitor request handling +- Debug issues without screenshots + +### Debugging (`.vscode/launch.json`) + +| Configuration | Description | +|---------------|-------------| +| `.NET: Launch Backend` | Debug ASP.NET Core API with C# Dev Kit | +| `.NET: Attach to Backend` | Attach to running dotnet watch process | +| `Chrome: Frontend` | Debug React app in Chrome with source maps | +| `Edge: Frontend` | Debug React app in Edge | +| `Full Stack Debug` | Launch backend + Chrome together | ### Settings (`.vscode/settings.json`) -- **GitHub Copilot** - Enabled with custom agent mode support -- **Instruction Files** - Loads `.github/instructions/*.md` and `AGENTS.md` hierarchy +- **GitHub Copilot** - Code generation uses instruction files (`github.copilot.chat.codeGeneration.useInstructionFiles: true`) +- **Agent Customization** - Agent customization skill enabled (`chat.agentCustomizationSkill.enabled: true`) +- **Skills** - On-demand loading from `.github/skills/` for efficient context +- **Terminal Scrollback** - Limited to 500 lines to prevent overwhelming AI context - **Markdown Linting** - Disabled to prevent noise from instruction files ## Configuration -### Azure AI Foundry +### Microsoft Foundry -`azd up` automatically discovers your AI Foundry resource, project, and agent: +`azd up` automatically discovers your Foundry resource, project, and agent: - **1 resource found**: Auto-selects and configures RBAC - **Multiple resources found**: Prompts you to select which one to use -- **RBAC**: Automatically grants the Container App's managed identity "Cognitive Services User" role +- **RBAC**: Automatically grants the Container App's managed identity `Cognitive Services OpenAI Contributor` + `Azure AI Developer` roles + +### Coming from the AI Foundry Portal + +The portal's "View sample app code" dialog provides AI resource variables (`AI_AGENT_ENDPOINT`, `AI_AGENT_ID`, etc.), which tell the app *which agent to talk to*. However, this app also requires an **Entra ID app registration** for user authentication — which the portal does not create. Running `azd up` creates it, along with the `.env` files that wire everything together. + +**What the portal gives you**: AI Foundry project endpoint and agent ID — these identify your agent. +**What `azd up` adds**: Entra app registration, JWT auth config, redirect URIs, RBAC grants, Azure infrastructure. + +Without `azd up`, the frontend shows `undefined` in the login URL because `VITE_ENTRA_SPA_CLIENT_ID` and `VITE_ENTRA_TENANT_ID` don't exist yet. + +If you clicked "View sample app code" in the portal, you can either paste the portal variables into a root `.env` file or set them via `azd env set`, then run `azd up`: + +```powershell +# Option 1: Paste portal variables into a root .env file +# Create a .env file in the repo root with the portal values, then: +azd up + +# Option 2: Set via azd environment +azd env set AZURE_EXISTING_AGENT_ID "your-agent:2" +azd env set AZURE_EXISTING_AIPROJECT_ENDPOINT "https://your-resource.services.ai.azure.com/api/projects/your-project" +azd env set AZURE_EXISTING_RESOURCE_ID "/subscriptions/.../accounts/your-resource" +azd up +``` +The preprovision hook detects these portal variables (from either location) and maps them automatically. **Change AI Foundry resource**: ```powershell @@ -90,30 +192,83 @@ azd env set AI_AGENT_ID > 💡 `azd provision` (or `azd up`) automatically regenerates `.env` files and updates RBAC assignments when configuration changes. +## Features + +- **AI Chat** — Real-time streaming chat with Azure AI Foundry agents +- **Message Actions** — Copy, regenerate, edit, and rate responses +- **Rich Input** — Voice dictation, drag-and-drop files, keyboard shortcuts +- **Conversation Management** — History sidebar, search, export as Markdown +- **Resilience** — Auto-retry with recovery, message queueing during streaming +- **Tool Visualization** — See when the agent searches files, runs code, or calls tools + +See [`frontend/README.md`](frontend/README.md) for the full feature list. + +## Known Limitations + +- **Uploaded image files accumulate.** Image attachments are uploaded to Azure + Foundry's Files endpoint (purpose `assistants`) and referenced by file id + from the Responses API. The GA `Azure.AI.Extensions.OpenAI` and `OpenAI` + SDKs do not expose an `expires_after` parameter on file upload, so files + persist until deleted. The in-app **Settings → Uploaded files** panel lists + the count/size of files previously uploaded by this app and offers a + one-click cleanup; operators can also purge via the Foundry portal. + ## Development Workflow +### Option 1: VS Code Tasks (Recommended for AI-assisted development) +```powershell +# Run the compound task via Command Palette (Ctrl+Shift+P): +# "Tasks: Run Task" → "Start Dev (VS Code Terminals)" +# Or press Ctrl+Shift+B (default build task) + +# Servers run in VS Code terminal panel with visible logs +# AI agents can read logs via get_terminal_output +``` + +### Option 2: PowerShell Script ```powershell -# Start local development (first time or daily) +# Start local development (spawns separate terminal windows) .\deployment\scripts\start-local-dev.ps1 +``` -# Work with instant feedback: -# - React: Hot Module Replacement (HMR) -# - C#: Watch mode recompilation -# - Test at http://localhost:5173 +### Hot Reload +- **React**: Hot Module Replacement (HMR) - instant browser updates +- **C#**: Watch mode - auto-recompiles on save, check terminal for errors +- **Test at**: http://localhost:5173 +### Deploy +```powershell # Deploy code changes to Azure -.\deployment\scripts\deploy.ps1 # 3-5 minutes +azd deploy # 3-5 minutes ``` +### Setup Detection + +Multiple layers catch incomplete setup before cryptic errors appear: + +| Layer | What It Checks | When It Runs | +|-------|---------------|--------------| +| **Vite env check plugin** | `VITE_ENTRA_SPA_CLIENT_ID`, `VITE_ENTRA_TENANT_ID` | Dev server startup (`npm run dev`) — serves a styled error page instead of the app | +| **preToolUse hook** | Context-aware: frontend commands check frontend env, backend commands check backend env (including `AI_AGENT_ENDPOINT`, `AI_AGENT_ID`) | AI agents running dev commands — advisory message, non-blocking | +| **Validate Configuration task** | Both `.env` files with auth variables | On-demand via VS Code (`Tasks: Run Task` → `Validate Configuration`) | +| **`validating-local-setup` skill** | Full diagnostic checklist with error patterns and step-by-step fixes | Loaded by AI agents when setup issues are detected | + +All layers point to the same fix: run `azd up` from the repo root. + ## Architecture -**Frontend**: React 18 + TypeScript + Vite +**Frontend**: React 19 + TypeScript + Vite **Backend**: ASP.NET Core 9 Minimal APIs **Authentication**: Microsoft Entra ID (PKCE flow) -**AI Integration**: Azure AI Foundry Agent Service +**AI Integration**: Foundry Agent Service v2 Agents API (`Azure.AI.Projects` SDK) **Deployment**: Single container, Azure Container Apps **Local Dev**: Native (no Docker required) +### Known Limitations + +- **Office Documents**: DOCX, PPTX, and XLSX files are not supported for upload. Use PDF, images, or plain text files instead. +- **GA Azure SDK**: This application uses GA `Azure.AI.Projects` and `Azure.AI.Extensions.OpenAI` packages. Check `backend/WebApp.Api/WebApp.Api.csproj` for current versions. +- **npm Peer Dependencies**: React 19 has peer dependency conflicts with some packages. If adding packages that have peer dependencies (like `yjs` for `@lexical/yjs`), you must add them explicitly to `package.json`. Run `npm ci` locally to verify before committing. ## Commands @@ -123,22 +278,46 @@ azd env set AI_AGENT_ID | Command | Purpose | Duration | |---------|---------|----------| | `azd up` | Initial deployment (infra + code) | 10-12 min | -| `.\deployment\scripts\deploy.ps1` | Deploy code changes only | 3-5 min | +| `azd deploy` | Deploy code changes only | 3-5 min | | `.\deployment\scripts\start-local-dev.ps1` | Start local development | Instant | | `.\deployment\scripts\list-agents.ps1` | List agents in your project | Instant | | `azd provision` | Re-deploy infrastructure / update RBAC | 2-3 min | | `azd down --force --purge` | Delete all Azure resources | 2-3 min | -> **Why not `azd deploy`?** This template uses an infra-only pattern. The `postprovision` hook handles initial builds, and `deploy.ps1` handles code updates to avoid redundant operations. - ## Documentation -For contributors and AI agents, detailed technical documentation is available: -- `.github/copilot-instructions.md` - Architecture overview and cross-cutting patterns -- `backend/AGENTS.md` - ASP.NET Core implementation patterns -- `frontend/AGENTS.md` - React and MSAL integration patterns -- `infra/AGENTS.md` - Bicep infrastructure patterns -- `deployment/AGENTS.md` - Deployment and Docker patterns +### For Developers +- `ARCHITECTURE-FLOW.md` - State machines, data flow diagrams, and SSE event mapping +- `backend/README.md` - ASP.NET Core API setup and configuration +- `frontend/README.md` - React frontend development +- `infra/README.md` - Azure infrastructure overview +- `deployment/README.md` - Deployment scripts and hooks + +### For AI Assistants (GitHub Copilot) +This repository uses VS Code's Agent Skills feature for on-demand context loading: + +- `.github/copilot-instructions.md` - Architecture overview (always loaded) +- `.github/skills/` - Domain-specific guidance loaded when relevant: + - `understanding-architecture` - State machines, SSE events, data flow + - `deploying-to-azure` - Deployment commands and troubleshooting + - `writing-csharp-code` - C#/ASP.NET Core patterns + - `writing-typescript-code` - TypeScript/React patterns + - `writing-bicep-templates` - Bicep infrastructure patterns + - `implementing-chat-streaming` - SSE streaming patterns + - `troubleshooting-authentication` - MSAL/JWT debugging + - `researching-azure-ai-sdk` - SDK research workflow + - `testing-with-playwright` - Browser testing workflow + - `syncing-mcp-servers` - MCP server config synchronization + - `testing-cli-compatibility` - CLI compatibility validation + - `writing-unit-tests-csharp` - C#/MSTest unit test patterns + - `writing-unit-tests-typescript` - TypeScript/Vitest unit test patterns + - `validating-ui-features` - UI feature validation procedures + - `committing-code` - Commit message format and conventional commit workflow + - `validating-local-setup` - Setup diagnostics: missing env vars, `azd up` guidance + - `reviewing-documentation` - Documentation audit checklists and quality standards + - `triaging-issues` - Issue triage workflow, priority definitions, and report format + - `planning-features` - Structured plan template for feature implementation +- `.github/hooks/` — Agent hook system (commit gate, setup detection) for enforcing workflows ## Azure Resources Provisioned @@ -146,18 +325,106 @@ This template deploys the following Azure resources: - **Azure Container Apps** - Serverless container hosting (0.5 vCPU, 1GB RAM, scale-to-zero enabled) - **Azure Container Registry** - Private container image storage (Basic tier) -- **Log Analytics Workspace** - Application logging and monitoring -- **Managed Identity** - System-assigned identity with RBAC to AI Foundry resource +- **Log Analytics Workspace** - Centralized logging (30-day retention) +- **Application Insights (Backend)** - OpenTelemetry traces, metrics, and distributed tracing (`APPLICATIONINSIGHTS_CONNECTION_STRING` env var) +- **Application Insights (Frontend)** - Browser telemetry via `@microsoft/applicationinsights-web` (separate resource to isolate browser metrics from server metrics) +- **User-Assigned Managed Identity** - `isolationScope: Regional` — used for ACR pull, AI Foundry RBAC, and OBO FIC. No admin credentials or secrets. -**Local development requires no Azure resources** - runs natively without Docker or cloud dependencies. +All resources deploy to the same region (`AZURE_LOCATION`). The managed identity's regional isolation ensures it can only be assigned to compute resources in the deployment region. +> **Region tip**: For best resilience, deploy to the **same region** as your AI Foundry resource. The `preprovision` hook warns if regions don't match. To align: `azd env set AZURE_LOCATION ` +## Authentication & Identity +### Default: Managed Identity (Zero-Touch) +By default, `azd up` configures everything automatically: -## Project Structure +| Component | Identity | How | +|-----------|----------|-----| +| Frontend → Backend | User's Entra ID token (MSAL.js PKCE) | SPA app registration created by Bicep | +| Backend → Agent Service | Container App's managed identity | User-assigned MI + RBAC (see [Azure Resources](#azure-resources-provisioned)) | + +The managed identity has `Cognitive Services OpenAI Contributor` + `Azure AI Developer` roles on the AI Foundry resource. All agent tool calls (MCP, OpenAPI, Logic Apps) use the **agent's own identity** configured in the Foundry portal — NOT the web app's identity and NOT the user's identity. +**Scope requested by `AIProjectClient`**: `https://ai.azure.com/.default` + +### Advanced: On-Behalf-Of (OBO) — Opt-In + +OBO replaces the managed identity with the **user's own identity** for Agent Service API calls. This gives you per-user audit trails and rate limiting but adds enterprise friction. + +> **⚠️ Important**: OBO does NOT pass the user's identity to agent tools. Tool authentication (MCP servers, OpenAPI endpoints, Logic Apps) is controlled by the [Agent Identity](https://learn.microsoft.com/azure/ai-foundry/agents/concepts/agent-identity) configured in the Foundry portal. OBO only affects who the Agent Service API sees as the caller. + +#### How OBO Works (Secretless via Federated Identity Credential) + +```text +┌──────────┐ JWT (user) ┌──────────────┐ OBO token (user) ┌──────────────┐ +│ Frontend │ ────────────►│ Backend API │ ──────────────────►│ Agent Service│ +│ (MSAL.js)│ │ (ASP.NET) │ │ (Foundry) │ +└──────────┘ └──────────────┘ └──────────────┘ + │ + │ 1. ManagedIdentityClientAssertion + │ → gets MI token (audience: api://AzureADTokenExchange) + │ + │ 2. OnBehalfOfCredential(tenantId, backendClientId, + │ miAssertionCallback, userJWT) + │ → exchanges user JWT for OBO token + │ → scope: https://ai.azure.com/.default + │ + │ No secrets! MI token replaces client secret. +``` + +**References**: +- [OBO flow protocol](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow) +- [Federated Identity Credentials (FIC) with managed identities](https://learn.microsoft.com/entra/workload-id/workload-identity-federation-config-app-trust-managed-identity) +- [ManagedIdentityClientAssertion](https://learn.microsoft.com/entra/msal/dotnet/acquiring-tokens/web-apps-apis/workload-identity-federation) +- [Agent Identity in Foundry](https://learn.microsoft.com/azure/ai-foundry/agents/concepts/agent-identity) + +#### Enable OBO + +```powershell +azd env set ENABLE_OBO true +azd up ``` + +This creates a backend API app registration with FIC, sets `api://{backendClientId}` identifier URI, and attempts admin consent. If consent fails, follow the printed instructions. + +#### RBAC & Consent Requirements + +| Requirement | Who | What | Why | +|-------------|-----|------|-----| +| **FIC creation** | Deployer | `Application Administrator` role in Entra ID | To create the federated identity credential on the backend app | +| **Admin consent** | Entra admin | Grant **Azure Machine Learning Services / `user_impersonation`** delegated permission | The OBO token exchange requests `https://ai.azure.com/.default` which resolves to Azure Machine Learning Services (appId: `18a66f5f-dbdf-4c17-9dd7-1634712a9cbe`). Without admin consent, token acquisition fails with `AADSTS65001`. | +| **User RBAC** | Each user | `Azure AI User` role on the Foundry resource | OBO tokens carry the user's identity; the user needs data-plane permissions ([docs](https://learn.microsoft.com/azure/ai-foundry/concepts/rbac-foundry)) | +| **Known client** | Deployer (optional) | Add SPA client ID to backend app's `knownClientApplications` | Enables combined consent prompt so users consent to both SPA + backend in one step ([docs](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent)) | + +> **⚠️ Common mistake: consenting to the wrong service.** The Azure portal shows "Microsoft Cognitive Services" (`https://cognitiveservices.azure.com`, appId: `7d312290-...`) which looks like the right choice — but `AIProjectClient` actually requests tokens for `https://ai.azure.com/.default` which maps to **Azure Machine Learning Services** (appId: `18a66f5f-dbdf-4c17-9dd7-1634712a9cbe`). These are **different first-party service principals**. You must consent to the correct one: +> +> ```bash +> # Add the CORRECT permission (Azure Machine Learning Services, not Cognitive Services) +> az ad app permission add \ +> --id \ +> --api 18a66f5f-dbdf-4c17-9dd7-1634712a9cbe \ +> --api-permissions 1a7925b5-f871-417a-9b8b-303f9f29fa10=Scope +> +> # Then grant admin consent +> az ad app permission admin-consent --id +> ``` +> +> Or in the portal: API permissions → Add → "APIs my organization uses" → search **"Azure Machine Learning"** → `user_impersonation` (Delegated). + +#### OBO Gotchas + +| Gotcha | Detail | +|--------|--------| +| **Wrong consent target** | Portal shows "Microsoft Cognitive Services" (`cognitiveservices.azure.com`, appId `7d312290-...`) — this is NOT correct. `AIProjectClient` uses `ai.azure.com/.default` → **Azure Machine Learning Services** (appId `18a66f5f-...`). Consenting to wrong one gives green checkmark but runtime `AADSTS65001`. | +| **Tool identity is separate** | OBO only affects the Agent Service API caller. Agent tools (MCP, OpenAPI, Logic Apps) use the agent's identity from Foundry portal. Configure [Agent Identity](https://learn.microsoft.com/azure/ai-foundry/agents/concepts/agent-identity) separately for per-user tool access. | +| **Conversations not user-scoped in MI mode** | MI uses a shared identity — all users see all conversations. OBO provides per-user isolation. | +| **Local dev uses CLI credentials** | OBO requires a managed identity for FIC. Locally, the app uses `az login` credentials regardless of `ENTRA_BACKEND_CLIENT_ID`. | + +## Project Structure + +```text ├── backend/WebApp.Api/ # ASP.NET Core API + serves frontend ├── frontend/ # React + TypeScript + Vite ├── infra/ # Bicep infrastructure templates @@ -166,6 +433,25 @@ This template deploys the following Azure resources: │ ├── scripts/ # User commands │ └── docker/ # Multi-stage Dockerfile └── .github/ - ├── copilot-instructions.md # Architecture patterns - └── instructions/ # Language-specific standards -``` \ No newline at end of file + ├── copilot-instructions.md # Architecture overview (always loaded) + ├── hooks/ # Agent hooks (commit gate, custom policies) + └── skills/ # 18 on-demand AI assistant skills +``` + +## License + +MIT — see [LICENSE](LICENSE). This template is published by Microsoft under the +same MIT terms as the official Microsoft sample +[`Azure-Samples/get-started-with-ai-agents`](https://github.com/Azure-Samples/get-started-with-ai-agents), +which is the React/Fluent UI Copilot reference this app's chat interface was +built on top of (same `@fluentui-copilot/*` component libraries, same +streaming-chat patterns). You may use, modify, and redistribute this code — +including in commercial and white-label products — subject to the MIT license. + +> **Third-party packages.** The MIT grant covers this template's source code +> only. Each runtime dependency (npm and NuGet) is governed by its own +> license — review and accept them independently before redistribution. In +> particular, verify the current `@fluentui-copilot/*` package terms on npm: +> Microsoft's reuse of these packages in `Azure-Samples/get-started-with-ai-agents` +> indicates the component model is intended for sample/template reuse, but it +> does not by itself license those packages to you. \ No newline at end of file diff --git a/azure.yaml b/azure.yaml index a4abe1a..8bf3a51 100644 --- a/azure.yaml +++ b/azure.yaml @@ -3,29 +3,46 @@ metadata: template: ai-foundry-agent@0.0.1 location: eastus2 -# Infra-only pattern - no services defined -# Container image deployment handled by hooks - +# Infra-only pattern - container build handled by hooks for local Docker support infra: path: ./infra module: main hooks: + # Pre-deploy: Build container (local Docker if available, ACR cloud build as fallback) + predeploy: + windows: + shell: pwsh + run: ./deployment/hooks/predeploy.ps1 + continueOnError: false + posix: + shell: pwsh + run: ./deployment/hooks/predeploy.ps1 + continueOnError: false + # Phase 1: Create Entra app, generate config files preprovision: windows: shell: pwsh run: ./deployment/hooks/preprovision.ps1 continueOnError: false + posix: + shell: pwsh + run: ./deployment/hooks/preprovision.ps1 + continueOnError: false # Phase 2: Provision (automatic - Bicep templates with placeholder image) - # Phase 3: Build real image and deploy to Container App (runs after provision) + # Phase 3: Update Entra redirect URIs + assign RBAC to AI Foundry postprovision: windows: shell: pwsh run: ./deployment/hooks/postprovision.ps1 continueOnError: false + posix: + shell: pwsh + run: ./deployment/hooks/postprovision.ps1 + continueOnError: false # Cleanup Entra app and config files postdown: @@ -33,3 +50,7 @@ hooks: shell: pwsh run: ./deployment/hooks/postdown.ps1 continueOnError: false + posix: + shell: pwsh + run: ./deployment/hooks/postdown.ps1 + continueOnError: false diff --git a/azure.yaml.json b/azure.yaml.json index bf163d6..0bfcf0c 100644 --- a/azure.yaml.json +++ b/azure.yaml.json @@ -1,6 +1,6 @@ { "name": "ai-foundry-agent", - "description": "Secure AI-powered web application with .NET, React, and Azure AI Foundry Agent Service. Features Entra ID authentication with PKCE, managed identity, and automated deployment to Azure Container Apps.", + "description": "Secure AI-powered web application with .NET, React, and Foundry Agent Service. Features Entra ID authentication with PKCE, managed identity, and automated deployment to Azure Container Apps.", "author": "Microsoft", "repositoryPath": ".", "tags": [ diff --git a/backend/AGENTS.md b/backend/AGENTS.md deleted file mode 100644 index aa748a1..0000000 --- a/backend/AGENTS.md +++ /dev/null @@ -1,122 +0,0 @@ -# Backend - ASP.NET Core API - -**Context**: See `.github/copilot-instructions.md` for architecture - -## Middleware Pipeline - -**Goal**: Serve static files → validate auth → route APIs → SPA fallback - -```csharp -app.UseDefaultFiles(); // index.html for / -app.UseStaticFiles(); // wwwroot/* assets -app.UseCors(); // Dev only -app.UseAuthentication(); // Validate JWT -app.UseAuthorization(); // Enforce scope -// Map endpoints here -app.MapFallbackToFile("index.html"); // MUST BE LAST -``` - -## Endpoint Pattern - -```csharp -app.MapPost("/api/chat/stream", async ( - ChatRequest request, - AzureAIAgentService agentService, - HttpContext httpContext, - CancellationToken cancellationToken) => -{ - httpContext.Response.Headers.Append("Content-Type", "text/event-stream"); - httpContext.Response.Headers.Append("Cache-Control", "no-cache"); - - var conversationId = request.ConversationId ?? await agentService.CreateConversationAsync(request.Message, cancellationToken); - - await httpContext.Response.WriteAsync($"data: {{\"type\":\"conversationId\",\"conversationId\":\"{conversationId}\"}}\n\n", cancellationToken); - await httpContext.Response.Body.FlushAsync(cancellationToken); - - await foreach (var chunk in agentService.StreamMessageAsync(conversationId, request.Message, request.ImageDataUris, cancellationToken)) - { - var json = System.Text.Json.JsonSerializer.Serialize(new { type = "chunk", content = chunk }); - await httpContext.Response.WriteAsync($"data: {json}\n\n", cancellationToken); - await httpContext.Response.Body.FlushAsync(cancellationToken); - } - - await httpContext.Response.WriteAsync("data: {\"type\":\"done\"}\n\n", cancellationToken); -}) -.RequireAuthorization("RequireChatScope") -.WithName("StreamChatMessage"); -``` - -## Error Handling - -**Pattern**: Use `ErrorResponseFactory` for consistent error responses following RFC 7807 Problem Details. - -**See**: -- `backend/WebApp.Api/Models/ErrorResponse.cs` for `ErrorResponseFactory` implementation -- `backend/WebApp.Api/Program.cs` endpoints (`/api/chat/stream`, `/api/agent`, `/api/agent/info`) for usage patterns - -**Key points**: -- Development: Returns full exception details + stack trace in extensions -- Production: Returns user-friendly messages, hides internal details -- Maps status codes to actionable error messages - -## AzureAIAgentService Implementation - -**See**: `backend/WebApp.Api/Services/AzureAIAgentService.cs` - -**Key patterns**: -- `IDisposable` implementation with `_agentLock.Dispose()` -- Disposal guards (`ObjectDisposedException.ThrowIf`) in all public methods -- Environment-aware credential selection (dev: `ChainedTokenCredential`, prod: `ManagedIdentityCredential`) -- Cached agent instance with `SemaphoreSlim` for thread safety -- Configuration validation (`AI_AGENT_ENDPOINT`, `AI_AGENT_ID`) - -**Streaming pattern**: See `StreamMessageAsync` method for: -- Disposal guard before processing -- Multi-modal message support (text + image data URIs) -- `IAsyncEnumerable` with `[EnumeratorCancellation]` -- `MessageContentUpdate` filtering for text content - -**Image Validation**: Server-side validation enforces security constraints on base64 image data URIs: -- Maximum 5 images per request -- Maximum 5MB per image (decoded size) -- Allowed MIME types: `image/png`, `image/jpeg`, `image/jpg`, `image/gif`, `image/webp` -- Base64 integrity checking before processing -- Aggregated error reporting with structured logging via `ILogger` -- Returns HTTP 400 with validation details if constraints violated - -**See**: `ValidateImageDataUris()` method in `AzureAIAgentService.cs` for implementation details. -``` - -## JWT Validation - -**Pattern**: See `.github/instructions/csharp.instructions.md` for complete authentication setup. - -**Key detail**: Accept both `clientId` and `api://{clientId}` as valid audiences for dual-format token support. - -## Configuration (.env file) - -**Auto-loaded** before building configuration: - -```csharp -var envFile = Path.Combine(Directory.GetCurrentDirectory(), ".env"); -if (File.Exists(envFile)) -{ - foreach (var line in File.ReadAllLines(envFile) - .Where(l => !string.IsNullOrWhiteSpace(l) && !l.StartsWith("#"))) - { - var parts = line.Split('=', 2); - if (parts.Length == 2) - Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1].Trim()); - } -} -``` - -## Models - -**See**: `backend/WebApp.Api/Models/` for request/response models: -- `ChatRequest.cs` - Conversation ID, message, image data URIs -- `ChatResponse.cs` - Response message, conversation ID -- `ErrorResponse.cs` - RFC 7807 Problem Details -- `ConversationModels.cs` - Conversation creation/deletion models -- `AgentMetadata.cs` - Agent info for UI display - diff --git a/backend/README.md b/backend/README.md index e13186d..551ad8b 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,41 +1,44 @@ # Backend - ASP.NET Core API -**Technical details**: See `backend/AGENTS.md` for implementation patterns. +**AI Assistance**: See `.github/skills/writing-csharp-code/SKILL.md` for coding patterns. ## Overview ASP.NET Core 9 Minimal API application that: - Serves both the REST API (`/api/*`) and React SPA (single container pattern) - Authenticates requests via JWT Bearer tokens from Microsoft Entra ID -- Communicates with Azure AI Foundry Agent Service using managed identity +- Communicates with Foundry Agent Service using user-assigned managed identity (with optional OBO) - Streams AI agent responses via Server-Sent Events (SSE) ## Key Features - **Single Container**: Serves both API and frontend from `wwwroot` - **JWT Authentication**: Validates tokens with `Chat.ReadWrite` scope -- **AI Agent Integration**: Azure.AI.Agents.Persistent v1.2.0-beta.6 (pinned) +- **AI Agent Integration**: Azure.AI.Projects (v2 Agents API) - **Streaming**: SSE-based chat streaming with cancellation support -- **Environment-Aware Auth**: ChainedTokenCredential (dev) / ManagedIdentityCredential (prod) +- **Starter Prompts**: Dynamic prompts from agent metadata +- **Environment-Aware Auth**: ChainedTokenCredential (dev) / User-assigned ManagedIdentity (prod) / OBO (opt-in) ## Project Structure -``` +```text WebApp.Api/ ├── Program.cs # Middleware pipeline + endpoints ├── Models/ # Request/response DTOs │ ├── ChatRequest.cs │ ├── ChatResponse.cs +│ ├── AgentMetadata.cs +│ ├── StreamChunk.cs │ └── ConversationModels.cs ├── Services/ -│ └── AzureAIAgentService.cs # AI Foundry client + streaming +│ └── AgentFrameworkService.cs # AI Foundry v2 Agents API + streaming └── appsettings.json # Configuration (no secrets) ``` ## Running Locally ### Prerequisites -- .NET 9 SDK +- .NET 10 SDK - Azure CLI authenticated (`az login`) - `.env` file generated (run `azd up` first) @@ -56,7 +59,7 @@ Backend runs at http://localhost:8080 (API + static files). ### Configuration `.env` file (auto-generated by `azd up`): -``` +```ini AzureAd__ClientId=... AzureAd__TenantId=... AI_AGENT_ENDPOINT=... @@ -69,11 +72,15 @@ Environment variables are loaded before ASP.NET Core configuration builder runs. | Endpoint | Method | Auth | Purpose | |----------|--------|------|---------| -| `/api/agents` | GET | Required | List available agents (metadata) | -| `/api/chat/stream` | POST | Required | Send message, receive streaming response | -| `/api/threads` | POST | Required | Create new thread (future) | +| `/api/chat/stream` | POST | Required | Send message, receive SSE streaming response | +| `/api/agent` | GET | Required | Get agent metadata (name, model, starter prompts) | +| `/api/agent/info` | GET | Required | Debug agent info | +| `/api/conversations` | GET | Required | List past conversations | +| `/api/conversations/{id}/messages` | GET | Required | Get conversation message history | +| `/api/conversations/{id}` | DELETE | Required | Delete a conversation (returns 501 — SDK not yet supported) | +| `/api/health` | GET | None | Health check for container probes | -All endpoints require `Chat.ReadWrite` scope in JWT token. +All endpoints except /api/health require `Chat.ReadWrite` scope in JWT token. ## Development Tips @@ -94,7 +101,6 @@ Production builds are created in Docker multi-stage builds (see `deployment/dock ## Testing ```powershell -# Run tests (if added) dotnet test # Check for vulnerabilities @@ -103,21 +109,22 @@ dotnet list package --vulnerable ## Key Dependencies -| Package | Version | Purpose | -|---------|---------|---------| -| Azure.AI.Agents.Persistent | 1.2.0-beta.6 | AI Foundry Agent Service SDK (pinned) | -| Azure.Identity | Latest | ManagedIdentityCredential for Azure auth | -| Microsoft.Identity.Web | Latest | JWT Bearer authentication | -| Microsoft.AspNetCore.OpenApi | Latest | OpenAPI documentation | +| Package | Purpose | +|---------|---------| +| Azure.AI.Projects | AI Foundry v2 Agents API SDK | +| Azure.AI.Extensions.OpenAI | Project-scoped OpenAI clients (conversations, responses, files) | +| Azure.Identity | ManagedIdentityCredential + OnBehalfOfCredential | +| Microsoft.Identity.Web | JWT Bearer authentication | +| Microsoft.Identity.Web.Certificateless | Secretless OBO via ManagedIdentityClientAssertion | -**SDK pinning rationale**: Beta SDK pinned to ensure stability until GA release. +See `WebApp.Api.csproj` for current versions. Uses v2 Agents API via `AgentAdministrationClient` for metadata and `ProjectResponsesClient` for streaming (required for MCP approvals and annotations). `AI_AGENT_VERSION` pins a specific version; when unset the newest version is resolved. ## Security - ✅ JWT validation with dual audience support - ✅ HTTPS-only in production (Container Apps) - ✅ No secrets in code or `appsettings.json` -- ✅ Managed identity for Azure resource access +- ✅ User-assigned managed identity with regional isolation (no secrets) - ✅ Scope-based authorization (`Chat.ReadWrite`) ## Troubleshooting @@ -129,4 +136,4 @@ dotnet list package --vulnerable | Local auth fails | Run `az login` or `azd auth login` | | Port 8080 in use | Change in `launchSettings.json` | -See `backend/AGENTS.md` for code examples and implementation patterns. +For AI-assisted development, see `.github/skills/writing-csharp-code/SKILL.md` and `.github/skills/implementing-chat-streaming/SKILL.md`. diff --git a/backend/WebApp.Api.Tests/AgentFrameworkServiceConfigTests.cs b/backend/WebApp.Api.Tests/AgentFrameworkServiceConfigTests.cs new file mode 100644 index 0000000..2b7b917 --- /dev/null +++ b/backend/WebApp.Api.Tests/AgentFrameworkServiceConfigTests.cs @@ -0,0 +1,201 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace WebApp.Api.Tests; + +[TestClass] +public class AgentFrameworkServiceConfigTests +{ + [TestMethod] + public void UseObo_TrueWhenBackendClientIdAndTenantIdSet() + { + // OBO requires: ENTRA_BACKEND_CLIENT_ID + ENTRA_TENANT_ID + not Development + var backendClientId = "test-backend-id"; + var tenantId = "test-tenant-id"; + var environment = "Production"; + + var useObo = !string.IsNullOrEmpty(backendClientId) + && !string.IsNullOrEmpty(tenantId) + && environment != "Development"; + + Assert.IsTrue(useObo); + } + + [TestMethod] + public void UseObo_FalseInDevelopment() + { + var backendClientId = "test-backend-id"; + var tenantId = "test-tenant-id"; + var environment = "Development"; + + var useObo = !string.IsNullOrEmpty(backendClientId) + && !string.IsNullOrEmpty(tenantId) + && environment != "Development"; + + Assert.IsFalse(useObo); + } + + [TestMethod] + public void UseObo_FalseWhenBackendClientIdMissing() + { + string? backendClientId = null; + var tenantId = "test-tenant-id"; + var environment = "Production"; + + var useObo = !string.IsNullOrEmpty(backendClientId) + && !string.IsNullOrEmpty(tenantId) + && environment != "Development"; + + Assert.IsFalse(useObo); + } + + [TestMethod] + public void UseObo_FalseWhenTenantIdMissing() + { + var backendClientId = "test-backend-id"; + string? tenantId = null; + var environment = "Production"; + + var useObo = !string.IsNullOrEmpty(backendClientId) + && !string.IsNullOrEmpty(tenantId) + && environment != "Development"; + + Assert.IsFalse(useObo); + } + + [TestMethod] + public void OboRequiresManagedIdentityClientId() + { + // When OBO is enabled, MANAGED_IDENTITY_CLIENT_ID must be set + var useObo = true; + string? managedIdentityClientId = null; + + Assert.ThrowsExactly(() => + { + if (useObo && string.IsNullOrEmpty(managedIdentityClientId)) + { + throw new InvalidOperationException( + "OBO mode requires MANAGED_IDENTITY_CLIENT_ID to be set for the FIC assertion."); + } + }); + } + + [TestMethod] + public void OboDoesNotThrowWhenManagedIdentityClientIdSet() + { + var useObo = true; + var managedIdentityClientId = "test-mi-id"; + + // Should not throw + if (useObo && string.IsNullOrEmpty(managedIdentityClientId)) + { + throw new InvalidOperationException("Should not reach here"); + } + // If we get here, test passes + } + + [TestMethod] + public void AgentVersion_ParsedWhenSet() + { + string? configValue = "2"; + + var agentVersion = string.IsNullOrWhiteSpace(configValue) ? null : configValue; + + Assert.AreEqual("2", agentVersion); + } + + [TestMethod] + public void AgentVersion_NullWhenMissing() + { + string? configValue = null; + + var agentVersion = string.IsNullOrWhiteSpace(configValue) ? null : configValue; + + Assert.IsNull(agentVersion); + } + + [TestMethod] + public void AgentVersion_NullWhenEmpty() + { + string? configValue = ""; + + var agentVersion = string.IsNullOrWhiteSpace(configValue) ? null : configValue; + + Assert.IsNull(agentVersion); + } + + [TestMethod] + public void AgentVersion_NullWhenWhitespace() + { + string? configValue = " "; + + var agentVersion = string.IsNullOrWhiteSpace(configValue) ? null : configValue; + + Assert.IsNull(agentVersion); + } + + [TestMethod] + public void PortalAgentId_SplitsNameAndVersion() + { + // Portal format: "dadjokes:2" + var portalAgentId = "dadjokes:2"; + var parts = portalAgentId.Split(':', 2); + + var agentName = parts[0].Trim(); + var agentVersion = parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]) + ? parts[1].Trim() : null; + + Assert.AreEqual("dadjokes", agentName); + Assert.AreEqual("2", agentVersion); + } + + [TestMethod] + public void PortalAgentId_HandlesNoVersion() + { + // No version suffix + var portalAgentId = "dadjokes"; + var parts = portalAgentId.Split(':', 2); + + var agentName = parts[0].Trim(); + var agentVersion = parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]) + ? parts[1].Trim() : null; + + Assert.AreEqual("dadjokes", agentName); + Assert.IsNull(agentVersion); + } + + [TestMethod] + public void PortalAgentId_HandlesWhitespaceVersion() + { + // Whitespace-only version suffix + var portalAgentId = "dadjokes: "; + var parts = portalAgentId.Split(':', 2); + + var agentName = parts[0].Trim(); + var agentVersion = parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]) + ? parts[1].Trim() : null; + + Assert.AreEqual("dadjokes", agentName); + Assert.IsNull(agentVersion); + } + + [TestMethod] + public void PortalResourceId_ExtractsResourceName() + { + var armPath = "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.CognitiveServices/accounts/my-foundry-resource"; + + var resourceName = armPath.Split("/accounts/").Last().Split('/').First().Trim(); + + Assert.AreEqual("my-foundry-resource", resourceName); + } + + [TestMethod] + public void PortalResourceId_HandlesProjectSuffix() + { + // ARM path with project suffix (AZURE_EXISTING_AIPROJECT_RESOURCE_ID format) + var armPath = "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.CognitiveServices/accounts/my-foundry/projects/my-project"; + + var resourceName = armPath.Split("/accounts/").Last().Split('/').First().Trim(); + + Assert.AreEqual("my-foundry", resourceName); + } +} diff --git a/backend/WebApp.Api.Tests/ConversationsEndpointTests.cs b/backend/WebApp.Api.Tests/ConversationsEndpointTests.cs new file mode 100644 index 0000000..79f22eb --- /dev/null +++ b/backend/WebApp.Api.Tests/ConversationsEndpointTests.cs @@ -0,0 +1,79 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace WebApp.Api.Tests; + +[TestClass] +public class ConversationsEndpointTests +{ + [TestMethod] + public void LimitClamping_DefaultIs20() + { + // Test: Math.Clamp(null ?? 20, 1, 100) == 20 + int? limit = null; + var pageSize = Math.Clamp(limit ?? 20, 1, 100); + Assert.AreEqual(20, pageSize); + } + + [TestMethod] + public void LimitClamping_MinIs1() + { + int? limit = 0; + var pageSize = Math.Clamp(limit ?? 20, 1, 100); + Assert.AreEqual(1, pageSize); + } + + [TestMethod] + public void LimitClamping_MaxIs100() + { + int? limit = 500; + var pageSize = Math.Clamp(limit ?? 20, 1, 100); + Assert.AreEqual(100, pageSize); + } + + [TestMethod] + public void LimitClamping_NegativeClampedTo1() + { + int? limit = -5; + var pageSize = Math.Clamp(limit ?? 20, 1, 100); + Assert.AreEqual(1, pageSize); + } + + [TestMethod] + public void HasMore_TrueWhenResultsExceedLimit() + { + var conversations = Enumerable.Range(0, 21).Select(i => $"conv-{i}").ToList(); + var pageSize = 20; + var hasMore = conversations.Count > pageSize; + Assert.IsTrue(hasMore); + } + + [TestMethod] + public void HasMore_FalseWhenResultsEqualLimit() + { + var conversations = Enumerable.Range(0, 20).Select(i => $"conv-{i}").ToList(); + var pageSize = 20; + var hasMore = conversations.Count > pageSize; + Assert.IsFalse(hasMore); + } + + [TestMethod] + public void HasMore_FalseWhenResultsBelowLimit() + { + var conversations = Enumerable.Range(0, 5).Select(i => $"conv-{i}").ToList(); + var pageSize = 20; + var hasMore = conversations.Count > pageSize; + Assert.IsFalse(hasMore); + } + + [TestMethod] + public void HasMore_TruncatesListToPageSize() + { + var conversations = Enumerable.Range(0, 25).Select(i => $"conv-{i}").ToList(); + var pageSize = 20; + var hasMore = conversations.Count > pageSize; + if (hasMore) + conversations = conversations.Take(pageSize).ToList(); + Assert.AreEqual(20, conversations.Count); + Assert.IsTrue(hasMore); + } +} diff --git a/backend/WebApp.Api.Tests/ErrorResponseFactoryTests.cs b/backend/WebApp.Api.Tests/ErrorResponseFactoryTests.cs new file mode 100644 index 0000000..aec0d6e --- /dev/null +++ b/backend/WebApp.Api.Tests/ErrorResponseFactoryTests.cs @@ -0,0 +1,157 @@ +using WebApp.Api.Models; + +namespace WebApp.Api.Tests; + +[TestClass] +public class ErrorResponseFactoryTests +{ + [TestMethod] + [DataRow(400, "Invalid Request")] + [DataRow(401, "Session Expired")] + [DataRow(403, "Access Denied")] + [DataRow(404, "Not Found")] + [DataRow(429, "Too Many Requests")] + [DataRow(500, "Service Temporarily Unavailable")] + [DataRow(503, "Service Unavailable")] + public void CreateFromException_MapsStatusCodeToTitle(int statusCode, string expectedTitle) + { + // Arrange + var exception = new Exception("Test error"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, statusCode, isDevelopment: false); + + // Assert + Assert.AreEqual(expectedTitle, response.Title); + Assert.AreEqual(statusCode, response.Status); + } + + [TestMethod] + [DataRow(400, "The request contains invalid data. Please check your input and try again.")] + [DataRow(401, "Your session has expired. Please sign in again to continue.")] + [DataRow(403, "You don't have permission to perform this action.")] + [DataRow(404, "The requested resource was not found.")] + [DataRow(429, "You've made too many requests. Please wait a moment and try again.")] + [DataRow(503, "The service is temporarily unavailable. Please try again in a few moments.")] + public void CreateFromException_MapsStatusCodeToDetail(int statusCode, string expectedDetail) + { + // Arrange + var exception = new Exception("Internal error message"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, statusCode, isDevelopment: false); + + // Assert + Assert.AreEqual(expectedDetail, response.Detail); + } + + [TestMethod] + public void CreateFromException_InDevelopment_IncludesExceptionDetails() + { + // Arrange + var exception = new InvalidOperationException("Detailed error message"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, 500, isDevelopment: true); + + // Assert + Assert.AreEqual("Detailed error message", response.Detail); + Assert.IsNotNull(response.Extensions); + Assert.AreEqual("InvalidOperationException", response.Extensions["exceptionType"]); + Assert.IsTrue(response.Extensions.ContainsKey("stackTrace")); + } + + [TestMethod] + public void CreateFromException_InProduction_HidesExceptionDetails() + { + // Arrange + var exception = new InvalidOperationException("Sensitive internal error"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, 500, isDevelopment: false); + + // Assert + Assert.IsFalse(response.Detail?.Contains("Sensitive") ?? false); + Assert.IsNull(response.Extensions); + } + + [TestMethod] + public void CreateFromException_UnknownStatusCode_ReturnsGenericMessage() + { + // Arrange + var exception = new Exception("Test error"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, 418, isDevelopment: false); + + // Assert + Assert.AreEqual("An Error Occurred", response.Title); + } + + [TestMethod] + public void CreateFromException_500InProduction_ReturnsGenericDetail() + { + // Arrange + var exception = new Exception("Database connection failed"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, 500, isDevelopment: false); + + // Assert + Assert.AreEqual("An unexpected error occurred. Our team has been notified. Please try again later.", response.Detail); + } + + [TestMethod] + public void CreateFromException_UnknownStatusCode_ReturnsGenericDetail() + { + // Arrange + var exception = new Exception("Weird error"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, 418, isDevelopment: false); + + // Assert + Assert.AreEqual("An unexpected error occurred. Please try again.", response.Detail); + } + + [TestMethod] + public void CreateFromException_UnknownStatusCodeInDevelopment_ShowsExceptionMessage() + { + // Arrange + var exception = new Exception("Development debug message"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, 418, isDevelopment: true); + + // Assert + Assert.AreEqual("Development debug message", response.Detail); + Assert.IsNotNull(response.Extensions); + } + + [TestMethod] + public void CreateFromException_NullStackTrace_ReturnsNA() + { + // Arrange - Exception without stack trace (not thrown) + var exception = new Exception("Test"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, 500, isDevelopment: true); + + // Assert + Assert.IsNotNull(response.Extensions); + Assert.AreEqual("N/A", response.Extensions["stackTrace"]); + } + + [TestMethod] + public void CreateFromException_SetsTypeToAboutBlank() + { + // Arrange + var exception = new Exception("Test"); + + // Act + var response = ErrorResponseFactory.CreateFromException(exception, 400, isDevelopment: false); + + // Assert + Assert.AreEqual("about:blank", response.Type); + } +} diff --git a/backend/WebApp.Api.Tests/UploadedFilesPrefixTests.cs b/backend/WebApp.Api.Tests/UploadedFilesPrefixTests.cs new file mode 100644 index 0000000..a1d1f6d --- /dev/null +++ b/backend/WebApp.Api.Tests/UploadedFilesPrefixTests.cs @@ -0,0 +1,46 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using WebApp.Api.Services; + +namespace WebApp.Api.Tests; + +/// +/// Verifies the filename-prefix convention used by the uploaded-files cleanup feature. +/// The cleanup endpoint scopes deletes by matching this prefix on the stored filename; +/// any regression in the constant or the matcher would cause the UI to either miss our +/// files or delete unrelated files in the shared Foundry project. +/// +[TestClass] +public class UploadedFilesPrefixTests +{ + [TestMethod] + public void Prefix_IsStableValue() + { + // Locked by contract: changing this value would orphan every file already uploaded + // by a previous build, because the cleanup matcher would no longer recognize them. + Assert.AreEqual("webapp-upload-", AgentFrameworkService.WebAppUploadFilenamePrefix); + } + + [TestMethod] + public void PrefixMatch_AcceptsWebAppUpload() + { + var name = $"{AgentFrameworkService.WebAppUploadFilenamePrefix}abc123.png"; + Assert.IsTrue(name.StartsWith(AgentFrameworkService.WebAppUploadFilenamePrefix, StringComparison.Ordinal)); + } + + [TestMethod] + public void PrefixMatch_RejectsForeignFile() + { + // A file uploaded by some other tool/user in the shared Foundry project must not match. + var name = "dataset-2025-training.png"; + Assert.IsFalse(name.StartsWith(AgentFrameworkService.WebAppUploadFilenamePrefix, StringComparison.Ordinal)); + } + + [TestMethod] + public void PrefixMatch_RejectsLegacyName() + { + // Pre-cleanup builds used "image-{guid}" — legacy uploads are intentionally left alone + // because we cannot prove they were ours. + var name = "image-abc123.png"; + Assert.IsFalse(name.StartsWith(AgentFrameworkService.WebAppUploadFilenamePrefix, StringComparison.Ordinal)); + } +} diff --git a/backend/WebApp.Api.Tests/WebApp.Api.Tests.csproj b/backend/WebApp.Api.Tests/WebApp.Api.Tests.csproj new file mode 100644 index 0000000..ba72601 --- /dev/null +++ b/backend/WebApp.Api.Tests/WebApp.Api.Tests.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/backend/WebApp.Api/Models/AgentMetadata.cs b/backend/WebApp.Api/Models/AgentMetadata.cs index 608ca8f..01839d7 100644 --- a/backend/WebApp.Api/Models/AgentMetadata.cs +++ b/backend/WebApp.Api/Models/AgentMetadata.cs @@ -13,4 +13,11 @@ public record AgentMetadataResponse public required string Model { get; init; } public string? Instructions { get; init; } public Dictionary? Metadata { get; init; } + + /// + /// Starter prompts to display as suggestions in the chat interface. + /// Populated from agent metadata key "starterPrompts" (newline-separated text). + /// Configure in Microsoft Foundry portal under agent Configuration > Starter prompts. + /// + public List? StarterPrompts { get; init; } } diff --git a/backend/WebApp.Api/Models/AnnotationInfo.cs b/backend/WebApp.Api/Models/AnnotationInfo.cs new file mode 100644 index 0000000..06a56c8 --- /dev/null +++ b/backend/WebApp.Api/Models/AnnotationInfo.cs @@ -0,0 +1,58 @@ +namespace WebApp.Api.Models; + +/// +/// Represents a citation annotation from AI agent responses. +/// Supports all Azure AI Agent SDK annotation types: +/// - uri_citation: Bing, Azure AI Search, SharePoint +/// - file_citation: File search from vector stores +/// - file_path: Code interpreter generated files +/// - container_file_citation: Container file citations +/// +public record AnnotationInfo +{ + /// + /// The type of annotation: "uri_citation", "file_citation", "file_path", or "container_file_citation". + /// + public required string Type { get; init; } + + /// + /// Display label for the citation (title or filename). + /// + public required string Label { get; init; } + + /// + /// URL for URI citations (null for file citations). + /// + public string? Url { get; init; } + + /// + /// File ID for file citations (null for URI citations). + /// + public string? FileId { get; init; } + + /// + /// Container ID for container file citations (code interpreter outputs). + /// Required together with FileId to download container files. + /// + public string? ContainerId { get; init; } + + /// + /// The placeholder text in the response to replace (e.g., "【4:0†source】"). + /// + public string? TextToReplace { get; init; } + + /// + /// Start index in the text where the citation applies. + /// + public int? StartIndex { get; init; } + + /// + /// End index in the text where the citation applies. + /// + public int? EndIndex { get; init; } + + /// + /// Quote from the source document (for file citations). + /// + public string? Quote { get; init; } +} diff --git a/backend/WebApp.Api/Models/ChatRequest.cs b/backend/WebApp.Api/Models/ChatRequest.cs index 0391bfd..4df3c6e 100644 --- a/backend/WebApp.Api/Models/ChatRequest.cs +++ b/backend/WebApp.Api/Models/ChatRequest.cs @@ -9,4 +9,45 @@ public record ChatRequest /// Images are sent inline with the message, no file upload needed. /// public List? ImageDataUris { get; init; } + /// + /// File attachments with metadata (filename, MIME type, base64 data). + /// Supports documents like PDF, DOCX, TXT, etc. + /// + public List? FileDataUris { get; init; } + /// + /// MCP tool approval response (for resuming after approval request). + /// + public McpApprovalResponse? McpApproval { get; init; } + /// + /// Response ID to continue from (for MCP approval flow). + /// + public string? PreviousResponseId { get; init; } +} + +/// +/// Represents a user's approval/rejection decision for an MCP tool call. +/// +public record McpApprovalResponse +{ + public required string ApprovalRequestId { get; init; } + public required bool Approved { get; init; } +} + +/// +/// Represents a file attachment with metadata for document upload. +/// +public record FileAttachment +{ + /// + /// Base64 data URI (e.g., data:application/pdf;base64,...) + /// + public required string DataUri { get; init; } + /// + /// Original filename with extension + /// + public required string FileName { get; init; } + /// + /// MIME type (e.g., application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document) + /// + public required string MimeType { get; init; } } diff --git a/backend/WebApp.Api/Models/ConversationModels.cs b/backend/WebApp.Api/Models/ConversationModels.cs index 59f9447..53fa26a 100644 --- a/backend/WebApp.Api/Models/ConversationModels.cs +++ b/backend/WebApp.Api/Models/ConversationModels.cs @@ -31,3 +31,16 @@ public record FileAttachmentInfo( string FileName, long FileSizeBytes ); + +public record ConversationSummary +{ + public required string Id { get; init; } + public string? Title { get; init; } + public long CreatedAt { get; init; } +} + +public record ConversationMessageInfo +{ + public required string Role { get; init; } + public required string Content { get; init; } +} diff --git a/backend/WebApp.Api/Models/StreamChunk.cs b/backend/WebApp.Api/Models/StreamChunk.cs new file mode 100644 index 0000000..e8dd00b --- /dev/null +++ b/backend/WebApp.Api/Models/StreamChunk.cs @@ -0,0 +1,80 @@ +namespace WebApp.Api.Models; + +/// +/// Represents a chunk of streaming response data. +/// Can contain text content, annotations (citations), or MCP tool approval requests. +/// +public record StreamChunk +{ + /// + /// Text content chunk (delta). Null if this chunk contains annotations or approval request. + /// + public string? TextDelta { get; init; } + + /// + /// Annotations/citations extracted from the response. Null if this chunk contains text or approval request. + /// + public List? Annotations { get; init; } + + /// + /// MCP tool approval request. Null if this chunk contains text or annotations. + /// + public McpApprovalRequest? McpApprovalRequest { get; init; } + + /// + /// Whether this chunk signals a tool-use step (e.g. file_search, code_interpreter). + /// + public bool IsToolUse { get; init; } + + /// + /// Name of the tool being invoked (set when IsToolUse is true). + /// + public string? ToolName { get; init; } + + /// + /// Creates a text delta chunk. + /// + public static StreamChunk Text(string delta) => new() { TextDelta = delta }; + + /// + /// Creates an annotations chunk. + /// + public static StreamChunk WithAnnotations(List annotations) => new() { Annotations = annotations }; + + /// + /// Creates an MCP approval request chunk. + /// + public static StreamChunk McpApproval(McpApprovalRequest request) => new() { McpApprovalRequest = request }; + + /// + /// Creates a tool-use indicator chunk. + /// + public static StreamChunk ToolUse(string toolName) => new() { IsToolUse = true, ToolName = toolName }; + + /// + /// Whether this chunk contains text content. + /// + public bool IsText => TextDelta != null; + + /// + /// Whether this chunk contains annotations. + /// + public bool HasAnnotations => Annotations != null && Annotations.Count > 0; + + /// + /// Whether this chunk contains an MCP approval request. + /// + public bool IsMcpApprovalRequest => McpApprovalRequest != null; +} + +/// +/// Represents an MCP tool call requiring user approval. +/// +public record McpApprovalRequest +{ + public required string Id { get; init; } + public required string ToolName { get; init; } + public required string ServerLabel { get; init; } + public string? Arguments { get; init; } + public string? PreviousResponseId { get; init; } +} diff --git a/backend/WebApp.Api/Models/UploadedFilesModels.cs b/backend/WebApp.Api/Models/UploadedFilesModels.cs new file mode 100644 index 0000000..d88a9ea --- /dev/null +++ b/backend/WebApp.Api/Models/UploadedFilesModels.cs @@ -0,0 +1,12 @@ +namespace WebApp.Api.Models; + +/// +/// Summary of files previously uploaded by this web app for image attachments. +/// Only files whose name begins with the web-app upload prefix are counted. +/// +public record UploadedFilesInfo(int Count, long TotalBytes); + +/// +/// Result of a cleanup operation that deletes all web-app uploaded files. +/// +public record UploadedFilesCleanupResult(int Deleted, int Failed); diff --git a/backend/WebApp.Api/Program.cs b/backend/WebApp.Api/Program.cs index 487e950..a3030da 100644 --- a/backend/WebApp.Api/Program.cs +++ b/backend/WebApp.Api/Program.cs @@ -37,6 +37,9 @@ // Add ProblemDetails service for standardized RFC 7807 error responses builder.Services.AddProblemDetails(); +// Register IHttpContextAccessor for services that need access to the current HTTP request +builder.Services.AddHttpContextAccessor(); + // Configure CORS for local development and production var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? new[] { "http://localhost:8080" }; @@ -100,12 +103,16 @@ { builder.Configuration.Bind("AzureAd", options); var configuredClientId = builder.Configuration["AzureAd:ClientId"]; + var backendClientId = builder.Configuration["ENTRA_BACKEND_CLIENT_ID"]; - options.TokenValidationParameters.ValidAudiences = new[] + // When OBO is enabled, tokens are scoped to the backend API app + var audiences = new List { configuredClientId!, $"api://{configuredClientId}" }; + if (!string.IsNullOrEmpty(backendClientId)) { - configuredClientId, - $"api://{configuredClientId}" - }; + audiences.Add(backendClientId); + audiences.Add($"api://{backendClientId}"); + } + options.TokenValidationParameters.ValidAudiences = audiences; options.TokenValidationParameters.NameClaimType = ClaimTypes.Name; options.TokenValidationParameters.RoleClaimType = ClaimTypes.Role; @@ -121,10 +128,10 @@ }); }); -// Register Azure AI Agent Service as scoped -// Scoped is preferred for services making external API calls to ensure proper disposal -// and avoid potential issues with long-lived connections -builder.Services.AddScoped(); +// Register Foundry Agent Service (v2 Agents API) +// Uses Azure.AI.Projects SDK which works with v2 Agents API (/agents/ endpoint with human-readable IDs). +builder.Services.AddHttpClient(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -153,27 +160,15 @@ app.UseAuthentication(); app.UseAuthorization(); -// Authenticated health endpoint exposes caller identity -app.MapGet("/api/health", (HttpContext context) => -{ - var userId = context.User.FindFirst("oid")?.Value ?? "unknown"; - var userName = context.User.FindFirst("name")?.Value ?? "unknown"; - - return Results.Ok(new - { - status = "healthy", - timestamp = DateTime.UtcNow, - authenticated = true, - user = new { id = userId, name = userName } - }); -}) -.RequireAuthorization(ScopePolicyName) +// Unauthenticated health endpoint for container probes +app.MapGet("/api/health", () => Results.Ok(new { status = "healthy" })) .WithName("GetHealth"); // Streaming Chat endpoint: Streams agent response via SSE (conversationId → chunks → usage → done) +// Supports MCP tool approval flow with previousResponseId and mcpApproval parameters app.MapPost("/api/chat/stream", async ( ChatRequest request, - AzureAIAgentService agentService, + AgentFrameworkService agentService, HttpContext httpContext, IHostEnvironment environment, CancellationToken cancellationToken) => @@ -195,30 +190,44 @@ conversationId, request.Message, request.ImageDataUris, + request.FileDataUris, + request.PreviousResponseId, + request.McpApproval, cancellationToken)) { - await WriteChunkEvent(httpContext.Response, chunk, cancellationToken); + if (chunk.IsText && chunk.TextDelta != null) + { + await WriteChunkEvent(httpContext.Response, chunk.TextDelta, cancellationToken); + } + else if (chunk.HasAnnotations && chunk.Annotations != null) + { + await WriteAnnotationsEvent(httpContext.Response, chunk.Annotations, cancellationToken); + } + else if (chunk.IsMcpApprovalRequest && chunk.McpApprovalRequest != null) + { + await WriteMcpApprovalRequestEvent(httpContext.Response, chunk.McpApprovalRequest, cancellationToken); + } + else if (chunk.IsToolUse && chunk.ToolName != null) + { + await WriteToolUseEvent(httpContext.Response, chunk.ToolName, cancellationToken); + } } var duration = (DateTime.UtcNow - startTime).TotalMilliseconds; - var usage = await agentService.GetLastRunUsageAsync(cancellationToken); - - if (usage != null) - { - await WriteUsageEvent(httpContext.Response, new - { - duration, - promptTokens = usage.PromptTokens, - completionTokens = usage.CompletionTokens, - totalTokens = usage.TotalTokens - }, cancellationToken); - } + var usage = agentService.GetLastUsage(); + await WriteUsageEvent( + httpContext.Response, + duration, + usage?.InputTokens ?? 0, + usage?.OutputTokens ?? 0, + usage?.TotalTokens ?? 0, + cancellationToken); await WriteDoneEvent(httpContext.Response, cancellationToken); } - catch (ArgumentException ex) when (ex.Message.Contains("Invalid image attachments")) + catch (ArgumentException ex) when (ex.Message.Contains("Invalid") && (ex.Message.Contains("attachments") || ex.Message.Contains("image") || ex.Message.Contains("file"))) { - // Validation errors from image processing - return 400 Bad Request + // Validation errors from image/file processing - return 400 Bad Request var errorResponse = ErrorResponseFactory.CreateFromException( ex, 400, @@ -231,6 +240,9 @@ await WriteErrorEvent( } catch (Exception ex) { + var logger = httpContext.RequestServices.GetRequiredService>(); + logger.LogError(ex, "Chat stream error: {Message}", ex.Message); + var errorResponse = ErrorResponseFactory.CreateFromException( ex, 500, @@ -247,9 +259,8 @@ await WriteErrorEvent( static async Task WriteConversationIdEvent(HttpResponse response, string conversationId, CancellationToken ct) { - await response.WriteAsync( - $"data: {{\"type\":\"conversationId\",\"conversationId\":\"{conversationId}\"}}\n\n", - ct); + var json = System.Text.Json.JsonSerializer.Serialize(new { type = "conversationId", conversationId }); + await response.WriteAsync($"data: {json}\n\n", ct); await response.Body.FlushAsync(ct); } @@ -260,15 +271,62 @@ static async Task WriteChunkEvent(HttpResponse response, string content, Cancell await response.Body.FlushAsync(ct); } -static async Task WriteUsageEvent(HttpResponse response, object usageData, CancellationToken ct) +static async Task WriteToolUseEvent(HttpResponse response, string toolName, CancellationToken ct) +{ + var json = System.Text.Json.JsonSerializer.Serialize(new { type = "toolUse", toolName }); + await response.WriteAsync($"data: {json}\n\n", ct); + await response.Body.FlushAsync(ct); +} + +static async Task WriteAnnotationsEvent(HttpResponse response, List annotations, CancellationToken ct) +{ + var json = System.Text.Json.JsonSerializer.Serialize(new + { + type = "annotations", + annotations = annotations.Select(a => new + { + type = a.Type, + label = a.Label, + url = a.Url, + fileId = a.FileId, + containerId = a.ContainerId, + textToReplace = a.TextToReplace, + startIndex = a.StartIndex, + endIndex = a.EndIndex, + quote = a.Quote + }) + }); + await response.WriteAsync($"data: {json}\n\n", ct); + await response.Body.FlushAsync(ct); +} + +static async Task WriteMcpApprovalRequestEvent(HttpResponse response, WebApp.Api.Models.McpApprovalRequest approval, CancellationToken ct) +{ + var json = System.Text.Json.JsonSerializer.Serialize(new + { + type = "mcpApprovalRequest", + approvalRequest = new + { + id = approval.Id, + toolName = approval.ToolName, + serverLabel = approval.ServerLabel, + arguments = approval.Arguments, + previousResponseId = approval.PreviousResponseId + } + }); + await response.WriteAsync($"data: {json}\n\n", ct); + await response.Body.FlushAsync(ct); +} + +static async Task WriteUsageEvent(HttpResponse response, double duration, int promptTokens, int completionTokens, int totalTokens, CancellationToken ct) { var json = System.Text.Json.JsonSerializer.Serialize(new { type = "usage", - duration = ((dynamic)usageData).duration, - promptTokens = ((dynamic)usageData).promptTokens, - completionTokens = ((dynamic)usageData).completionTokens, - totalTokens = ((dynamic)usageData).totalTokens + duration, + promptTokens, + completionTokens, + totalTokens }); await response.WriteAsync($"data: {json}\n\n", ct); await response.Body.FlushAsync(ct); @@ -290,7 +348,7 @@ static async Task WriteErrorEvent(HttpResponse response, string message, Cancell // Get agent metadata (name, description, model, metadata) // Used by frontend to display agent information in the UI app.MapGet("/api/agent", async ( - AzureAIAgentService agentService, + AgentFrameworkService agentService, IHostEnvironment environment, CancellationToken cancellationToken) => { @@ -319,7 +377,7 @@ static async Task WriteErrorEvent(HttpResponse response, string message, Cancell // Get agent info (for debugging) app.MapGet("/api/agent/info", async ( - AzureAIAgentService agentService, + AgentFrameworkService agentService, IHostEnvironment environment, CancellationToken cancellationToken) => { @@ -350,7 +408,214 @@ static async Task WriteErrorEvent(HttpResponse response, string message, Cancell .RequireAuthorization(ScopePolicyName) .WithName("GetAgentInfo"); +// List conversations +app.MapGet("/api/conversations", async ( + AgentFrameworkService agentService, + IHostEnvironment environment, + int? limit, + CancellationToken cancellationToken) => +{ + // MI mode: conversations are agent-scoped, not user-scoped. + // All authenticated users see all conversations for this agent. + // This is by-design — OBO mode (ENTRA_BACKEND_CLIENT_ID set) scopes per-user. + try + { + var pageSize = Math.Clamp(limit ?? 20, 1, 100); + var conversations = await agentService.ListConversationsAsync(pageSize, cancellationToken); + var hasMore = conversations.Count > pageSize; + if (hasMore) + conversations = conversations.Take(pageSize).ToList(); + return Results.Ok(new { conversations, hasMore }); + } + catch (Exception ex) + { + var errorResponse = ErrorResponseFactory.CreateFromException(ex, 500, environment.IsDevelopment()); + return Results.Problem( + title: errorResponse.Title, + detail: errorResponse.Detail, + statusCode: errorResponse.Status, + extensions: errorResponse.Extensions + ); + } +}) +.RequireAuthorization(ScopePolicyName) +.WithName("ListConversations"); + +// Get conversation messages +app.MapGet("/api/conversations/{conversationId}/messages", async ( + string conversationId, + AgentFrameworkService agentService, + IHostEnvironment environment, + CancellationToken cancellationToken) => +{ + try + { + var messages = await agentService.GetConversationMessagesAsync(conversationId, cancellationToken); + return Results.Ok(messages); + } + catch (Exception ex) + { + var errorResponse = ErrorResponseFactory.CreateFromException(ex, 500, environment.IsDevelopment()); + return Results.Problem( + title: errorResponse.Title, + detail: errorResponse.Detail, + statusCode: errorResponse.Status, + extensions: errorResponse.Extensions + ); + } +}) +.RequireAuthorization(ScopePolicyName) +.WithName("GetConversationMessages"); + +// Delete conversation +app.MapDelete("/api/conversations/{conversationId}", async ( + string conversationId, + AgentFrameworkService agentService, + IHostEnvironment environment, + CancellationToken cancellationToken) => +{ + try + { + await agentService.DeleteConversationAsync(conversationId, cancellationToken); + return Results.NoContent(); + } + catch (NotSupportedException) + { + return Results.Problem( + title: "Not Implemented", + detail: "Conversation deletion is not yet supported by the Azure.AI.Projects SDK.", + statusCode: 501 + ); + } + catch (Exception ex) + { + var errorResponse = ErrorResponseFactory.CreateFromException(ex, 500, environment.IsDevelopment()); + return Results.Problem( + title: errorResponse.Title, + detail: errorResponse.Detail, + statusCode: errorResponse.Status, + extensions: errorResponse.Extensions + ); + } +}) +.RequireAuthorization(ScopePolicyName) +.WithName("DeleteConversation"); + +// File download endpoint for code interpreter outputs +app.MapGet("/api/files/{fileId}", async ( + string fileId, + string? containerId, + AgentFrameworkService agentService, + IHostEnvironment environment, + CancellationToken cancellationToken) => +{ + try + { + var (content, fileName) = await agentService.DownloadFileAsync(fileId, containerId, cancellationToken); + var contentType = GetMimeType(fileName); + return Results.File(content.ToArray(), contentType, fileName); + } + catch (HttpRequestException httpEx) + { + var statusCode = (int?)httpEx.StatusCode ?? 502; + var errorResponse = ErrorResponseFactory.CreateFromException(httpEx, statusCode, environment.IsDevelopment()); + return Results.Problem( + title: errorResponse.Title, + detail: errorResponse.Detail, + statusCode: errorResponse.Status, + extensions: errorResponse.Extensions + ); + } + catch (Exception ex) + { + var errorResponse = ErrorResponseFactory.CreateFromException(ex, 500, environment.IsDevelopment()); + return Results.Problem( + title: errorResponse.Title, + detail: errorResponse.Detail, + statusCode: errorResponse.Status, + extensions: errorResponse.Extensions + ); + } +}) +.RequireAuthorization(ScopePolicyName) +.WithName("DownloadFile"); + +// Uploaded-files cleanup endpoints — inspect & delete image files previously uploaded by +// this web app. Uses the WebAppUploadFilenamePrefix tag applied on upload to scope the +// operation to our own files, because the Foundry Files API does not expose a typed +// expires_after parameter in the GA SDK (see README "Known limitations"). +app.MapGet("/api/files/uploaded", async ( + AgentFrameworkService agentService, + IHostEnvironment environment, + CancellationToken cancellationToken) => +{ + try + { + var info = await agentService.ListUploadedFilesAsync(cancellationToken); + return Results.Ok(info); + } + catch (Exception ex) + { + var errorResponse = ErrorResponseFactory.CreateFromException(ex, 500, environment.IsDevelopment()); + return Results.Problem( + title: errorResponse.Title, + detail: errorResponse.Detail, + statusCode: errorResponse.Status, + extensions: errorResponse.Extensions + ); + } +}) +.RequireAuthorization(ScopePolicyName) +.WithName("ListUploadedFiles"); + +app.MapPost("/api/files/cleanup", async ( + AgentFrameworkService agentService, + IHostEnvironment environment, + CancellationToken cancellationToken) => +{ + try + { + var result = await agentService.CleanupUploadedFilesAsync(cancellationToken); + return Results.Ok(result); + } + catch (Exception ex) + { + var errorResponse = ErrorResponseFactory.CreateFromException(ex, 500, environment.IsDevelopment()); + return Results.Problem( + title: errorResponse.Title, + detail: errorResponse.Detail, + statusCode: errorResponse.Status, + extensions: errorResponse.Extensions + ); + } +}) +.RequireAuthorization(ScopePolicyName) +.WithName("CleanupUploadedFiles"); + // Fallback route for SPA - serve index.html for any non-API routes app.MapFallbackToFile("index.html"); app.Run(); + +// Helper to determine MIME type from file extension +static string GetMimeType(string fileName) +{ + var ext = Path.GetExtension(fileName).ToLowerInvariant(); + return ext switch + { + ".png" => "image/png", + ".jpg" or ".jpeg" => "image/jpeg", + ".gif" => "image/gif", + ".webp" => "image/webp", + ".svg" => "image/svg+xml", + ".pdf" => "application/pdf", + ".csv" => "text/csv", + ".json" => "application/json", + ".txt" => "text/plain", + ".md" => "text/markdown", + ".html" => "text/html", + ".py" => "text/x-python", + ".js" => "text/javascript", + _ => "application/octet-stream", + }; +} diff --git a/backend/WebApp.Api/Services/AgentFrameworkService.cs b/backend/WebApp.Api/Services/AgentFrameworkService.cs new file mode 100644 index 0000000..34f010a --- /dev/null +++ b/backend/WebApp.Api/Services/AgentFrameworkService.cs @@ -0,0 +1,1214 @@ +using Azure.AI.Projects; +using Azure.AI.Projects.Agents; +using Azure.AI.Extensions.OpenAI; +using Azure.Core; +using Azure.Identity; +using OpenAI.Files; +using OpenAI.Responses; +using Microsoft.Identity.Client; +using Microsoft.Identity.Web; +using System.Runtime.CompilerServices; +using WebApp.Api.Models; + +namespace WebApp.Api.Services; + +#pragma warning disable OPENAI001 + +/// +/// Foundry Agent Service using v2 Agents API. +/// +/// +/// Uses AIProjectClient directly (Azure.AI.Projects GA): AgentAdministrationClient for agent +/// metadata and ProjectResponsesClient for streaming (required for annotations, MCP approvals). +/// See .github/skills/researching-azure-ai-sdk/SKILL.md for SDK patterns. +/// +public class AgentFrameworkService : IDisposable +{ + private readonly string _agentEndpoint; + private readonly string _agentId; + /// + /// Optional concrete agent version id (e.g. "3") from AI_AGENT_VERSION. + /// When set, the agent is pinned to that immutable version for both metadata + /// () and streaming (AgentReference passed to + /// ProjectResponsesClient). When null, the newest published version is + /// resolved on startup and used consistently. Foundry retains all published + /// versions, so pinning is useful for reproducibility across deployments. + /// + private readonly string? _configuredAgentVersion; + private readonly ILogger _logger; + private readonly IHttpContextAccessor? _httpContextAccessor; + private readonly string? _backendClientId; + private readonly string? _tenantId; + private readonly string? _managedIdentityClientId; + private readonly bool _useObo; + private readonly TokenCredential _fallbackCredential; + + // Agent metadata cache (static - shared across requests) + private static ProjectsAgentVersion? s_cachedAgentVersion; + private static AgentMetadataResponse? s_cachedMetadata; + private static readonly SemaphoreSlim s_agentLock = new(1, 1); + // MI assertion cache (static - user-independent, safe to share across requests) + private static ManagedIdentityClientAssertion? s_miAssertion; + + private readonly IHttpClientFactory _httpClientFactory; + + /// + /// Prefix applied to image files this web app uploads to the Foundry Files API, + /// used by the cleanup endpoint to scope deletes to files owned by this app. + /// + public const string WebAppUploadFilenamePrefix = "webapp-upload-"; + + // Per-request project client + private AIProjectClient? _projectClient; + private bool _disposed = false; + private ResponseTokenUsage? _lastUsage; + + public AgentFrameworkService( + IConfiguration configuration, + ILogger logger, + IHttpClientFactory httpClientFactory, + IHttpContextAccessor? httpContextAccessor = null) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + _httpContextAccessor = httpContextAccessor; + + _agentEndpoint = configuration["AI_AGENT_ENDPOINT"] + ?? throw new InvalidOperationException("AI_AGENT_ENDPOINT is not configured"); + + _agentId = configuration["AI_AGENT_ID"] + ?? throw new InvalidOperationException("AI_AGENT_ID is not configured"); + + _configuredAgentVersion = string.IsNullOrWhiteSpace(configuration["AI_AGENT_VERSION"]) + ? null + : configuration["AI_AGENT_VERSION"]; + + _logger.LogDebug( + "Initializing AgentFrameworkService: endpoint={Endpoint}, agentId={AgentId}, version={Version}", + _agentEndpoint, + _agentId, + _configuredAgentVersion ?? ""); + + _backendClientId = configuration["ENTRA_BACKEND_CLIENT_ID"]; + _tenantId = configuration["ENTRA_TENANT_ID"] ?? configuration["AzureAd:TenantId"]; + // User-assigned MI client ID — used for MI-only mode and as FIC assertion in OBO mode + _managedIdentityClientId = configuration["MANAGED_IDENTITY_CLIENT_ID"] + ?? configuration["OBO_MANAGED_IDENTITY_CLIENT_ID"]; // backward compat + + var environment = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Production"; + + // Determine if OBO is available + _useObo = !string.IsNullOrEmpty(_backendClientId) + && !string.IsNullOrEmpty(_tenantId) + && environment != "Development"; + + // Create credential for non-OBO operations (agent metadata cache, MI-only mode) + if (environment == "Development") + { + _logger.LogInformation("Development: Using ChainedTokenCredential (AzureCli -> AzureDeveloperCli)"); + _fallbackCredential = new ChainedTokenCredential( + new AzureCliCredential(), + new AzureDeveloperCliCredential() + ); + } + else if (!string.IsNullOrEmpty(_managedIdentityClientId)) + { + _logger.LogInformation("Production: Using user-assigned ManagedIdentityCredential: {MiClientId}", _managedIdentityClientId); + _fallbackCredential = new ManagedIdentityCredential(ManagedIdentityId.FromUserAssignedClientId(_managedIdentityClientId)); + } + else + { + _logger.LogInformation("Production: Using ManagedIdentityCredential (system-assigned)"); + _fallbackCredential = new ManagedIdentityCredential(ManagedIdentityId.SystemAssigned); + } + + if (_useObo) + { + if (string.IsNullOrEmpty(_managedIdentityClientId)) + { + throw new InvalidOperationException( + "OBO mode requires MANAGED_IDENTITY_CLIENT_ID to be set for the FIC assertion. " + + "This is the user-assigned managed identity that acts as the federated credential."); + } + _logger.LogInformation("OBO mode enabled: backendClientId={BackendClientId}. All API calls use user-delegated identity.", _backendClientId); + + // Initialize MI assertion eagerly — avoids thread-safety issues with lazy init + // in CreateOboCredential(). Safe here because the constructor runs once per scoped instance. + s_miAssertion ??= new ManagedIdentityClientAssertion(managedIdentityClientId: _managedIdentityClientId); + + // No cached project client in OBO mode — created per-request with user's token + } + else + { + _logger.LogInformation("MI mode: using managed identity for all API calls"); + _projectClient = new AIProjectClient(new Uri(_agentEndpoint), _fallbackCredential); + } + + _logger.LogInformation("AIProjectClient initialized successfully"); + } + + /// + /// Get AIProjectClient — OBO mode creates per-request with user's identity, MI mode uses cached client. + /// + private AIProjectClient GetProjectClient() + { + // MI mode: return cached client + if (!_useObo) + { + _projectClient ??= new AIProjectClient(new Uri(_agentEndpoint), _fallbackCredential); + return _projectClient; + } + + // OBO: create per-request client with user's token (cached for request lifetime) + if (_projectClient is null) + { + var userToken = ExtractBearerToken(); + if (string.IsNullOrEmpty(userToken)) + { + throw new InvalidOperationException( + "OBO mode requires a bearer token but none was found in the request. " + + "Ensure the frontend is sending an Authorization header with a valid token."); + } + + var oboCredential = CreateOboCredential(userToken); + _logger.LogDebug("Created OBO credential for request"); + _projectClient = new AIProjectClient(new Uri(_agentEndpoint), oboCredential); + } + + return _projectClient; + } + + /// + /// Create OBO credential using the user's JWT and managed identity FIC assertion. + /// + private OnBehalfOfCredential CreateOboCredential(string userToken) + { + // s_miAssertion is initialized eagerly in the constructor (OBO branch) + Func> assertionCallback = + async (ct) => await s_miAssertion!.GetSignedAssertionAsync( + new AssertionRequestOptions { CancellationToken = ct }); + + return new OnBehalfOfCredential( + _tenantId!, + _backendClientId!, + assertionCallback, + userToken, + new OnBehalfOfCredentialOptions()); + } + + /// + /// Extract bearer token from the current HTTP request. + /// + private string? ExtractBearerToken() + { + var authHeader = _httpContextAccessor?.HttpContext?.Request.Headers.Authorization.ToString(); + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return null; + + return authHeader["Bearer ".Length..].Trim(); + } + + /// + /// Load the agent version metadata via AgentAdministrationClient (v2 Agents API). + /// When is set, fetches that specific version by id. + /// When unset, lists versions in descending order and picks the first (= newest). + /// + private async Task GetAgentAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (s_cachedAgentVersion != null) + return s_cachedAgentVersion; + + await s_agentLock.WaitAsync(cancellationToken); + try + { + if (s_cachedAgentVersion != null) + return s_cachedAgentVersion; + + // Use the same credential path as all other operations (MI or OBO) + var client = GetProjectClient(); + + ProjectsAgentVersion? loaded; + if (!string.IsNullOrWhiteSpace(_configuredAgentVersion)) + { + _logger.LogInformation("Loading agent: {AgentId} version={Version}", _agentId, _configuredAgentVersion); + var response = await client.AgentAdministrationClient.GetAgentVersionAsync( + _agentId, + _configuredAgentVersion!, + cancellationToken); + loaded = response.Value; + } + else + { + _logger.LogInformation("Loading agent: {AgentId} version=", _agentId); + loaded = null; + await foreach (var v in client.AgentAdministrationClient.GetAgentVersionsAsync( + agentName: _agentId, + limit: 1, + order: AgentListOrder.Descending, + after: null, + before: null, + cancellationToken: cancellationToken)) + { + loaded = v; + break; + } + + if (loaded is null) + { + throw new InvalidOperationException( + $"Agent '{_agentId}' has no versions. Create at least one version in AI Foundry."); + } + } + + s_cachedAgentVersion = loaded; + + var definition = s_cachedAgentVersion.Definition as DeclarativeAgentDefinition; + + _logger.LogInformation( + "Loaded agent: name={AgentName}, model={Model}, version={Version} (pinned={Pinned})", + s_cachedAgentVersion.Name ?? _agentId, + definition?.Model ?? "unknown", + s_cachedAgentVersion.Version ?? "", + !string.IsNullOrWhiteSpace(_configuredAgentVersion)); + + // Log StructuredInputs at debug level for troubleshooting + if (definition?.StructuredInputs != null && definition.StructuredInputs.Count > 0) + { + _logger.LogDebug("Agent has {Count} StructuredInputs: {Keys}", + definition.StructuredInputs.Count, + string.Join(", ", definition.StructuredInputs.Keys)); + } + + return s_cachedAgentVersion; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load agent: {AgentId}", _agentId); + throw; + } + finally + { + s_agentLock.Release(); + } + } + + /// + /// Streams agent response for a message using ProjectResponsesClient (Responses API). + /// Returns StreamChunk objects containing text deltas, annotations, or MCP approval requests. + /// + /// + /// Uses direct ProjectResponsesClient instead of IChatClient because we need access to: + /// - McpToolCallApprovalRequestItem for MCP approval flows + /// - FileSearchCallResponseItem for file search quotes + /// - MessageResponseItem.OutputTextAnnotations for citations + /// The IChatClient abstraction doesn't expose these specialized response types. + /// + public async IAsyncEnumerable StreamMessageAsync( + string conversationId, + string message, + List? imageDataUris = null, + List? fileDataUris = null, + string? previousResponseId = null, + McpApprovalResponse? mcpApproval = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + _logger.LogInformation( + "Streaming message to conversation: {ConversationId}, ImageCount: {ImageCount}, FileCount: {FileCount}, HasApproval: {HasApproval}", + conversationId, + imageDataUris?.Count ?? 0, + fileDataUris?.Count ?? 0, + mcpApproval != null); + + CreateResponseOptions options = new() { StreamingEnabled = true }; + + // Resolve the concrete agent version up front so streaming and metadata use the same version. + var resolvedAgent = await GetAgentAsync(cancellationToken); + var resolvedVersion = _configuredAgentVersion ?? resolvedAgent.Version; + + // Always bind to conversation — the conversation maintains MCP approval state + ProjectResponsesClient responsesClient + = GetProjectClient().ProjectOpenAIClient.GetProjectResponsesClientForAgent( + new AgentReference(_agentId, resolvedVersion), + conversationId); + + // If continuing from MCP approval, add approval response items + // Don't set PreviousResponseId — the API rejects it with conversation binding, + // and the conversation already tracks the pending MCP state + if (!string.IsNullOrEmpty(previousResponseId) && mcpApproval != null) + { + options.InputItems.Add(ResponseItem.CreateMcpApprovalResponseItem( + mcpApproval.ApprovalRequestId, + mcpApproval.Approved)); + + _logger.LogInformation( + "Resuming with MCP approval: RequestId={RequestId}, Approved={Approved}", + mcpApproval.ApprovalRequestId, + mcpApproval.Approved); + } + else + { + if (string.IsNullOrWhiteSpace(message)) + { + _logger.LogWarning("Attempted to stream empty message to conversation {ConversationId}", conversationId); + throw new ArgumentException("Message cannot be null or whitespace", nameof(message)); + } + + // Build user message with optional images and files + ResponseItem userMessage = await BuildUserMessageAsync(message, imageDataUris, fileDataUris, cancellationToken); + options.InputItems.Add(userMessage); + } + + // Dictionary to collect file search results for quote extraction + var fileSearchQuotes = new Dictionary(); + // Track the current response ID for MCP approval resume flow + string? currentResponseId = null; + + await foreach (StreamingResponseUpdate update + in responsesClient.CreateResponseStreamingAsync( + options: options, + cancellationToken: cancellationToken)) + { + // Capture response ID from created event (needed for MCP approval resume) + if (update is StreamingResponseCreatedUpdate createdUpdate) + { + currentResponseId = createdUpdate.Response.Id; + _logger.LogDebug("Response created: {ResponseId}", currentResponseId); + continue; + } + + if (update is StreamingResponseOutputTextDeltaUpdate deltaUpdate) + { + yield return StreamChunk.Text(deltaUpdate.Delta); + } + else if (update is StreamingResponseOutputItemDoneUpdate itemDoneUpdate) + { + // Check for MCP tool approval request + if (itemDoneUpdate.Item is McpToolCallApprovalRequestItem mcpApprovalItem) + { + _logger.LogInformation( + "MCP tool approval requested: Id={Id}, Tool={Tool}, Server={Server}", + mcpApprovalItem.Id, + mcpApprovalItem.ToolName, + mcpApprovalItem.ServerLabel); + + // Parse tool arguments from BinaryData to string (JSON) + string? argumentsJson = mcpApprovalItem.ToolArguments?.ToString(); + + yield return StreamChunk.McpApproval(new McpApprovalRequest + { + Id = mcpApprovalItem.Id, + ToolName = mcpApprovalItem.ToolName ?? "Unknown tool", + ServerLabel = mcpApprovalItem.ServerLabel ?? "MCP Server", + Arguments = argumentsJson, + PreviousResponseId = currentResponseId + }); + continue; + } + + // Capture file search results for quote extraction + if (itemDoneUpdate.Item is FileSearchCallResponseItem fileSearchItem) + { + foreach (var result in fileSearchItem.Results) + { + if (!string.IsNullOrEmpty(result.FileId) && !string.IsNullOrEmpty(result.Text)) + { + fileSearchQuotes[result.FileId] = result.Text; + _logger.LogDebug( + "Captured file search quote for FileId={FileId}, QuoteLength={Length}", + result.FileId, + result.Text.Length); + } + } + continue; + } + + // Extract annotations/citations from completed output items + var annotations = ExtractAnnotations(itemDoneUpdate.Item, fileSearchQuotes); + if (annotations.Count > 0) + { + _logger.LogInformation("Extracted {Count} annotations from response", annotations.Count); + yield return StreamChunk.WithAnnotations(annotations); + } + } + else if (update is StreamingResponseOutputItemAddedUpdate itemAddedUpdate) + { + // Detect tool-use steps and signal the frontend for progress indicators + string? toolName = itemAddedUpdate.Item switch + { + FileSearchCallResponseItem => "file_search", + CodeInterpreterCallResponseItem => "code_interpreter", + _ when itemAddedUpdate.Item?.GetType().Name.Contains("ToolCall") == true => "function_call", + _ => null + }; + + if (toolName != null) + { + _logger.LogDebug("Tool use detected: {ToolName}", toolName); + yield return StreamChunk.ToolUse(toolName); + } + } + else if (update is StreamingResponseCompletedUpdate completedUpdate) + { + _lastUsage = completedUpdate.Response.Usage; + } + else if (update is StreamingResponseErrorUpdate errorUpdate) + { + _logger.LogError("Stream error: {Error}", errorUpdate.Message); + throw new InvalidOperationException($"Stream error: {errorUpdate.Message}"); + } + else + { + _logger.LogDebug("Unhandled stream update type: {Type}", update.GetType().Name); + } + } + + _logger.LogInformation("Completed streaming for conversation: {ConversationId}", conversationId); + } + + /// + /// Supported image MIME types for vision capabilities. + /// + private static readonly HashSet AllowedImageTypes = + ["image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp"]; + + /// + /// Supported document MIME types for file input. + /// Note: Office documents (docx, pptx, xlsx) are NOT supported - they cannot be parsed. + /// + private static readonly HashSet AllowedDocumentTypes = + [ + "application/pdf", + "text/plain", + "text/markdown", + "text/csv", + "application/json", + "text/html", + "application/xml", + "text/xml" + ]; + + /// + /// Text-based document MIME types that should be inlined as text rather than sent as file input. + /// The Responses API only supports PDF for CreateInputFilePart. + /// + private static readonly HashSet TextBasedDocumentTypes = + [ + "text/plain", + "text/markdown", + "text/csv", + "application/json", + "text/html", + "application/xml", + "text/xml" + ]; + + /// + /// MIME types that can be sent as file input (only PDF is currently supported by Responses API). + /// + private static readonly HashSet FileInputTypes = + [ + "application/pdf" + ]; + + /// + /// Maximum number of images per message. + /// + private const int MaxImageCount = 5; + + /// + /// Maximum number of files per message. + /// + private const int MaxFileCount = 10; + + /// + /// Maximum size per image in bytes (5MB). + /// + private const long MaxImageSizeBytes = 5 * 1024 * 1024; + + /// + /// Maximum size per document file in bytes (20MB). + /// + private const long MaxFileSizeBytes = 20 * 1024 * 1024; + + /// + /// Builds a ResponseItem for the user message with optional image and file attachments. + /// Validates count, size, MIME type, and Base64 format. Image bytes are uploaded to the + /// Foundry Files API (purpose: assistants) and referenced by file id. + /// + private async Task BuildUserMessageAsync( + string message, + List? imageDataUris, + List? fileDataUris, + CancellationToken cancellationToken) + { + if ((imageDataUris == null || imageDataUris.Count == 0) && + (fileDataUris == null || fileDataUris.Count == 0)) + { + return ResponseItem.CreateUserMessageItem(message); + } + + var contentParts = new List + { + ResponseContentPart.CreateInputTextPart(message) + }; + + var errors = new List(); + + // Process images + if (imageDataUris != null && imageDataUris.Count > 0) + { + // Enforce maximum image count + if (imageDataUris.Count > MaxImageCount) + { + throw new ArgumentException( + $"Invalid image attachments: Too many images ({imageDataUris.Count}), maximum {MaxImageCount} allowed"); + } + + for (int i = 0; i < imageDataUris.Count; i++) + { + var label = $"Image {i + 1}"; + + if (!TryParseDataUri(imageDataUris[i], out var mediaType, out var bytes, out var parseError)) + { + errors.Add($"{label}: {parseError}"); + continue; + } + + if (!AllowedImageTypes.Contains(mediaType)) + { + errors.Add($"{label}: Unsupported type '{mediaType}'. Allowed: PNG, JPEG, GIF, WebP"); + continue; + } + + if (bytes.Length > MaxImageSizeBytes) + { + var sizeMB = bytes.Length / (1024.0 * 1024.0); + errors.Add($"{label}: Size {sizeMB:F1}MB exceeds maximum 5MB"); + continue; + } + + // Upload image bytes via the OpenAI Files API and reference the returned file id. + // Foundry's Files proxy rejects purpose=vision/user_data with "Invalid file ContentType"; + // purpose=assistants is the accepted path and the resulting file id works with + // CreateInputImagePart on the Responses API. + var fileClient = GetProjectClient().ProjectOpenAIClient.GetOpenAIFileClient(); + var extension = mediaType switch + { + "image/png" => ".png", + "image/jpeg" => ".jpg", + "image/gif" => ".gif", + "image/webp" => ".webp", + _ => ".bin", + }; + // Prefix uploaded filenames so the cleanup endpoint can identify files uploaded + // by this web app versus other files in the shared Foundry project. + var imageFileName = $"{WebAppUploadFilenamePrefix}{Guid.NewGuid():N}{extension}"; + using var imageStream = new MemoryStream(bytes); + // Azure Foundry Files API only accepts purpose = assistants | batch | fine-tune | evals. + // Use purpose=assistants per Azure Responses API docs. + // See: learn.microsoft.com/azure/foundry/openai/how-to/responses#file-input + var uploaded = await fileClient.UploadFileAsync( + imageStream, + imageFileName, + FileUploadPurpose.Assistants, + cancellationToken); + contentParts.Add(ResponseContentPart.CreateInputImagePart(uploaded.Value.Id)); + } + } + + // Process file attachments + if (fileDataUris != null && fileDataUris.Count > 0) + { + // Enforce maximum file count + if (fileDataUris.Count > MaxFileCount) + { + throw new ArgumentException( + $"Invalid file attachments: Too many files ({fileDataUris.Count}), maximum {MaxFileCount} allowed"); + } + + for (int i = 0; i < fileDataUris.Count; i++) + { + var file = fileDataUris[i]; + var label = $"File {i + 1} ({file.FileName})"; + + if (!TryParseDataUri(file.DataUri, out var mediaType, out var bytes, out var parseError)) + { + errors.Add($"{label}: {parseError}"); + continue; + } + + if (!AllowedDocumentTypes.Contains(mediaType)) + { + errors.Add($"{label}: Unsupported type '{mediaType}'"); + continue; + } + + // Verify MIME type matches what was declared + if (!string.Equals(mediaType, file.MimeType.ToLowerInvariant(), StringComparison.OrdinalIgnoreCase)) + { + errors.Add($"{label}: MIME type mismatch (declared: {file.MimeType}, detected: {mediaType})"); + continue; + } + + if (bytes.Length > MaxFileSizeBytes) + { + var sizeMB = bytes.Length / (1024.0 * 1024.0); + errors.Add($"{label}: Size {sizeMB:F1}MB exceeds maximum 20MB"); + continue; + } + + // Handle text-based files by inlining their content + // The Responses API only supports PDF for CreateInputFilePart + if (TextBasedDocumentTypes.Contains(mediaType)) + { + var textContent = System.Text.Encoding.UTF8.GetString(bytes); + var inlineText = $"\n\n--- Content of {file.FileName} ---\n{textContent}\n--- End of {file.FileName} ---\n"; + contentParts.Add(ResponseContentPart.CreateInputTextPart(inlineText)); + } + else if (FileInputTypes.Contains(mediaType)) + { + contentParts.Add(ResponseContentPart.CreateInputFilePart( + BinaryData.FromBytes(bytes), + mediaType, + file.FileName)); + } + } + } + + if (errors.Count > 0) + { + throw new ArgumentException($"Invalid attachments: {string.Join("; ", errors)}"); + } + + return ResponseItem.CreateUserMessageItem(contentParts); + } + + /// + /// Parses a data URI into its media type and decoded bytes. + /// + /// true if parsing succeeded; false with an error message otherwise. + private static bool TryParseDataUri(string dataUri, out string mediaType, out byte[] bytes, out string error) + { + mediaType = string.Empty; + bytes = Array.Empty(); + error = string.Empty; + + if (!dataUri.StartsWith("data:")) + { + error = "Invalid format (must be data URI)"; + return false; + } + + var semiIndex = dataUri.IndexOf(';'); + var commaIndex = dataUri.IndexOf(','); + + if (semiIndex < 0 || commaIndex < 0 || commaIndex < semiIndex) + { + error = "Malformed data URI"; + return false; + } + + mediaType = dataUri[5..semiIndex].ToLowerInvariant(); + + var base64Data = dataUri[(commaIndex + 1)..]; + try + { + bytes = Convert.FromBase64String(base64Data); + } + catch (FormatException) + { + error = "Invalid Base64 encoding"; + return false; + } + + return true; + } + + /// + /// Extracts annotation information from a completed response item. + /// + private List ExtractAnnotations( + ResponseItem? item, + Dictionary? fileSearchQuotes = null) + { + var annotations = new List(); + + if (item is not MessageResponseItem messageItem) + return annotations; + + foreach (var content in messageItem.Content) + { + if (content.OutputTextAnnotations == null) continue; + + foreach (var annotation in content.OutputTextAnnotations) + { + var annotationInfo = annotation switch + { + UriCitationMessageAnnotation uriAnnotation => new AnnotationInfo + { + Type = "uri_citation", + Label = uriAnnotation.Title ?? "Source", + Url = uriAnnotation.Uri?.ToString(), + StartIndex = uriAnnotation.StartIndex, + EndIndex = uriAnnotation.EndIndex + }, + + FileCitationMessageAnnotation fileCitation => new AnnotationInfo + { + Type = "file_citation", + Label = fileCitation.Filename ?? fileCitation.FileId ?? "File", + FileId = fileCitation.FileId, + StartIndex = fileCitation.Index, + EndIndex = fileCitation.Index, + Quote = fileSearchQuotes?.TryGetValue(fileCitation.FileId ?? string.Empty, out var quote) == true + ? quote : null + }, + + FilePathMessageAnnotation filePath => new AnnotationInfo + { + Type = "file_path", + Label = filePath.FileId?.Split('/').LastOrDefault() ?? "Generated File", + FileId = filePath.FileId, + StartIndex = filePath.Index, + EndIndex = filePath.Index + }, + + ContainerFileCitationMessageAnnotation containerCitation => new AnnotationInfo + { + Type = "container_file_citation", + Label = containerCitation.Filename ?? "Container File", + FileId = containerCitation.FileId, + ContainerId = containerCitation.ContainerId, + StartIndex = containerCitation.StartIndex, + EndIndex = containerCitation.EndIndex, + Quote = fileSearchQuotes?.TryGetValue(containerCitation.FileId ?? string.Empty, out var containerQuote) == true + ? containerQuote : null + }, + + _ => null + }; + + if (annotationInfo != null) + annotations.Add(annotationInfo); + } + } + + return annotations; + } + + /// + /// Create a new conversation for the agent. + /// Uses ProjectConversation from Azure.AI.Projects for server-managed state. + /// + public async Task CreateConversationAsync(string? firstMessage = null, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + try + { + _logger.LogInformation("Creating new conversation"); + + ProjectConversationCreationOptions conversationOptions = new(); + + if (!string.IsNullOrEmpty(firstMessage)) + { + // Store title in metadata (truncate to 50 chars) + var title = firstMessage.Length > 50 + ? firstMessage[..50] + "..." + : firstMessage; + conversationOptions.Metadata["title"] = title; + } + + ProjectConversation conversation + = await GetProjectClient().ProjectOpenAIClient.GetProjectConversationsClient().CreateProjectConversationAsync( + conversationOptions, + cancellationToken); + + _logger.LogInformation( + "Created conversation: {ConversationId}", + conversation.Id); + return conversation.Id; + } + catch (OperationCanceledException) + { + _logger.LogWarning("Conversation creation was cancelled"); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create conversation"); + throw; + } + } + + /// + /// List conversations for the current agent. + /// + public async Task> ListConversationsAsync(int limit = 20, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + try + { + _logger.LogInformation("Listing conversations (limit={Limit})", limit); + + // Pin to the same resolved version metadata/streaming use. + var resolvedAgent = await GetAgentAsync(cancellationToken); + var resolvedVersion = _configuredAgentVersion ?? resolvedAgent.Version; + + var conversations = new List(); + // Fetch limit+1 to detect if more conversations exist beyond the requested page + var fetchLimit = limit + 1; + await foreach (var conv in GetProjectClient().ProjectOpenAIClient.GetProjectConversationsClient().GetProjectConversationsAsync( + new AgentReference(_agentId, resolvedVersion), cancellationToken: cancellationToken)) + { + conversations.Add(new ConversationSummary + { + Id = conv.Id, + Title = conv.Metadata?.TryGetValue("title", out var title) == true ? title : null, + CreatedAt = conv.CreatedAt.ToUnixTimeSeconds() + }); + + if (conversations.Count >= fetchLimit) + break; + } + + _logger.LogInformation("Found {Count} conversations", conversations.Count); + return conversations; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to list conversations"); + throw; + } + } + + /// + /// Get messages for a specific conversation. + /// + public async Task> GetConversationMessagesAsync( + string conversationId, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + try + { + _logger.LogInformation("Getting messages for conversation: {ConversationId}", conversationId); + + var messages = new List(); + + // Filter to message items only + await foreach (var item in GetProjectClient().ProjectOpenAIClient.GetProjectConversationsClient().GetProjectConversationItemsAsync( + conversationId, itemKind: AgentResponseItemKind.Message, cancellationToken: cancellationToken)) + { + var responseItem = item.AsResponseResultItem(); + if (responseItem is MessageResponseItem messageItem) + { + var content = string.Join("", messageItem.Content + .Where(c => c.Text != null) + .Select(c => c.Text)); + + messages.Add(new ConversationMessageInfo + { + Role = messageItem.Role.ToString().ToLowerInvariant(), + Content = content + }); + } + } + + _logger.LogInformation("Found {Count} messages in conversation {ConversationId}", messages.Count, conversationId); + messages.Reverse(); + return messages; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get messages for conversation: {ConversationId}", conversationId); + throw; + } + } + + /// + /// Delete a conversation. + /// + /// + /// TODO: The Azure.AI.Projects SDK does not expose a delete conversation API. + /// This method is a stub that will need to be updated when the SDK adds delete support. + /// + public Task DeleteConversationAsync(string conversationId, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + _logger.LogWarning( + "DeleteConversationAsync is not yet supported by the SDK. ConversationId: {ConversationId}", + conversationId); + + // TODO: Replace with actual SDK call when available. + // The ProjectConversationsClient currently only supports Create, Get, List, and Update. + throw new NotSupportedException( + "Conversation deletion is not yet supported by the Azure.AI.Projects SDK."); + } + + /// + /// Download a file generated by code interpreter or other tools. + /// Container files (with containerId) use the REST API: GET /openai/v1/containers/{containerId}/files/{fileId}/content. + /// Standard files use the OpenAI FileClient. + /// + public async Task<(BinaryData Content, string FileName)> DownloadFileAsync( + string fileId, + string? containerId = null, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + try + { + if (!string.IsNullOrEmpty(containerId)) + { + return await DownloadContainerFileAsync(fileId, containerId, cancellationToken); + } + + _logger.LogInformation("Downloading standard file: {FileId}", fileId); + var fileClient = GetProjectClient().ProjectOpenAIClient.GetOpenAIFileClient(); + var fileContent = await fileClient.DownloadFileAsync(fileId, cancellationToken); + var fileInfo = await fileClient.GetFileAsync(fileId, cancellationToken); + var fileName = fileInfo.Value?.Filename ?? $"{fileId}.bin"; + _logger.LogInformation("Downloaded file: {FileId}, Name: {FileName}, Size: {Size} bytes", + fileId, fileName, fileContent.Value.ToMemory().Length); + return (fileContent.Value, fileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to download file {FileId}. Error: {Error}", fileId, ex.Message); + throw; + } + } + + /// + /// Download a container file via REST API. + /// Endpoint: GET {projectEndpoint}/openai/v1/containers/{containerId}/files/{fileId}/content + /// + private async Task<(BinaryData Content, string FileName)> DownloadContainerFileAsync( + string fileId, + string containerId, + CancellationToken cancellationToken) + { + _logger.LogInformation("Downloading container file: {FileId} from container: {ContainerId}", fileId, containerId); + + // Reuse the same credential as the project client (MI or OBO) + TokenCredential credential; + if (_useObo) + { + var userToken = ExtractBearerToken(); + credential = CreateOboCredential(userToken ?? throw new InvalidOperationException("OBO requires bearer token")); + } + else + { + credential = _fallbackCredential; + } + + var tokenRequestContext = new TokenRequestContext(["https://ai.azure.com/.default"]); + var accessToken = await credential.GetTokenAsync(tokenRequestContext, cancellationToken); + + var requestUrl = $"{_agentEndpoint.TrimEnd('/')}/openai/v1/containers/{Uri.EscapeDataString(containerId)}/files/{Uri.EscapeDataString(fileId)}/content"; + using var httpClient = _httpClientFactory.CreateClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken.Token); + + var response = await httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + + var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken); + + // Try to extract filename from Content-Disposition header, fall back to fileId + var fileName = $"{fileId}.bin"; + if (response.Content.Headers.ContentDisposition?.FileName is { } headerFileName) + { + fileName = headerFileName.Trim('"'); + } + + _logger.LogInformation("Downloaded container file: {FileId}, Name: {FileName}, Size: {Size} bytes", + fileId, fileName, bytes.Length); + return (BinaryData.FromBytes(bytes), fileName); + } + + /// + /// Get the agent metadata (name, description, etc.) for display in UI. + /// Reads directly from the cached ProjectsAgentVersion. + /// + public async Task GetAgentMetadataAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var agentVersion = await GetAgentAsync(cancellationToken); + + if (s_cachedMetadata != null) + return s_cachedMetadata; + + var definition = agentVersion.Definition as DeclarativeAgentDefinition; + var metadata = agentVersion.Metadata?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + // Log metadata keys at debug level for troubleshooting + if (metadata != null && metadata.Count > 0) + { + _logger.LogDebug("Agent metadata keys: {Keys}", string.Join(", ", metadata.Keys)); + } + + // Parse starter prompts from metadata + List? starterPrompts = ParseStarterPrompts(metadata); + + s_cachedMetadata = new AgentMetadataResponse + { + Id = _agentId, + Object = "agent", + CreatedAt = agentVersion.CreatedAt.ToUnixTimeSeconds(), + Name = agentVersion.Name ?? "AI Assistant", + Description = agentVersion.Description, + Model = definition?.Model ?? string.Empty, + Instructions = definition?.Instructions ?? string.Empty, + Metadata = metadata, + StarterPrompts = starterPrompts + }; + + return s_cachedMetadata; + } + + /// + /// Parse starter prompts from agent metadata. + /// Microsoft Foundry stores starter prompts as newline-separated text in the "starterPrompts" metadata key. + /// Example: "How's the weather?\nIs your fridge running?\nTell me a joke" + /// + private List? ParseStarterPrompts(Dictionary? metadata) + { + if (metadata == null) + return null; + + // Microsoft Foundry uses camelCase "starterPrompts" key with newline-separated values + if (!metadata.TryGetValue("starterPrompts", out var starterPromptsValue)) + return null; + + if (string.IsNullOrWhiteSpace(starterPromptsValue)) + return null; + + // Split by newlines and filter out empty entries + var prompts = starterPromptsValue + .Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries) + .Select(p => p.Trim()) + .Where(p => !string.IsNullOrEmpty(p)) + .ToList(); + + if (prompts.Count > 0) + { + _logger.LogDebug("Parsed {Count} starter prompts from agent metadata", prompts.Count); + return prompts; + } + + return null; + } + + /// + /// Get basic agent info string (for debugging). + /// + public async Task GetAgentInfoAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var agentVersion = await GetAgentAsync(cancellationToken); + return agentVersion.Name ?? _agentId; + } + + /// + /// Get token usage from the last streaming response. + /// + public (int InputTokens, int OutputTokens, int TotalTokens)? GetLastUsage() => + _lastUsage is null ? null : (_lastUsage.InputTokenCount, _lastUsage.OutputTokenCount, _lastUsage.TotalTokenCount); + + /// + /// Returns a count and total byte size of files uploaded by this web app (identified by + /// filename prefix ) that are still stored in the + /// Foundry project. Uses because that is the purpose + /// under which stores image uploads. + /// + public async Task ListUploadedFilesAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var fileClient = GetProjectClient().ProjectOpenAIClient.GetOpenAIFileClient(); + var result = await fileClient.GetFilesAsync(FilePurpose.Assistants, cancellationToken); + + int count = 0; + long totalBytes = 0; + foreach (var file in result.Value) + { + if (file.Filename != null && file.Filename.StartsWith(WebAppUploadFilenamePrefix, StringComparison.Ordinal)) + { + count++; + totalBytes += file.SizeInBytesLong ?? file.SizeInBytes ?? 0; + } + } + + _logger.LogInformation("ListUploadedFiles: {Count} files, {TotalBytes} bytes", count, totalBytes); + return new UploadedFilesInfo(count, totalBytes); + } + + /// + /// Deletes every file in the Foundry project whose filename begins with + /// . Intended as a user-triggered cleanup + /// because the GA Files API does not expose expires_after on upload — see README + /// "Known limitations". Returns counts of successful and failed deletions; failures are + /// logged but do not abort the loop. + /// + public async Task CleanupUploadedFilesAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + var fileClient = GetProjectClient().ProjectOpenAIClient.GetOpenAIFileClient(); + var result = await fileClient.GetFilesAsync(FilePurpose.Assistants, cancellationToken); + + int deleted = 0; + int failed = 0; + foreach (var file in result.Value) + { + if (file.Filename == null || !file.Filename.StartsWith(WebAppUploadFilenamePrefix, StringComparison.Ordinal)) + { + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + + try + { + await fileClient.DeleteFileAsync(file.Id, cancellationToken); + deleted++; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + failed++; + _logger.LogWarning(ex, "Failed to delete uploaded file {FileId} ({FileName})", file.Id, file.Filename); + } + } + + _logger.LogInformation("CleanupUploadedFiles: deleted={Deleted} failed={Failed}", deleted, failed); + return new UploadedFilesCleanupResult(deleted, failed); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + // AIProjectClient does not implement IDisposable (verified via reflection on + // Azure.AI.Projects assembly). No cleanup needed for _projectClient. + _projectClient = null; + _logger.LogDebug("AgentFrameworkService disposed"); + } + } +} diff --git a/backend/WebApp.Api/Services/AzureAIAgentService.cs b/backend/WebApp.Api/Services/AzureAIAgentService.cs deleted file mode 100644 index a522973..0000000 --- a/backend/WebApp.Api/Services/AzureAIAgentService.cs +++ /dev/null @@ -1,402 +0,0 @@ -using Azure.AI.Projects; -using Azure.AI.Projects.OpenAI; -using Azure.Core; -using Azure.Identity; -using OpenAI.Responses; -using System.Runtime.CompilerServices; -using WebApp.Api.Models; - -namespace WebApp.Api.Services; - -#pragma warning disable OPENAI001 - -public class AzureAIAgentService : IDisposable -{ - private readonly AIProjectClient _projectClient; - private readonly string _agentId; - private readonly ILogger _logger; - private AgentVersion? _latestAgentVersion; - private AgentMetadataResponse? _cachedMetadata; // Cache metadata to avoid repeated calls - private readonly SemaphoreSlim _agentLock = new(1, 1); - private UsageInfo? _lastRunUsage; - private bool _disposed = false; - - public AzureAIAgentService( - IConfiguration configuration, - ILogger logger) - { - _logger = logger; - - // Get Azure AI Agent Service configuration - var endpoint = configuration["AI_AGENT_ENDPOINT"] - ?? throw new InvalidOperationException("AI_AGENT_ENDPOINT is not configured"); - - _agentId = configuration["AI_AGENT_ID"] - ?? throw new InvalidOperationException("AI_AGENT_ID is not configured"); - - _logger.LogInformation("Initializing Azure AI Agent Service client for endpoint: {Endpoint}, Agent ID: {AgentId}", endpoint, _agentId); - - // IMPORTANT: Use explicit credential types to avoid unexpected behavior - // Reference: https://learn.microsoft.com/en-us/dotnet/azure/sdk/authentication/best-practices - - TokenCredential credential; - var environment = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Production"; - - if (environment == "Development") - { - // Local development: Use ChainedTokenCredential for explicit, predictable behavior - // This avoids the "fail fast" mode issues with DefaultAzureCredential - _logger.LogInformation("Development environment: Using ChainedTokenCredential (AzureCli -> AzureDeveloperCli)"); - - credential = new ChainedTokenCredential( - new AzureCliCredential(), - new AzureDeveloperCliCredential() - ); - } - else - { - // Production: Use explicit ManagedIdentityCredential (system-assigned) - // This prevents DefaultAzureCredential from attempting other credential types - // and ensures deterministic behavior in production - _logger.LogInformation("Production environment: Using ManagedIdentityCredential (system-assigned)"); - credential = new ManagedIdentityCredential(); - } - - // Create client for Azure AI Agent Service - _projectClient = new AIProjectClient(new Uri(endpoint), credential); - - _logger.LogInformation("Azure AI Agent Service client initialized successfully"); - - // Pre-load agent at startup (matches Azure sample pattern in gunicorn.conf.py) - // This ensures the agent is ready before the first request - _ = Task.Run(async () => - { - try - { - await GetAgentAsync(); - _logger.LogInformation("Agent pre-loaded successfully at startup"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to pre-load agent at startup. Will retry on first request."); - } - }); - } - - private async Task GetAgentAsync(CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - if (_latestAgentVersion != null) - return _latestAgentVersion; - - await _agentLock.WaitAsync(cancellationToken); - try - { - if (_latestAgentVersion != null) - return _latestAgentVersion; - - _logger.LogInformation("Loading existing agent from Azure AI Agent Service: {AgentId}", _agentId); - - // Get the existing agent - AgentRecord agentRecord = await _projectClient.Agents.GetAgentAsync(_agentId, cancellationToken); - _latestAgentVersion = agentRecord.Versions.Latest; - - _logger.LogInformation("Successfully connected to existing Azure AI Agent: {AgentId}", _agentId); - return _latestAgentVersion; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to load agent from Azure AI Agent Service"); - throw; - } - finally - { - _agentLock.Release(); - } - } - - public async Task GetAgentInfoAsync(CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - var agent = await GetAgentAsync(cancellationToken); - return agent?.ToString() ?? "AI Assistant"; - } - - /// - /// Get the agent metadata (name, description, metadata) for display in UI. - /// Cached after first call to avoid repeated Azure API calls. - /// - public async Task GetAgentMetadataAsync(CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - // Return cached metadata if available (matches Azure sample pattern) - if (_cachedMetadata != null) - { - _logger.LogDebug("Returning cached agent metadata"); - return _cachedMetadata; - } - - var agent = await GetAgentAsync(cancellationToken); - PromptAgentDefinition? promptAgentDefinition = (agent.Definition as PromptAgentDefinition); - - _cachedMetadata = new AgentMetadataResponse - { - Id = agent.Id,//persistentAgent.Value.Id, - Object = "agent", - CreatedAt = agent.CreatedAt.ToUnixTimeSeconds(), - Name = agent.Name ?? "AI Assistant", - Description = agent.Description, - Model = promptAgentDefinition?.Model ?? string.Empty, - Instructions = promptAgentDefinition?.Instructions ?? string.Empty, - Metadata = agent.Metadata?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) - }; - - _logger.LogInformation("Cached agent metadata for future requests"); - return _cachedMetadata; - } - - public async Task CreateConversationAsync(string? firstMessage = null, CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - try - { - _logger.LogInformation("Creating new conversation"); - - ProjectConversationCreationOptions conversationOptions = new(); - - if (!string.IsNullOrEmpty(firstMessage)) - { - // Store title in metadata (truncate to 50 chars) - var title = firstMessage.Length > 50 - ? firstMessage[..50] + "..." - : firstMessage; - conversationOptions.Metadata["title"] = title; - } - - ProjectConversation conversation - = await _projectClient.OpenAI.Conversations.CreateProjectConversationAsync( - conversationOptions, - cancellationToken); - - _logger.LogInformation( - "Created conversation: {ConversationId} with title: {Title}", - conversation.Id, - conversation.Metadata.TryGetValue("title", out string? metadataTitle) - ? metadataTitle - : "New Conversation"); - return conversation.Id; - } - catch (OperationCanceledException) - { - _logger.LogWarning("Conversation creation was cancelled"); - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create conversation"); - throw; - } - } - - /// - /// Streams agent response for a message in a conversation with optional image attachments. - /// Returns chunks of text as they arrive, capturing usage metrics for the run. - /// - public async IAsyncEnumerable StreamMessageAsync( - string conversationId, - string message, - List? imageDataUris = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - var agent = await GetAgentAsync(cancellationToken); - - _logger.LogInformation( - "Streaming message to conversation: {ConversationId}, ImageCount: {ImageCount}", - conversationId, - imageDataUris?.Count ?? 0); - - if (string.IsNullOrWhiteSpace(message)) - { - _logger.LogWarning("Attempted to stream empty message to conversation {ConversationId}", conversationId); - throw new ArgumentException("Message cannot be null or whitespace", nameof(message)); - } - - ResponseItem userMessage = BuildUserMessage(message, imageDataUris); - - ProjectResponsesClient responsesClient - = _projectClient.OpenAI.GetProjectResponsesClientForAgent(agent, conversationId); - - await foreach (StreamingResponseUpdate update - in responsesClient.CreateResponseStreamingAsync( - inputItems: [userMessage], - options: new ResponseCreationOptions(), - cancellationToken: cancellationToken)) - { - if (update is StreamingResponseOutputTextDeltaUpdate deltaUpdate) - { - yield return deltaUpdate.Delta; - } - if (update is StreamingResponseCompletedUpdate completedUpdate) - { - _lastRunUsage = ExtractUsageInfo(completedUpdate.Response.Usage); - } - } - - _logger.LogInformation("Completed streaming response for conversation: {ConversationId}", conversationId); - } - - /// - /// Validates image data URIs for count, size, MIME type, and base64 integrity. - /// Returns list of validation errors, or empty list if all valid. - /// - private static List ValidateImageDataUris(List? imageDataUris) - { - var errors = new List(); - - if (imageDataUris == null || imageDataUris.Count == 0) - return errors; - - // Enforce maximum count - if (imageDataUris.Count > 5) - { - errors.Add($"Too many images: {imageDataUris.Count} provided, maximum 5 allowed"); - return errors; // Short-circuit if count exceeded - } - - var allowedMimeTypes = new[] { "image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp" }; - const long maxSizeBytes = 5 * 1024 * 1024; // 5MB - - for (int i = 0; i < imageDataUris.Count; i++) - { - var dataUri = imageDataUris[i]; - - // Validate format: data:[][;base64], - if (!dataUri.StartsWith("data:")) - { - errors.Add($"Image {i + 1}: Invalid data URI format (must start with 'data:')"); - continue; - } - - var semiIndex = dataUri.IndexOf(';'); - var commaIndex = dataUri.IndexOf(','); - - if (semiIndex < 0 || commaIndex < 0 || commaIndex < semiIndex) - { - errors.Add($"Image {i + 1}: Malformed data URI structure"); - continue; - } - - // Extract and validate MIME type - string mediaType = dataUri["data:".Length..semiIndex].ToLowerInvariant(); - if (!allowedMimeTypes.Contains(mediaType)) - { - errors.Add($"Image {i + 1}: Unsupported MIME type '{mediaType}' (allowed: PNG, JPEG, GIF, WebP)"); - continue; - } - - // Validate and decode base64 - string base64Data = dataUri[(commaIndex + 1)..]; - try - { - byte[] imageBytes = Convert.FromBase64String(base64Data); - - // Enforce size limit - if (imageBytes.Length > maxSizeBytes) - { - var sizeMB = imageBytes.Length / (1024.0 * 1024.0); - errors.Add($"Image {i + 1}: Size {sizeMB:F1}MB exceeds maximum 5MB"); - } - } - catch (FormatException) - { - errors.Add($"Image {i + 1}: Invalid base64 encoding"); - } - } - - return errors; - } - - /// - /// Builds message content blocks from text and optional image data URIs. - /// Images are encoded as base64 data URIs for inline vision analysis. - /// - private MessageResponseItem BuildUserMessage(string message, List? imageDataUris) - { - // Validate images before processing - var validationErrors = ValidateImageDataUris(imageDataUris); - if (validationErrors.Count > 0) - { - _logger.LogWarning("Image attachment validation failed: {Errors}", string.Join("; ", validationErrors)); - throw new ArgumentException($"Invalid image attachments: {string.Join(", ", validationErrors)}"); - } - - List messageContentParts = - [ - ResponseContentPart.CreateInputTextPart(message), - ]; - - foreach (string imageDataUri in imageDataUris ?? []) - { - // data:[][;base64], - if (imageDataUri.StartsWith("data:")) - { - string mediaType = imageDataUri["data:".Length..imageDataUri.IndexOf(';')]; - BinaryData imageBytes = BinaryData.FromBytes( - Convert.FromBase64String(imageDataUri[(imageDataUri.IndexOf(',') + 1)..])); - messageContentParts.Add(ResponseContentPart.CreateInputImagePart(imageBytes, mediaType)); - } - else - { - messageContentParts.Add(ResponseContentPart.CreateInputImagePart(new Uri(imageDataUri))); - } - } - - return ResponseItem.CreateUserMessageItem(messageContentParts); - } - - /// - /// Extracts token usage metrics from Azure AI Agents run usage data. - /// - private static UsageInfo ExtractUsageInfo(ResponseTokenUsage usage) - { - return new UsageInfo - { - PromptTokens = usage.InputTokenCount, - CompletionTokens = usage.OutputTokenCount, - TotalTokens = usage.TotalTokenCount - }; - } - - public Task GetLastRunUsageAsync(CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - return Task.FromResult(_lastRunUsage); - } - - /// - /// Dispose of managed resources. - /// - public void Dispose() - { - if (!_disposed) - { - _agentLock.Dispose(); - _disposed = true; - _logger.LogDebug("AzureAIAgentService disposed"); - } - } -} - -public class UsageInfo -{ - public int PromptTokens { get; set; } - public int CompletionTokens { get; set; } - public int TotalTokens { get; set; } -} diff --git a/backend/WebApp.Api/WebApp.Api.csproj b/backend/WebApp.Api/WebApp.Api.csproj index c8b3dc7..b8e871f 100644 --- a/backend/WebApp.Api/WebApp.Api.csproj +++ b/backend/WebApp.Api/WebApp.Api.csproj @@ -1,17 +1,18 @@  - net9.0 + net10.0 enable enable e523184f-8410-46c3-b331-b2f38fba7e1f - - - - + + + + + diff --git a/backend/WebApp.ServiceDefaults/Extensions.cs b/backend/WebApp.ServiceDefaults/Extensions.cs index 4860861..a9cfb1c 100644 --- a/backend/WebApp.ServiceDefaults/Extensions.cs +++ b/backend/WebApp.ServiceDefaults/Extensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Logging; +using Azure.Monitor.OpenTelemetry.AspNetCore; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; @@ -48,6 +49,7 @@ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicati }) .WithTracing(tracing => { + tracing.AddSource("Azure.*"); tracing.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation(); }); @@ -66,6 +68,15 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli builder.Services.AddOpenTelemetry().UseOtlpExporter(); } + var aiConnStr = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]; + if (!string.IsNullOrEmpty(aiConnStr)) + { + builder.Services.AddOpenTelemetry().UseAzureMonitor(options => + { + options.ConnectionString = aiConnStr; + }); + } + return builder; } diff --git a/backend/WebApp.ServiceDefaults/WebApp.ServiceDefaults.csproj b/backend/WebApp.ServiceDefaults/WebApp.ServiceDefaults.csproj index 1b4fccc..efe75d2 100644 --- a/backend/WebApp.ServiceDefaults/WebApp.ServiceDefaults.csproj +++ b/backend/WebApp.ServiceDefaults/WebApp.ServiceDefaults.csproj @@ -1,19 +1,20 @@  - net9.0 + net10.0 enable enable - - - - - - - + + + + + + + + diff --git a/backend/WebApp.sln b/backend/WebApp.sln index 878375d..ed8d071 100644 --- a/backend/WebApp.sln +++ b/backend/WebApp.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp.ServiceDefaults", "W EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp.Api", "WebApp.Api\WebApp.Api.csproj", "{57812A12-8EB0-41D2-A2D7-55B6494A76F8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApp.Api.Tests", "WebApp.Api.Tests\WebApp.Api.Tests.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,18 @@ Global {57812A12-8EB0-41D2-A2D7-55B6494A76F8}.Release|x64.Build.0 = Release|Any CPU {57812A12-8EB0-41D2-A2D7-55B6494A76F8}.Release|x86.ActiveCfg = Release|Any CPU {57812A12-8EB0-41D2-A2D7-55B6494A76F8}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/deployment/AGENTS.md b/deployment/AGENTS.md deleted file mode 100644 index 104f2c6..0000000 --- a/deployment/AGENTS.md +++ /dev/null @@ -1,195 +0,0 @@ -# Deployment - azd Hooks - -**Context**: See `.github/copilot-instructions.md` for architecture - -## Preprovision Hook - -**Goal**: Create Entra app + discover AI Foundry + generate config files - -```powershell -# Get environment -$envName = azd env get-value AZURE_ENV_NAME -$tenantId = (az account show | ConvertFrom-Json).tenantId - -# Create app registration -$clientId = & "$PSScriptRoot\modules\New-EntraAppRegistration.ps1" ` - -AppName "$envName-client" -TenantId $tenantId - -azd env set ENTRA_SPA_CLIENT_ID $clientId -azd env set ENTRA_TENANT_ID $tenantId - -# Discover AI Foundry resources -$foundryResources = az resource list --resource-type "Microsoft.MachineLearningServices/workspaces" | ConvertFrom-Json - -if ($foundryResources.Count -eq 0) { - Write-Error "No AI Foundry resources found in subscription" - exit 1 -} - -# If multiple resources, prompt user to select -if ($foundryResources.Count -gt 1) { - Write-Host "Multiple AI Foundry resources found:" - for ($i = 0; $i -lt $foundryResources.Count; $i++) { - Write-Host "[$i] $($foundryResources[$i].name) (Resource Group: $($foundryResources[$i].resourceGroup))" - } - $selection = Read-Host "Select resource [0-$($foundryResources.Count - 1)]" - $selectedResource = $foundryResources[$selection] -} else { - $selectedResource = $foundryResources[0] -} - -$resourceGroup = $selectedResource.resourceGroup -$resourceName = $selectedResource.name - -# Discover agents via REST API (using shared module) -$allAgents = & "$PSScriptRoot/modules/Get-AIFoundryAgents.ps1" -ProjectEndpoint $aiEndpoint - -if ($allAgents.Count -eq 0) { - Write-Error "No agents found in AI Foundry resource" - exit 1 -} - -# Select first agent or prompt if multiple -if ($allAgents.Count -eq 1) { - $agentName = $allAgents[0].name -} else { - # Display agents and use first one - Write-Host "Found $($allAgents.Count) agents, using first: $($allAgents[0].name)" - $agentName = $allAgents[0].name -} - -# Set environment variables -azd env set AI_FOUNDRY_RESOURCE_GROUP $resourceGroup -azd env set AI_FOUNDRY_RESOURCE_NAME $resourceName -azd env set AI_AGENT_ENDPOINT $aiEndpoint -azd env set AI_AGENT_ID $agentName - -# Generate frontend .env.local -@" -VITE_ENTRA_SPA_CLIENT_ID=$clientId -VITE_ENTRA_TENANT_ID=$tenantId -"@ | Set-Content "frontend/.env.local" - -# Generate backend .env -@" -AzureAd__ClientId=$clientId -AzureAd__TenantId=$tenantId -AI_AGENT_ENDPOINT=$aiEndpoint -AI_AGENT_ID=$agentName -"@ | Set-Content "backend/WebApp.Api/.env" -``` - -**Key Points**: -- Auto-discovers AI Foundry resources in current subscription -- Prompts user to select if multiple resources exist -- Discovers agents via REST API (v2025-11-15-preview) using `Get-AIFoundryAgents.ps1` module -- Uses agent names (not IDs) for configuration -- Generates `.env` files with all required configuration - -## Postprovision Hook - -**Goal**: Update redirect URIs + build/deploy container - -**Pattern**: Updates Entra app redirect URIs, then calls shared `build-and-deploy-container.ps1` module - -```powershell -# 1. Get Container App URL -$appUrl = az containerapp show --name $containerApp --resource-group $rg ` - --query "properties.configuration.ingress.fqdn" -o tsv -$appUrl = "https://$appUrl" - -# 2. Update app registration redirect URIs (local dev + production) -$redirectUris = @("http://localhost:5173", "http://localhost:8080", $appUrl) -az rest --method PATCH ` - --uri "https://graph.microsoft.com/v1.0/applications/$appObjectId" ` - --body (ConvertTo-Json @{ spa = @{ redirectUris = $redirectUris } }) - -# 3. Call shared build/deploy module (same logic as deploy.ps1) -$scriptPath = Join-Path (Split-Path $PSScriptRoot -Parent) "scripts\build-and-deploy-container.ps1" -$containerAppUrl = & $scriptPath ` - -ClientId $clientId ` - -TenantId $tenantId ` - -ResourceGroup $resourceGroup ` - -ContainerApp $containerApp ` - -AcrName $acrName -``` - -## Docker Multi-Stage Build - -**Pattern**: Build React → Build .NET → Runtime - -**Custom npm Registries**: Add `.npmrc` to `frontend/` directory - it's automatically copied - -```dockerfile -# Stage 1: Build React -FROM node:20-alpine AS frontend -ARG ENTRA_SPA_CLIENT_ID -ARG ENTRA_TENANT_ID -WORKDIR /app -COPY frontend/package*.json ./ -COPY frontend/.npmrc* ./ 2>/dev/null || true # Copy .npmrc if present (custom registries) -RUN npm ci # Respects .npmrc for custom registries -COPY frontend/ ./ -ENV VITE_ENTRA_SPA_CLIENT_ID=$ENTRA_SPA_CLIENT_ID -RUN npm run build - -# Stage 2: Build .NET -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS backend -WORKDIR /app -COPY backend/*.sln ./ -COPY backend/WebApp.Api/*.csproj ./WebApp.Api/ -RUN dotnet restore -COPY backend/ ./ -RUN dotnet publish -c Release -o /app/publish - -# Stage 3: Runtime -FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine -WORKDIR /app -COPY --from=backend /app/publish ./ -COPY --from=frontend /app/dist ./wwwroot -EXPOSE 8080 -ENTRYPOINT ["dotnet", "WebApp.Api.dll"] -``` - -## Shared Build Module - -`deployment/scripts/build-and-deploy-container.ps1` - -**Usage**: Called by both `postprovision` hook and `deploy` script - -```powershell -& "$scriptPath" -ClientId $id -TenantId $tid -ResourceGroup $rg -ContainerApp $app -AcrName $acr -``` - -**Logic**: -1. Detects if Docker is available and running -2. Uses local Docker build + push if available -3. Falls back to ACR cloud build if Docker unavailable -4. Updates Container App with new image -5. Returns Container App URL - -## Local Development Scripts - -`deployment/scripts/start-local-dev.ps1` - -**Features**: -- Validates configuration files -- Checks and installs/repairs npm dependencies if needed -- Starts both backend and frontend servers -- Displays URLs for local development - -## Troubleshooting - -```powershell -# Check current image -az containerapp show --name $app --resource-group $rg ` - --query "properties.template.containers[0].image" - -# View logs -az containerapp logs show --name $app --resource-group $rg --tail 100 - -# Check RBAC -$principalId = az containerapp show --name $app --resource-group $rg ` - --query "identity.principalId" -o tsv -az role assignment list --assignee $principalId -``` diff --git a/deployment/README.md b/deployment/README.md index f9e23c6..530863e 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -1,6 +1,6 @@ # Deployment Directory -**Context**: See `.github/copilot-instructions.md` for commands and workflows. See `deployment/AGENTS.md` for technical implementation. +**AI Assistance**: See `.github/skills/deploying-to-azure/SKILL.md` for deployment patterns. ## Structure @@ -10,51 +10,53 @@ deployment/ │ └── frontend.Dockerfile # Single-container build (React + ASP.NET Core) ├── hooks/ # Azure Developer CLI lifecycle hooks │ ├── preprovision.ps1 # Create Entra app + discover AI Foundry + generate config -│ ├── postprovision.ps1 # Build & deploy initial container -│ └── postdown.ps1 # Cleanup (optional) +│ ├── postprovision.ps1 # Update Entra redirect URIs + assign RBAC +│ ├── predeploy.ps1 # Build container (local Docker or ACR cloud build) +│ ├── postdown.ps1 # Cleanup (optional) │ └── modules/ # Reusable PowerShell modules +│ ├── Get-AIFoundryAgents.ps1 │ └── New-EntraAppRegistration.ps1 -└── scripts/ # User-invoked deployment scripts - ├── build-and-deploy-container.ps1 # Shared build/deploy logic (DRY) - ├── deploy.ps1 # Quick deploy (code-only updates) - └── start-local-dev.ps1 # Start native local development +└── scripts/ # User-invoked scripts + └── start-local-dev.ps1 # Start native local development ``` -## Key Files +## Build Strategy -| File | Purpose | When to Use | -|------|---------|-------------| -| `hooks/preprovision.ps1` | Discovery & config generation | Auto-runs during `azd up` | -| `hooks/postprovision.ps1` | Initial build & deployment | Auto-runs during `azd up` | -| `scripts/deploy.ps1` | Code-only deployment | Manual invocation for code changes | -| `scripts/start-local-dev.ps1` | Local dev server startup | Manual invocation for development | -| `scripts/build-and-deploy-container.ps1` | Shared build module | Called by postprovision & deploy | -| `docker/frontend.Dockerfile` | Production build | Used by build scripts | +Container builds use **local Docker when available** with **ACR cloud build as fallback**: -## Hook Relationship +| Docker Installed | Build Method | Speed | +|------------------|--------------|-------| +| ✅ Yes, running | Local Docker build + push to ACR | ~2 min | +| ❌ No | ACR cloud build | ~4-5 min | + +This is handled automatically by `predeploy.ps1`. + +## Key Commands + +| Command | Purpose | When to Use | +|---------|---------|-------------| +| `azd up` | Full provision + deploy | Initial setup, infrastructure changes | +| `azd deploy` | Code-only deployment | Fast iteration on code changes | +| `azd down` | Tear down resources | Cleanup | + +## Hook Workflow ``` azd up ├─ preprovision.ps1 (Entra + AI Foundry discovery + .env generation) - ├─ provision (Bicep deployment) - └─ postprovision.ps1 (calls build-and-deploy-container.ps1) + ├─ provision (Bicep deployment with placeholder image) + ├─ postprovision.ps1 (updates Entra redirect URIs + RBAC) + └─ predeploy.ps1 (builds container - local Docker or ACR cloud) -deploy.ps1 (direct invocation) - └─ build-and-deploy-container.ps1 - -build-and-deploy-container.ps1 (shared module) - ├─ Detects Docker availability - ├─ Local build + push OR ACR cloud build - └─ Updates Container App +azd deploy + └─ predeploy.ps1 (builds + pushes + updates Container App) ``` ## Quick Reference -For complete commands and workflows, see `.github/copilot-instructions.md` → Development Workflow section. - **Common tasks**: - First deployment: `azd up` -- Deploy code changes: `.\deployment\scripts\deploy.ps1` +- Deploy code changes: `azd deploy` - Local development: `.\deployment\scripts\start-local-dev.ps1` - Clean up: `azd down --force --purge` @@ -62,10 +64,8 @@ For complete commands and workflows, see `.github/copilot-instructions.md` → D **Build strategy**: Multi-stage (React build → .NET build → Runtime) -**Custom npm registries**: Add `.npmrc` to `frontend/` directory - automatically copied during build +**Build args**: Client ID and Tenant ID are automatically passed to the Dockerfile from azd environment variables. -**Build modes**: -- Local Docker (faster, recommended): Uses installed Docker daemon -- ACR cloud build (fallback): Uploads context to ACR for cloud build +**Custom npm registries**: Add `.npmrc` to `frontend/` directory - automatically copied during build -See `AGENTS.md` for complete Dockerfile and build module implementation. +For AI-assisted development, see `.github/skills/deploying-to-azure/SKILL.md`. diff --git a/deployment/docker/backend.Dockerfile b/deployment/docker/backend.Dockerfile deleted file mode 100644 index cceb18c..0000000 --- a/deployment/docker/backend.Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base -WORKDIR /app -EXPOSE 8080 - -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -WORKDIR /src - -# Copy solution and project files -COPY ["backend/WebApp.sln", "./"] -COPY ["backend/WebApp.Api/WebApp.Api.csproj", "backend/WebApp.Api/"] -COPY ["backend/WebApp.ServiceDefaults/WebApp.ServiceDefaults.csproj", "backend/WebApp.ServiceDefaults/"] - -# Restore dependencies -RUN dotnet restore "backend/WebApp.Api/WebApp.Api.csproj" - -# Copy source code -COPY . . - -# Build project -WORKDIR "/src/backend/WebApp.Api" -RUN dotnet build "WebApp.Api.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "WebApp.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "WebApp.Api.dll"] diff --git a/deployment/docker/frontend.Dockerfile b/deployment/docker/frontend.Dockerfile index 82f1234..f0f86fc 100644 --- a/deployment/docker/frontend.Dockerfile +++ b/deployment/docker/frontend.Dockerfile @@ -4,6 +4,8 @@ FROM node:22-alpine AS frontend-builder # Build arguments for environment variables (required at build time) ARG ENTRA_SPA_CLIENT_ID ARG ENTRA_TENANT_ID +ARG ENTRA_BACKEND_CLIENT_ID="" +ARG APPLICATIONINSIGHTS_FRONTEND_CONNECTION_STRING="" WORKDIR /app/frontend @@ -20,13 +22,15 @@ RUN rm -f .env.local .env.development .env ENV NODE_ENV=production ENV VITE_ENTRA_SPA_CLIENT_ID=$ENTRA_SPA_CLIENT_ID ENV VITE_ENTRA_TENANT_ID=$ENTRA_TENANT_ID +ENV VITE_ENTRA_BACKEND_CLIENT_ID=$ENTRA_BACKEND_CLIENT_ID +ENV VITE_APPLICATIONINSIGHTS_CONNECTION_STRING=$APPLICATIONINSIGHTS_FRONTEND_CONNECTION_STRING # Don't set VITE_API_URL - will default to "/api" (same origin) # Build the frontend RUN npm run build # Stage 2: Build .NET API Backend -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS backend-builder +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS backend-builder WORKDIR /app @@ -45,7 +49,7 @@ COPY backend/ ./backend/ RUN dotnet publish backend/WebApp.Api/WebApp.Api.csproj -c Release -o /app/publish # Stage 3: Runtime - .NET API serving both backend and frontend static files -FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine WORKDIR /app @@ -62,5 +66,8 @@ EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 ENV ASPNETCORE_ENVIRONMENT=Production +# Run as non-root user (built-in 'app' user in ASP.NET alpine images) +USER app + # Start the .NET API (which will also serve frontend static files from wwwroot) ENTRYPOINT ["dotnet", "WebApp.Api.dll"] diff --git a/deployment/hooks/README.md b/deployment/hooks/README.md index c76d3bd..6637118 100644 --- a/deployment/hooks/README.md +++ b/deployment/hooks/README.md @@ -1,40 +1,42 @@ # Azure Developer CLI Hooks -**Context**: See `.github/copilot-instructions.md` and `deployment/AGENTS.md` for overall architecture. +**AI Assistance**: See `.github/skills/deploying-to-azure/SKILL.md` for deployment patterns. ## Hook Execution Order | Phase | Command | Hooks Executed | Duration | |-------|---------|----------------|----------| -| **Deploy** | `azd up` | preprovision → provision → postprovision | 10-12 min | +| **Deploy** | `azd up` | preprovision → provision → postprovision → predeploy | 10-12 min | +| **Code Only** | `azd deploy` | predeploy | 3-5 min | | **Teardown** | `azd down` | (resources deleted) → postdown | 2-3 min | -| **Reprovision** | `azd provision` | preprovision → provision | 2-3 min | +| **Reprovision** | `azd provision` | preprovision → provision → postprovision | 2-3 min | + +## Logging + +All hooks start a PowerShell transcript automatically and write logs to `.azure//logs/` with timestamped filenames (one per hook run). The transcript captures the same console output shown during `azd` execution for post-run troubleshooting. ## Hook Details | Hook | Purpose | Key Actions | Outputs | |------|---------|-------------|---------| -| **preprovision.ps1** | Create Entra app + discover AI Foundry + generate config | • Discovers AI Foundry resources
• Creates Entra SPA app
• Generates `.env` files | `.env` and `.env.local` files | -| **postprovision.ps1** | Build & deploy container | • Updates redirect URIs
• Builds Docker image
• Deploys to Container App | Updated Container App | -| **postdown.ps1** | Cleanup (optional) | • Optionally removes Docker images
• Default: preserves for fast redeploy | Clean slate | +| **preprovision.ps1** | Discover AI Foundry + configure agent | • Discovers AI Foundry resources
• Auto-detects tenant ID
• Discovers agent in project | AI Foundry env vars | +| **postprovision.ps1** | Configure Entra app + RBAC + local config | • Sets `identifierUri` on Entra app (can't be done in Bicep — self-references `appId`)
• Updates redirect URIs with production URL
• Assigns `Cognitive Services OpenAI Contributor` + `Azure AI Developer` roles to AI Foundry
• Generates local dev config files (`.env`, `.env.local`) | Configured Entra app + RBAC + local config | +| **predeploy.ps1** | Build container image | • Detects Docker availability
• Passes `APPLICATIONINSIGHTS_FRONTEND_CONNECTION_STRING` as Docker build arg for frontend browser telemetry
• Local Docker build + push OR ACR cloud build
• Updates Container App if it exists | Container image in ACR | +| **postdown.ps1** | Cleanup (optional) | • Removes RBAC assignment
• Deletes Entra app (Graph resources aren't tied to RGs)
• Optionally removes Docker images | Clean slate | -## Module Scripts +## Entra App Registration -### modules/New-EntraAppRegistration.ps1 +The Entra app is created **declaratively via Bicep** (`infra/entra-app.bicep`) using the Microsoft Graph Bicep extension. -Reusable module for creating Entra ID app registrations with PKCE flow. +**What Bicep handles**: App creation, display name, sign-in audience, SPA redirect URIs (localhost only), `Chat.ReadWrite` scope, service principal, and `serviceManagementReference`. -**Usage**: -```powershell -$clientId = & ".\modules\New-EntraAppRegistration.ps1" ` - -AppName "my-app" ` - -TenantId $tenantId ` - -RedirectUris @("http://localhost:5173") -``` +**What postprovision handles**: `identifierUri` (requires auto-generated `appId`), Container App FQDN redirect URI, and local dev config generation. + +## Module Scripts ### modules/Get-AIFoundryAgents.ps1 -Discovers agents in an Azure AI Foundry project via REST API (`/agents?api-version=2025-11-15-preview`). +Discovers agents in a Microsoft Foundry project via REST API (`/agents?api-version=2025-11-15-preview`). **Usage**: ```powershell @@ -69,11 +71,12 @@ azd up | Issue | Fix | |-------|-----| -| App registration fails with policy error | Set `$env:ENTRA_SERVICE_MANAGEMENT_REFERENCE = 'guid'` (see note below) | -| Preprovision fails | Verify Azure CLI auth: `az account show` | -| Postprovision Docker build fails | Check Docker running: `docker version` | -| AI Foundry not found | Create resource at https://ai.azure.com | -| Multiple AI Foundry resources | Hook will prompt for selection | +| App registration fails with policy error | Run `azd env set ENTRA_SERVICE_MANAGEMENT_REFERENCE 'guid'` then `azd up` (Bicep passes it to Microsoft Graph extension) | +| Provision fails | Verify Azure CLI auth: `az account show` | +| Predeploy Docker build fails | Check Docker running: `docker version` (falls back to ACR cloud build) | +| AI Foundry not found | Create resource at [ai.azure.com](https://ai.azure.com) or use [foundry-samples Bicep templates](https://github.com/microsoft-foundry/foundry-samples/tree/main/infrastructure/infrastructure-setup-bicep) | +| Multiple AI Foundry resources | Set `AI_FOUNDRY_RESOURCE_NAME` or select when prompted | +| RBAC assignment fails | Verify you have User Access Administrator role on AI Foundry resource | ### App Registration Policies @@ -81,10 +84,11 @@ Some organizations require [`serviceManagementReference`](https://learn.microsof **Quick fix**: ```powershell -$env:ENTRA_SERVICE_MANAGEMENT_REFERENCE = 'your-guid-here'; azd up +azd env set ENTRA_SERVICE_MANAGEMENT_REFERENCE 'your-guid-here' +azd up ``` -**Persistent fix**: +**Persistent fix** (environment variable): ```powershell [System.Environment]::SetEnvironmentVariable('ENTRA_SERVICE_MANAGEMENT_REFERENCE', 'your-guid-here', 'User') # Restart terminal @@ -92,6 +96,15 @@ $env:ENTRA_SERVICE_MANAGEMENT_REFERENCE = 'your-guid-here'; azd up Contact your Entra ID admin for the required GUID. +### Multiple AI Foundry Resources + +If you have multiple AI Foundry resources in your subscription, the preprovision hook will prompt you to select one. + +**To skip the prompt**, pre-configure your preferred resource: +```powershell +azd env set AI_FOUNDRY_RESOURCE_NAME "your-ai-foundry-resource-name" +``` + ## Customization ### Change Default Behavior @@ -101,3 +114,7 @@ Contact your Entra ID admin for the required GUID. | Always clean Docker images | `postdown.ps1` | Set `$cleanDockerImages = $true` | | Change ports | `start-local-dev.ps1` + Entra app URIs | Update port references | | Skip auto-opening browser | `postprovision.ps1` | Comment out `Start-Process` line | + +## See Also + +- `.github/hooks/` — Copilot agent hooks (commit gate, custom workflow policies). These are **different** from the azd deployment hooks in this directory. diff --git a/deployment/hooks/modules/Get-AIFoundryAgents.ps1 b/deployment/hooks/modules/Get-AIFoundryAgents.ps1 index 2ef286a..c684547 100644 --- a/deployment/hooks/modules/Get-AIFoundryAgents.ps1 +++ b/deployment/hooks/modules/Get-AIFoundryAgents.ps1 @@ -1,43 +1,10 @@ #!/usr/bin/env pwsh -<# -.SYNOPSIS - Get agents from an Azure AI Foundry project +# Get agents from a Microsoft Foundry project (handles pagination) +# Returns: Array of agent objects with properties: name, id, versions, etc. -.DESCRIPTION - Uses the Azure AI Foundry REST API (v2025-11-15-preview) to enumerate agents in a project. - Handles pagination automatically to retrieve all agents. - -.PARAMETER ProjectEndpoint - The Azure AI Foundry project endpoint (e.g., https://myresource.services.ai.azure.com/api/projects/myproject) - -.PARAMETER AccessToken - Optional. Bearer token for authentication. If not provided, will attempt to get token via Azure CLI. - -.PARAMETER Quiet - Suppress informational output. Only returns agent data or errors. - -.OUTPUTS - Array of agent objects with properties: name, id, versions, etc. - -.EXAMPLE - $agents = & "$PSScriptRoot\modules\Get-AIFoundryAgents.ps1" -ProjectEndpoint $endpoint - -.EXAMPLE - $agents = & "$PSScriptRoot\modules\Get-AIFoundryAgents.ps1" -ProjectEndpoint $endpoint -Quiet - foreach ($agent in $agents) { - Write-Host $agent.name - } -#> - -[CmdletBinding()] param( - [Parameter(Mandatory=$true)] - [string]$ProjectEndpoint, - - [Parameter(Mandatory=$false)] + [Parameter(Mandatory=$true)][string]$ProjectEndpoint, [string]$AccessToken, - - [Parameter(Mandatory=$false)] [switch]$Quiet ) @@ -45,80 +12,37 @@ $ErrorActionPreference = 'Stop' # Get access token if not provided if ([string]::IsNullOrWhiteSpace($AccessToken)) { - if (-not $Quiet) { - Write-Host "Getting access token..." -ForegroundColor Cyan - } - $tokenData = az account get-access-token --resource 'https://ai.azure.com' 2>&1 | Out-String - - if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to get access token. Ensure you're logged in with 'az login'" - exit 1 - } - + if ($LASTEXITCODE -ne 0) { throw "Failed to get access token" } $AccessToken = ($tokenData | ConvertFrom-Json).accessToken - - if (-not $Quiet) { - Write-Host "[OK] Authenticated" -ForegroundColor Green - } } # List agents with pagination -if (-not $Quiet) { - Write-Host "Discovering agents in project..." -ForegroundColor Cyan -} - $allAgents = @() $afterCursor = $null $hasMore = $true -$pageCount = 0 -try { - while ($hasMore) { - $pageCount++ - $url = "$ProjectEndpoint/agents?api-version=2025-11-15-preview" - if ($afterCursor) { - $encodedCursor = [System.Web.HttpUtility]::UrlEncode($afterCursor) - $url += "&after=$encodedCursor" - } - - $response = curl --request GET --url $url ` - -H "Authorization: Bearer $AccessToken" ` - -H "Content-Type: application/json" ` - --silent --show-error 2>&1 - - if ($LASTEXITCODE -ne 0) { - Write-Error "API request failed: $response" - exit 1 - } - - $agentsData = ($response | ConvertFrom-Json) - - if ($agentsData.data) { - $allAgents += $agentsData.data - } - - # Check pagination - $hasMore = $agentsData.has_more -eq $true - $afterCursor = $agentsData.last_id - - if ($hasMore -and -not $Quiet) { - Write-Host " Fetching page $($pageCount + 1)..." -ForegroundColor Gray - } +while ($hasMore) { + $url = "$ProjectEndpoint/agents?api-version=2025-11-15-preview" + if ($afterCursor) { + $url += "&after=$([System.Web.HttpUtility]::UrlEncode($afterCursor))" } - if (-not $Quiet) { - if ($allAgents.Count -eq 0) { - Write-Host "[!] No agents found in project" -ForegroundColor Yellow - } else { - Write-Host "[OK] Found $($allAgents.Count) agent(s)" -ForegroundColor Green - } + try { + $response = Invoke-RestMethod -Uri $url -Headers @{ Authorization = "Bearer $AccessToken" } + } catch { + throw "API request failed: $_" } - # Return agents array - return $allAgents - -} catch { - Write-Error "Failed to list agents: $_" - exit 1 + if ($response.data) { $allAgents += $response.data } + $hasMore = $response.has_more -eq $true + $afterCursor = $response.last_id +} + +if (-not $Quiet) { + $count = $allAgents.Count + if ($count -eq 0) { Write-Host "[!] No agents found" -ForegroundColor Yellow } + else { Write-Host "[OK] Found $count agent(s)" -ForegroundColor Green } } + +return $allAgents diff --git a/deployment/hooks/modules/HookLogging.ps1 b/deployment/hooks/modules/HookLogging.ps1 new file mode 100644 index 0000000..565ddaf --- /dev/null +++ b/deployment/hooks/modules/HookLogging.ps1 @@ -0,0 +1,40 @@ +function Start-HookLog { + param( + [Parameter(Mandatory = $true)][string]$HookName, + [string]$EnvironmentName + ) + + $script:HookTranscriptStarted = $false + $script:HookLogFile = $null + + $envName = $EnvironmentName + if ([string]::IsNullOrWhiteSpace($envName)) { + $envName = $env:AZURE_ENV_NAME + } + if ([string]::IsNullOrWhiteSpace($envName) -and (Get-Command azd -EA SilentlyContinue)) { + try { + $envName = (azd env get-value AZURE_ENV_NAME 2>$null | Select-Object -First 1) + } catch { } + } + if ([string]::IsNullOrWhiteSpace($envName)) { $envName = "default" } + + $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path + $logDir = Join-Path (Join-Path (Join-Path $projectRoot ".azure") $envName) "logs" + + try { + if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } + $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" + $script:HookLogFile = Join-Path $logDir "$timestamp-$HookName.log" + Start-Transcript -Path $script:HookLogFile -Append | Out-Null + $script:HookTranscriptStarted = $true + Write-Host "[LOG] Capturing output to $script:HookLogFile" -ForegroundColor DarkGray + } catch { + Write-Host "[WARN] Could not start transcript: $($_.Exception.Message)" -ForegroundColor Yellow + } +} + +function Stop-HookLog { + if ($script:HookTranscriptStarted) { + try { Stop-Transcript | Out-Null } catch { } + } +} \ No newline at end of file diff --git a/deployment/hooks/modules/New-EntraAppRegistration.ps1 b/deployment/hooks/modules/New-EntraAppRegistration.ps1 deleted file mode 100644 index 3524918..0000000 --- a/deployment/hooks/modules/New-EntraAppRegistration.ps1 +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS -Creates or updates an Entra ID app registration for AI Foundry Agent application. - -.PARAMETER AppName -The display name for the app registration. - -.PARAMETER TenantId -The Entra ID tenant ID. - -.PARAMETER FrontendUrl -The frontend URL for SPA redirect URI. - -.PARAMETER ServiceManagementReference -Optional property for organizations with custom app registration policies. -Set via environment variable: $env:ENTRA_SERVICE_MANAGEMENT_REFERENCE = "your-guid" -If your organization requires this field, the error message will indicate what's needed. -Contact your Entra ID administrator for the required value. - -.LINK -https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.applications/invoke-mginstantiateapplicationtemplate?view=graph-powershell-1.0#-servicemanagementreference - -.LINK -https://learn.microsoft.com/en-us/graph/api/applicationtemplate-instantiate?view=graph-rest-1.0 - -.OUTPUTS -Returns the client ID of the created/updated app registration. -#> - -param( - [Parameter(Mandatory=$true)] - [string]$AppName, - - [Parameter(Mandatory=$true)] - [string]$TenantId, - - [Parameter(Mandatory=$false)] - [string]$FrontendUrl = "http://localhost:8080", - - [Parameter(Mandatory=$false)] - [string]$ServiceManagementReference = $null -) - -Write-Host "Checking for existing app registration: $AppName" -ForegroundColor Cyan - -# Check if app already exists -$existingApp = az ad app list --display-name $AppName --query "[0]" | ConvertFrom-Json - -if ($existingApp) { - Write-Host "[OK] Found existing app registration: $($existingApp.appId)" -ForegroundColor Green - $appId = $existingApp.appId -} else { - Write-Host "Creating new app registration: $AppName" -ForegroundColor Yellow - - # Build app body based on whether serviceManagementReference is provided - $appBody = @{ - displayName = $AppName - signInAudience = "AzureADMyOrg" - } - - # Add serviceManagementReference if provided (for organizations with custom policies) - if (-not [string]::IsNullOrWhiteSpace($ServiceManagementReference)) { - Write-Host "Using Service Management Reference: $ServiceManagementReference" -ForegroundColor Gray - $appBody.serviceManagementReference = $ServiceManagementReference - } - - $appBodyJson = $appBody | ConvertTo-Json - - # Save to temp file to avoid PowerShell quoting issues - $tempFile = [System.IO.Path]::GetTempFileName() - $appBodyJson | Out-File -FilePath $tempFile -Encoding utf8 - - $createResult = az rest --method POST ` - --uri "https://graph.microsoft.com/v1.0/applications" ` - --headers "Content-Type=application/json" ` - --body "@$tempFile" ` - 2>&1 - - Remove-Item $tempFile -ErrorAction SilentlyContinue - - if ($LASTEXITCODE -ne 0) { - $errorMessage = $createResult -join "`n" - Write-Host "" - Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Red - Write-Host "App Registration Failed" -ForegroundColor Red - Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Red - Write-Host "" - Write-Host "ERROR: Failed to create Entra ID app registration." -ForegroundColor Red - Write-Host "" - Write-Host "Error from Microsoft Graph API:" -ForegroundColor Yellow - Write-Host $errorMessage - Write-Host "" - Write-Host "Common Solutions:" -ForegroundColor Yellow - Write-Host "" - Write-Host "1. If your organization requires a Service Management Reference:" -ForegroundColor Cyan - Write-Host " • Contact your Entra ID administrator to get the required GUID" -ForegroundColor Gray - Write-Host " • Run azd up with the environment variable set:" -ForegroundColor Gray - Write-Host " `$env:ENTRA_SERVICE_MANAGEMENT_REFERENCE = ''; azd up" -ForegroundColor White - Write-Host " • Or set persistently: [Environment]::SetEnvironmentVariable('ENTRA_SERVICE_MANAGEMENT_REFERENCE', '', 'User')" -ForegroundColor Gray - Write-Host "" - Write-Host "2. If your organization has other custom policies:" -ForegroundColor Cyan - Write-Host " • Review the error message above for specific requirements" -ForegroundColor Gray - Write-Host " • Contact your Entra ID administrator" -ForegroundColor Gray - Write-Host " • You may need to manually create the app registration" -ForegroundColor Gray - Write-Host "" - Write-Host "For more details, see: deployment/hooks/README.md" -ForegroundColor Gray - Write-Host "" - throw "App registration creation failed" - } - - $appJson = $createResult | ConvertFrom-Json - $appId = $appJson.appId - $objectId = $appJson.id - - if (-not $appId) { - Write-Error "App registration created but client ID is empty" - throw "Invalid app registration" - } - - Write-Host "[OK] Created app registration: $appId" -ForegroundColor Green -} - -# Get app object ID (needed for updates) -$app = az ad app show --id $appId | ConvertFrom-Json -$objectId = $app.id - -Write-Host "Configuring app registration..." -ForegroundColor Cyan - -# Configure SPA redirect URIs -# Default: -# - http://localhost:5173 (Vite dev server - native development with hot reload) -# - http://localhost:8080 (Backend API - flexibility) -$redirectUris = @( - "http://localhost:5173", - "http://localhost:8080" -) - -if ($FrontendUrl -and $FrontendUrl -ne "http://localhost:5173" -and $FrontendUrl -ne "http://localhost:8080") { - $redirectUris += $FrontendUrl -} - -$spaBody = @{ - spa = @{ - redirectUris = $redirectUris - } -} | ConvertTo-Json -Depth 10 - -$tempFile = [System.IO.Path]::GetTempFileName() -$spaBody | Out-File -FilePath $tempFile -Encoding utf8 - -az rest --method PATCH ` - --uri "https://graph.microsoft.com/v1.0/applications/$objectId" ` - --headers "Content-Type=application/json" ` - --body "@$tempFile" ` - | Out-Null - -Remove-Item $tempFile -ErrorAction SilentlyContinue - -Write-Host "[OK] Configured SPA redirect URIs" -ForegroundColor Green - -# Set identifier URI and expose API scope -$identifierUri = "api://$appId" - -# Check if scope already exists -$existingScope = $app.api.oauth2PermissionScopes | Where-Object { $_.value -eq "Chat.ReadWrite" } - -if ($existingScope) { - # Scope exists - just update identifier URI if needed - Write-Host "[OK] API scope already exists: Chat.ReadWrite" -ForegroundColor Green - - if ($app.identifierUris -notcontains $identifierUri) { - $apiBody = @{ - identifierUris = @($identifierUri) - } | ConvertTo-Json -Depth 10 - - $tempFile = [System.IO.Path]::GetTempFileName() - $apiBody | Out-File -FilePath $tempFile -Encoding utf8 - - az rest --method PATCH ` - --uri "https://graph.microsoft.com/v1.0/applications/$objectId" ` - --headers "Content-Type=application/json" ` - --body "@$tempFile" ` - 2>&1 | Out-Null - - Remove-Item $tempFile -ErrorAction SilentlyContinue - Write-Host "[OK] Set identifier URI: $identifierUri" -ForegroundColor Green - } else { - Write-Host "[OK] Identifier URI already set: $identifierUri" -ForegroundColor Green - } -} else { - # Create new scope - $apiBody = @{ - identifierUris = @($identifierUri) - api = @{ - oauth2PermissionScopes = @( - @{ - adminConsentDescription = "Allows the app to read and write chat messages on behalf of the signed-in user" - adminConsentDisplayName = "Read and write chat messages" - id = (New-Guid).Guid - isEnabled = $true - type = "User" - userConsentDescription = "Allows the app to read and write your chat messages" - userConsentDisplayName = "Read and write your chat messages" - value = "Chat.ReadWrite" - } - ) - } - } | ConvertTo-Json -Depth 10 - - $tempFile = [System.IO.Path]::GetTempFileName() - $apiBody | Out-File -FilePath $tempFile -Encoding utf8 - - az rest --method PATCH ` - --uri "https://graph.microsoft.com/v1.0/applications/$objectId" ` - --headers "Content-Type=application/json" ` - --body "@$tempFile" ` - 2>&1 | Out-Null - - Remove-Item $tempFile -ErrorAction SilentlyContinue - - Write-Host "[OK] Set identifier URI: $identifierUri" -ForegroundColor Green - Write-Host "[OK] Exposed API scope: Chat.ReadWrite" -ForegroundColor Green -} - -Write-Host "" -Write-Host "App Registration Summary:" -ForegroundColor Cyan -Write-Host " Display Name: $AppName" -ForegroundColor White -Write-Host " Client ID: $appId" -ForegroundColor White -Write-Host " Tenant ID: $TenantId" -ForegroundColor White -Write-Host " Identifier URI: $identifierUri" -ForegroundColor White -Write-Host " API Scope: api://$appId/Chat.ReadWrite" -ForegroundColor White -Write-Host "" - -# Return client ID for azd environment -return $appId diff --git a/deployment/hooks/postdown.ps1 b/deployment/hooks/postdown.ps1 index 317f602..8c32652 100644 --- a/deployment/hooks/postdown.ps1 +++ b/deployment/hooks/postdown.ps1 @@ -1,163 +1,83 @@ #!/usr/bin/env pwsh -<# -.SYNOPSIS - Cleanup hook after azd down +# Post-down: Cleanup Entra app, role assignments, and local config after azd down -.DESCRIPTION - This hook runs after Azure resources are deleted by azd down. - - By default, Docker images are PRESERVED to speed up redeployment. - To clean Docker images, set environment variable: - $env:CLEAN_DOCKER_IMAGES = "true" - azd down --force --purge - -.EXAMPLE - # Normal teardown (preserves Docker images) - azd down --force --purge - -.EXAMPLE - # Full cleanup (removes Docker images too) - $env:CLEAN_DOCKER_IMAGES = "true" - azd down --force --purge -#> - -$ErrorActionPreference = "Stop" - -Write-Host "`n=====================================" -ForegroundColor Cyan -Write-Host " Post-Down Cleanup" -ForegroundColor Cyan -Write-Host "=====================================" -ForegroundColor Cyan +$ErrorActionPreference = "Continue" +. "$PSScriptRoot/modules/HookLogging.ps1" +Start-HookLog -HookName "postdown" -EnvironmentName $env:AZURE_ENV_NAME -# --- Get environment name --- +Write-Host "Post-Down Cleanup" -ForegroundColor Cyan $envName = $env:AZURE_ENV_NAME -if (-not $envName) { - Write-Host "[!] Warning: AZURE_ENV_NAME not set, skipping some cleanup steps" -ForegroundColor Yellow -} -# --- Delete Entra App Registration --- +# Remove role assignment from AI Foundry resource (if it exists) +$webIdentityPrincipalId = (azd env get-value WEB_IDENTITY_PRINCIPAL_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 +$aiFoundryResourceGroup = (azd env get-value AI_FOUNDRY_RESOURCE_GROUP 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 +$aiFoundryResourceName = (azd env get-value AI_FOUNDRY_RESOURCE_NAME 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 +$subscriptionId = (azd env get-value AZURE_SUBSCRIPTION_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 -if ($envName) { - Write-Host "`nCleaning up Entra app registration..." -ForegroundColor Cyan +if ($webIdentityPrincipalId -and $aiFoundryResourceGroup -and $aiFoundryResourceName -and $subscriptionId) { + Write-Host "Removing role assignment from AI Foundry resource..." -ForegroundColor Yellow + + $scope = "/subscriptions/$subscriptionId/resourceGroups/$aiFoundryResourceGroup/providers/Microsoft.CognitiveServices/accounts/$aiFoundryResourceName" - try { - # Try to get the client ID from the environment - $clientId = azd env get-value ENTRA_SPA_CLIENT_ID 2>$null + $roles = @("Cognitive Services User", "Cognitive Services OpenAI Contributor", "Azure AI Developer") + foreach ($roleName in $roles) { + az role assignment delete ` + --assignee $webIdentityPrincipalId ` + --role $roleName ` + --scope $scope 2>&1 | Out-Null - if ($clientId) { - Write-Host " Found app registration: $clientId" -ForegroundColor Gray - az ad app delete --id $clientId 2>&1 | Out-Null - Write-Host " [OK] Entra app registration deleted" -ForegroundColor Green - } - else { - Write-Host " No ENTRA_SPA_CLIENT_ID found in environment" -ForegroundColor Gray - } + Write-Host "[OK] $roleName — removed (if it existed)" -ForegroundColor Green } - catch { - Write-Host " [!] Could not delete Entra app registration" -ForegroundColor Yellow - Write-Host " $_" -ForegroundColor Gray - } -} - -# --- Delete local configuration files --- - -Write-Host "`nCleaning up local configuration files..." -ForegroundColor Cyan - -$envLocalPath = Join-Path $PSScriptRoot ".." "frontend" ".env.local" -if (Test-Path $envLocalPath) { - Remove-Item $envLocalPath -Force - Write-Host " [OK] Deleted frontend/.env.local" -ForegroundColor Green } -else { - Write-Host " frontend/.env.local not found" -ForegroundColor Gray -} - -# Clean up backend configuration -$backendEnvPath = Join-Path $PSScriptRoot ".." "backend" "WebApp.Api" ".env" -if (Test-Path $backendEnvPath) { - Remove-Item $backendEnvPath -Force - Write-Host " [OK] Deleted backend/WebApp.Api/.env" -ForegroundColor Green -} -else { - Write-Host " backend/WebApp.Api/.env not found" -ForegroundColor Gray -} - -# --- Delete azd environment folder --- +# Delete Entra app (Graph resources are NOT tied to Azure resource groups — azd down won't clean them up) if ($envName) { - Write-Host "`nCleaning up azd environment..." -ForegroundColor Cyan - - $envFolder = Join-Path $PSScriptRoot ".." ".azure" $envName - if (Test-Path $envFolder) { - Remove-Item $envFolder -Recurse -Force - Write-Host " [OK] Deleted .azure/$envName environment folder" -ForegroundColor Green + $clientId = (azd env get-value ENTRA_SPA_CLIENT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 + $deleted = $false + if (-not [string]::IsNullOrWhiteSpace($clientId)) { + az ad app delete --id $clientId 2>&1 | Out-Null + Write-Host "[OK] Entra app deleted: $clientId" -ForegroundColor Green + $deleted = $true } - else { - Write-Host " .azure/$envName not found" -ForegroundColor Gray + if (-not $deleted) { + # Fallback: look up by display name (matches Bicep uniqueName pattern) + $appName = "ai-foundry-agent-$envName" + $app = az ad app list --display-name $appName --query "[0].appId" -o tsv 2>$null + if ($app) { + az ad app delete --id $app 2>&1 | Out-Null + Write-Host "[OK] Entra app deleted (by name): $appName" -ForegroundColor Green + } } } -# --- Check if we should clean Docker images --- - -$cleanDockerImages = $env:CLEAN_DOCKER_IMAGES -eq "true" - -if ($cleanDockerImages) { - Write-Host "`nCleaning Docker images..." -ForegroundColor Yellow - - $images = docker images "ai-foundry-agent/*" -q - if ($images) { - $imageCount = ($images | Measure-Object).Count - Write-Host "Found $imageCount Docker image(s) to remove" -ForegroundColor Gray - - $images | ForEach-Object { - try { - docker rmi $_ -f - } - catch { - Write-Host "[!] Failed to remove image $_" -ForegroundColor Yellow - } - } - - Write-Host "[OK] Docker images removed" -ForegroundColor Green - } - else { - Write-Host "No Docker images found to clean" -ForegroundColor Gray +# Delete local config files +$projectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +@( + "frontend/.env.local", + "backend/WebApp.Api/.env" +) | ForEach-Object { + $path = Join-Path $projectRoot $_ + if (Test-Path $path) { + Remove-Item $path -Force + Write-Host "[OK] Deleted $_" -ForegroundColor Green } } -else { - Write-Host "`nDocker images PRESERVED (faster redeployment)" -ForegroundColor Green - - $images = docker images "ai-foundry-agent/*" -q - if ($images) { - $imageCount = ($images | Measure-Object).Count - Write-Host " Preserved $imageCount Docker image(s)" -ForegroundColor Gray - Write-Host "`nTo clean Docker images next time:" -ForegroundColor Yellow - Write-Host ' $env:CLEAN_DOCKER_IMAGES = "true"' -ForegroundColor White - Write-Host " azd down --force --purge" -ForegroundColor White + +# Delete azd environment folder (stop logging first to release file lock) +if ($envName) { + $envFolder = Join-Path $projectRoot ".azure" $envName + if (Test-Path $envFolder) { + Stop-HookLog # Release log file before deleting folder + Remove-Item $envFolder -Recurse -Force + Write-Host "[OK] Deleted .azure/$envName" -ForegroundColor Green } } -# --- Check preserved artifacts --- - -Write-Host "`nPreserved local development artifacts:" -ForegroundColor Cyan - -$nodeModulesPath = Join-Path $PSScriptRoot ".." "frontend" "node_modules" -if (Test-Path $nodeModulesPath) { - Write-Host " [OK] frontend/node_modules (no need to reinstall)" -ForegroundColor Gray +# Optional Docker cleanup +if ($env:CLEAN_DOCKER_IMAGES -eq "true" -and (Get-Command docker -EA SilentlyContinue)) { + docker images --filter "reference=*azurecr.io/web:*" -q | ForEach-Object { docker rmi $_ -f 2>$null } + Write-Host "[OK] Docker images cleaned" -ForegroundColor Green } -# --- Success Message --- - -Write-Host "`n=====================================" -ForegroundColor Green -Write-Host " Cleanup Complete!" -ForegroundColor Green -Write-Host "=====================================" -ForegroundColor Green -Write-Host "`nWhat was cleaned:" -ForegroundColor White -Write-Host " [OK] Azure resources (resource group)" -ForegroundColor Gray -Write-Host " [OK] Entra app registration" -ForegroundColor Gray -Write-Host " [OK] Local configuration files (.env.local, .env)" -ForegroundColor Gray -Write-Host " [OK] azd environment folder (.azure/$envName)" -ForegroundColor Gray -Write-Host "`nWhat was preserved:" -ForegroundColor White -Write-Host " [OK] Node modules (faster setup)" -ForegroundColor Gray -Write-Host " [OK] Docker images (faster redeployment)" -ForegroundColor Gray -Write-Host "`nTo redeploy:" -ForegroundColor Yellow -Write-Host " azd up" -ForegroundColor White -Write-Host "`n" +Write-Host "[OK] Cleanup complete. Run 'azd up' to redeploy." -ForegroundColor Green diff --git a/deployment/hooks/postprovision.ps1 b/deployment/hooks/postprovision.ps1 index 77bc9e2..37ec8e4 100644 --- a/deployment/hooks/postprovision.ps1 +++ b/deployment/hooks/postprovision.ps1 @@ -1,181 +1,217 @@ #!/usr/bin/env pwsh +# Post-provision: Configures Entra app (identifierUri + redirect URIs), assigns RBAC, generates local dev config +# The Entra app itself is created declaratively by Bicep (infra/entra-app.bicep) -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "Post-Provision: Build & Deploy Container" -ForegroundColor Cyan -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "" +$ErrorActionPreference = "Stop" +. "$PSScriptRoot/modules/HookLogging.ps1" +Start-HookLog -HookName "postprovision" -EnvironmentName $env:AZURE_ENV_NAME -# Get environment variables from azd -$envName = azd env get-value AZURE_ENV_NAME 2>$null -if (-not $envName) { - # Fallback to environment variable for backward compatibility - $envName = $env:AZURE_ENV_NAME -} +Write-Host "Post-Provision: Configure Entra App, RBAC & Local Config" -ForegroundColor Cyan +# Get required env vars (ENTRA_SPA_CLIENT_ID is now a Bicep output) $clientId = azd env get-value ENTRA_SPA_CLIENT_ID 2>$null -if (-not $clientId) { - $clientId = $env:ENTRA_SPA_CLIENT_ID -} - +$containerAppUrl = azd env get-value WEB_ENDPOINT 2>$null +$webIdentityPrincipalId = azd env get-value WEB_IDENTITY_PRINCIPAL_ID 2>$null +$aiFoundryResourceGroup = azd env get-value AI_FOUNDRY_RESOURCE_GROUP 2>$null +$aiFoundryResourceName = azd env get-value AI_FOUNDRY_RESOURCE_NAME 2>$null +$subscriptionId = azd env get-value AZURE_SUBSCRIPTION_ID 2>$null $tenantId = azd env get-value ENTRA_TENANT_ID 2>$null -if (-not $tenantId) { - $tenantId = $env:ENTRA_TENANT_ID -} - -$resourceGroup = azd env get-value AZURE_RESOURCE_GROUP_NAME 2>$null -$containerApp = azd env get-value AZURE_CONTAINER_APP_NAME 2>$null -$acrName = azd env get-value AZURE_CONTAINER_REGISTRY_NAME 2>$null - -if (-not $envName) { - Write-Error "AZURE_ENV_NAME not set" - exit 1 -} if (-not $clientId) { - Write-Error "ENTRA_SPA_CLIENT_ID not set. App registration may have failed." - exit 1 -} - -if (-not $resourceGroup) { - Write-Error "AZURE_RESOURCE_GROUP_NAME not set. Infrastructure may not be deployed." + Write-Host "[ERROR] ENTRA_SPA_CLIENT_ID not set (should be output from Bicep)" -ForegroundColor Red exit 1 } - -Write-Host "Environment: $envName" -ForegroundColor Green -Write-Host "Client ID: $clientId" -ForegroundColor Green -Write-Host "Resource Group: $resourceGroup" -ForegroundColor Green -Write-Host "" - -# Step 1: Get Container App URL -Write-Host "Step 1: Getting Container App URL..." -ForegroundColor Cyan - -$containerAppUrl = azd env get-value WEB_ENDPOINT 2>$null if (-not $containerAppUrl) { - $containerAppUrl = $env:WEB_ENDPOINT -} - -if (-not $containerAppUrl) { - Write-Error "WEB_ENDPOINT not set" + Write-Host "[ERROR] WEB_ENDPOINT not set" -ForegroundColor Red exit 1 } -Write-Host "[OK] Container App URL: $containerAppUrl" -ForegroundColor Green -Write-Host "" - -# Step 2: Update Entra App Registration with redirect URI -Write-Host "Step 2: Updating Entra App Registration redirect URIs..." -ForegroundColor Cyan +Write-Host "[OK] Client ID: $clientId (from Bicep)" -ForegroundColor Green +Write-Host "[OK] Container App: $containerAppUrl" -ForegroundColor Green +# Set identifierUri and update redirect URIs on Entra app +# identifierUri can't be set in Bicep because it references the auto-generated appId $app = az ad app show --id $clientId | ConvertFrom-Json $objectId = $app.id +$identifierUri = "api://$clientId" -# Build redirect URIs array including localhost and deployed URL -$redirectUris = @( - "http://localhost:8080", # Local Docker Compose (production-identical) - "http://localhost:5173", # Local Vite dev server (hot reload) - $containerAppUrl # Azure Container App (production) -) - -# Update SPA configuration using Microsoft Graph API -$spaBody = @{ +$patchBody = @{ + identifierUris = @($identifierUri) spa = @{ - redirectUris = $redirectUris + redirectUris = @( + "http://localhost:8080", + "http://localhost:5173", + $containerAppUrl + ) } } | ConvertTo-Json -Depth 10 $tempFile = [System.IO.Path]::GetTempFileName() -$spaBody | Out-File -FilePath $tempFile -Encoding utf8 +$patchBody | Out-File -FilePath $tempFile -Encoding utf8 az rest --method PATCH ` --uri "https://graph.microsoft.com/v1.0/applications/$objectId" ` --headers "Content-Type=application/json" ` - --body "@$tempFile" ` - | Out-Null + --body "@$tempFile" | Out-Null -Remove-Item $tempFile -ErrorAction SilentlyContinue +Remove-Item $tempFile -EA SilentlyContinue if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to update Entra app registration" + Write-Host "[ERROR] Failed to update Entra app" -ForegroundColor Red exit 1 } -Write-Host "[OK] Updated redirect URIs:" -ForegroundColor Green -foreach ($uri in $redirectUris) { - Write-Host " - $uri" -ForegroundColor White -} -Write-Host "" - -# Step 3: Build and deploy container using shared module -$scriptPath = Join-Path (Split-Path $PSScriptRoot -Parent) "scripts\build-and-deploy-container.ps1" - -try { - # Don't overwrite $containerAppUrl - it's already set from WEB_ENDPOINT - & $scriptPath ` - -ClientId $clientId ` - -TenantId $tenantId ` - -ResourceGroup $resourceGroup ` - -ContainerApp $containerApp ` - -AcrName $acrName | Out-Null +Write-Host "[OK] Identifier URI: $identifierUri" -ForegroundColor Green +Write-Host "[OK] Redirect URIs updated" -ForegroundColor Green + +# OBO: Bicep creates backend app registration + service principal + requiredResourceAccess. +# FIC + identifierUri + admin consent are handled here (not Bicep) because Graph API +# eventual consistency causes child resources to fail when the parent app hasn't replicated yet. +$backendClientId = azd env get-value ENTRA_BACKEND_CLIENT_ID 2>$null +if ($backendClientId) { + Write-Host "OBO: Configuring backend app ($backendClientId)..." -ForegroundColor Yellow + + $backendObjectId = az ad app show --id $backendClientId --query "id" -o tsv 2>$null + # Set identifierUri on backend app (required for scope resolution) + az ad app update --id $backendClientId --identifier-uris "api://$backendClientId" 2>$null if ($LASTEXITCODE -ne 0) { - Write-Error "Container deployment failed" + Write-Host "[ERROR] Failed to set identifierUri on backend app — OBO will not work" -ForegroundColor Red exit 1 } + + # Create Federated Identity Credential (MI → backend app, secretless OBO) + $existingFic = az ad app federated-credential list --id $backendObjectId --query "[?name=='container-app-mi-fic']" 2>$null | ConvertFrom-Json + if ($existingFic -and $existingFic.Count -gt 0) { + Write-Host "[OK] FIC already exists" -ForegroundColor Green + } else { + $ficBody = @{ + name = "container-app-mi-fic" + issuer = "https://login.microsoftonline.com/$tenantId/v2.0" + subject = $webIdentityPrincipalId + audiences = @("api://AzureADTokenExchange") + description = "User-assigned managed identity for secretless OBO" + } | ConvertTo-Json + $ficFile = [System.IO.Path]::GetTempFileName() + try { + $ficBody | Out-File -FilePath $ficFile -Encoding utf8 + az ad app federated-credential create --id $backendObjectId --parameters $ficFile 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "[ERROR] FIC creation failed — OBO will not work without it" -ForegroundColor Red + Write-Host " Create manually: az ad app federated-credential create --id $backendObjectId --parameters " + exit 1 + } + Write-Host "[OK] FIC created (MI → backend app)" -ForegroundColor Green + } finally { + Remove-Item $ficFile -ErrorAction SilentlyContinue + } + } + + # Grant admin consent (best-effort — may require Entra admin) + $backendSpId = az ad sp show --id $backendClientId --query "id" -o tsv 2>$null + $spaSpId = az ad sp show --id $clientId --query "id" -o tsv 2>$null + if ($backendSpId -and $spaSpId) { + $existingConsent = az rest --method GET --url "https://graph.microsoft.com/v1.0/oauth2PermissionGrants?`$filter=clientId eq '$spaSpId' and resourceId eq '$backendSpId'" --query "value[0].id" -o tsv 2>$null + if (-not $existingConsent) { + $consentBody = @{ clientId = $spaSpId; consentType = "AllPrincipals"; resourceId = $backendSpId; scope = "Chat.ReadWrite" } | ConvertTo-Json + $consentFile = [System.IO.Path]::GetTempFileName() + try { + $consentBody | Out-File -FilePath $consentFile -Encoding utf8 + az rest --method POST --url "https://graph.microsoft.com/v1.0/oauth2PermissionGrants" --body "@$consentFile" --headers "Content-Type=application/json" 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Host "[OK] Admin consent granted" -ForegroundColor Green + } else { + Write-Host "[WARN] Admin consent failed — an Entra admin must grant consent" -ForegroundColor Yellow + Write-Host " az ad app permission admin-consent --id $backendClientId" + } + } finally { + Remove-Item $consentFile -ErrorAction SilentlyContinue + } + } else { + Write-Host "[OK] Admin consent already exists" -ForegroundColor Green + } + } + + Write-Host "[OK] OBO configuration complete" -ForegroundColor Green } -catch { - Write-Error "Container deployment failed: $_" - exit 1 -} - -# Step 4: Verify deployment -Write-Host "Step 4: Verifying deployment..." -ForegroundColor Cyan -$response = Invoke-WebRequest -Uri "$containerAppUrl" -Method Get -UseBasicParsing -TimeoutSec 10 -ErrorAction SilentlyContinue - -if ($response -and $response.StatusCode -eq 200) { - Write-Host "[OK] Application responded successfully" -ForegroundColor Green -} elseif ($response) { - Write-Warning "Application responded with status: $($response.StatusCode)" +# Assign RBAC roles to web managed identity on AI Foundry resource +# - Cognitive Services User: wildcard data action (covers AIServices/agents/*) +# - Cognitive Services OpenAI Contributor: model access, conversations (OpenAI/*) +# - Azure AI Developer: v2 agents API (SpeechServices, ContentSafety, MaaS) +# Done via CLI (not Bicep) to prevent azd from tracking the external resource group +if ($webIdentityPrincipalId -and $aiFoundryResourceGroup -and $aiFoundryResourceName -and $subscriptionId) { + Write-Host "Assigning AI Foundry RBAC roles to web app identity..." -ForegroundColor Yellow + + $scope = "/subscriptions/$subscriptionId/resourceGroups/$aiFoundryResourceGroup/providers/Microsoft.CognitiveServices/accounts/$aiFoundryResourceName" + + $roles = @("Cognitive Services User", "Cognitive Services OpenAI Contributor", "Azure AI Developer") + foreach ($roleName in $roles) { + $existingAssignment = az role assignment list ` + --assignee $webIdentityPrincipalId ` + --role $roleName ` + --scope $scope 2>$null | ConvertFrom-Json + + if ($existingAssignment -and $existingAssignment.Count -gt 0) { + Write-Host "[OK] $roleName — already assigned" -ForegroundColor Green + } else { + az role assignment create ` + --assignee-object-id $webIdentityPrincipalId ` + --assignee-principal-type ServicePrincipal ` + --role $roleName ` + --scope $scope | Out-Null + + if ($LASTEXITCODE -eq 0) { + Write-Host "[OK] $roleName — assigned" -ForegroundColor Green + } else { + Write-Host "[WARN] $roleName — failed (you may need to assign manually)" -ForegroundColor Yellow + } + } + } } else { - Write-Warning "Application request failed - no response received" + Write-Host "[SKIP] AI Foundry role assignment - missing configuration" -ForegroundColor Yellow + Write-Host " Set AI_FOUNDRY_RESOURCE_GROUP and AI_FOUNDRY_RESOURCE_NAME environment variables" -ForegroundColor Gray } -Write-Host "" -Write-Host "========================================" -ForegroundColor Green -Write-Host "Post-Provision Complete!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "" -Write-Host "Application URL: $containerAppUrl" -ForegroundColor Cyan -Write-Host "Client ID: $clientId" -ForegroundColor Cyan -Write-Host "" - -# Step 5: Open browser to ACA URL -Write-Host "Step 5: Opening browser to deployed application..." -ForegroundColor Cyan - -if ($containerAppUrl) { - try { - Start-Process $containerAppUrl - Write-Host "[OK] Browser opened to: $containerAppUrl" -ForegroundColor Green - } - catch { - Write-Host "[!] Could not open browser automatically" -ForegroundColor Yellow - Write-Host " Open manually: $containerAppUrl" -ForegroundColor Gray - } +# Generate local dev config files (moved from preprovision — clientId comes from Bicep) +$aiAgentEndpoint = azd env get-value AI_AGENT_ENDPOINT 2>$null +$aiAgentId = azd env get-value AI_AGENT_ID 2>$null +$aiAgentVersion = azd env get-value AI_AGENT_VERSION 2>$null + +# Frontend .env.local +$frontendEnv = @" +# Auto-generated - Do not commit +VITE_ENTRA_SPA_CLIENT_ID=$clientId +VITE_ENTRA_TENANT_ID=$tenantId +"@ +if ($backendClientId) { + $frontendEnv += "`nVITE_ENTRA_BACKEND_CLIENT_ID=$backendClientId" } -else { - Write-Host "[!] Container App URL not found" -ForegroundColor Yellow +$frontendEnv | Out-File -FilePath "frontend/.env.local" -Encoding utf8 -Force + +# Backend .env +$backendEnvContent = @" +# Auto-generated - Do not commit +AzureAd__Instance=https://login.microsoftonline.com/ +AzureAd__TenantId=$tenantId +AzureAd__ClientId=$clientId +AzureAd__Audience=api://$clientId +AI_AGENT_ENDPOINT=$aiAgentEndpoint +AI_AGENT_ID=$aiAgentId +"@ +if ($aiAgentVersion) { + $backendEnvContent += "`nAI_AGENT_VERSION=$aiAgentVersion" } +$backendEnvContent | Out-File -FilePath "backend/WebApp.Api/.env" -Encoding utf8 -Force + +Write-Host "[OK] Local dev config created" -ForegroundColor Green + +# Open browser +try { Start-Process $containerAppUrl } catch { } -Write-Host "" -Write-Host "=====================================" -ForegroundColor Green -Write-Host " Deployment Complete!" -ForegroundColor Green -Write-Host "=====================================" -ForegroundColor Green -Write-Host "" -Write-Host "Production URL: " -NoNewline -ForegroundColor Cyan -Write-Host "$containerAppUrl" -ForegroundColor White -Write-Host "" -Write-Host "Next Steps:" -ForegroundColor Yellow -Write-Host " • Test production: $containerAppUrl" -ForegroundColor Gray -Write-Host " • Start local dev: .\deployment\scripts\start-local-dev.ps1" -ForegroundColor Gray -Write-Host " • Deploy updates: .\deployment\scripts\deploy.ps1" -ForegroundColor Gray -Write-Host " • View logs: az containerapp logs show -n $containerApp -g $resourceGroup --follow" -ForegroundColor Gray -Write-Host "" +Write-Host "[OK] Post-provision complete. URL: $containerAppUrl" -ForegroundColor Green + +if ($script:HookLogFile) { + Write-Host "[LOG] Log file: $script:HookLogFile" -ForegroundColor DarkGray +} +Stop-HookLog diff --git a/deployment/hooks/predeploy.ps1 b/deployment/hooks/predeploy.ps1 new file mode 100644 index 0000000..0d3ef60 --- /dev/null +++ b/deployment/hooks/predeploy.ps1 @@ -0,0 +1,111 @@ +#!/usr/bin/env pwsh +# Pre-deploy: Build container (local Docker if available, ACR cloud build as fallback) + +$ErrorActionPreference = "Stop" +. "$PSScriptRoot/modules/HookLogging.ps1" +Start-HookLog -HookName "predeploy" -EnvironmentName $env:AZURE_ENV_NAME + +Write-Host "Pre-Deploy: Building Container Image" -ForegroundColor Cyan + +# Get required values — azd injects env vars into hooks, but `azd env get-value` may fail +# if the subprocess can't locate azure.yaml. Fall back to $env: vars. +function Get-AzdValue($name) { + $val = (azd env get-value $name 2>&1) | Where-Object { $_ -notmatch 'ERROR|WARNING' } | Select-Object -First 1 + if ([string]::IsNullOrWhiteSpace($val)) { $val = [Environment]::GetEnvironmentVariable($name) } + return $val +} + +$clientId = Get-AzdValue 'ENTRA_SPA_CLIENT_ID' +$tenantId = Get-AzdValue 'ENTRA_TENANT_ID' +$backendClientId = Get-AzdValue 'ENTRA_BACKEND_CLIENT_ID' +$appInsightsConnStr = Get-AzdValue 'APPLICATIONINSIGHTS_FRONTEND_CONNECTION_STRING' +# Escape semicolons for ACR cloud builds — unescaped semicolons are interpreted as shell command separators +if ($appInsightsConnStr) { $appInsightsConnStrEscaped = $appInsightsConnStr -replace ';', '\;' } else { $appInsightsConnStrEscaped = '' } +$acrName = Get-AzdValue 'AZURE_CONTAINER_REGISTRY_NAME' +$resourceGroup = Get-AzdValue 'AZURE_RESOURCE_GROUP_NAME' +$containerApp = Get-AzdValue 'AZURE_CONTAINER_APP_NAME' + +if (-not $clientId -or -not $tenantId) { + Write-Host "[ERROR] ENTRA_SPA_CLIENT_ID or ENTRA_TENANT_ID not set" -ForegroundColor Red + exit 1 +} +if (-not $acrName) { + Write-Host "[ERROR] AZURE_CONTAINER_REGISTRY_NAME not set" -ForegroundColor Red + exit 1 +} + +$imageTag = "deploy-$([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())" +$imageName = "$acrName.azurecr.io/web:$imageTag" + +# Check Docker availability +$dockerAvailable = $false +if (Get-Command docker -EA SilentlyContinue) { + $dockerVersion = docker version --format '{{.Server.Version}}' 2>$null + if ($LASTEXITCODE -eq 0 -and $dockerVersion) { + $dockerAvailable = $true + Write-Host "[OK] Docker v$dockerVersion" -ForegroundColor Green + } +} + +$projectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +Push-Location $projectRoot + +try { + if ($dockerAvailable) { + Write-Host "Building with local Docker..." -ForegroundColor Cyan + $buildArgs = @( + "--platform", "linux/amd64", + "--build-arg", "ENTRA_SPA_CLIENT_ID=$clientId", + "--build-arg", "ENTRA_TENANT_ID=$tenantId" + ) + if ($backendClientId) { $buildArgs += @("--build-arg", "ENTRA_BACKEND_CLIENT_ID=$backendClientId") } + if ($appInsightsConnStr) { $buildArgs += @("--build-arg", "APPLICATIONINSIGHTS_FRONTEND_CONNECTION_STRING=$appInsightsConnStr") } + $buildArgs += @("-f", "deployment/docker/frontend.Dockerfile", "-t", $imageName, ".") + docker build @buildArgs 2>&1 | Out-Host + if ($LASTEXITCODE -ne 0) { throw "Docker build failed" } + + Write-Host "Pushing to ACR..." -ForegroundColor Cyan + az acr login --name $acrName | Out-Null + docker push $imageName 2>&1 | Out-Host + if ($LASTEXITCODE -ne 0) { throw "Docker push failed" } + } else { + Write-Host "Using ACR cloud build (3-5 min)..." -ForegroundColor Yellow + $acrBuildArgs = @("--build-arg", "ENTRA_SPA_CLIENT_ID=$clientId", "--build-arg", "ENTRA_TENANT_ID=$tenantId") + if ($backendClientId) { $acrBuildArgs += @("--build-arg", "ENTRA_BACKEND_CLIENT_ID=$backendClientId") } + if ($appInsightsConnStrEscaped) { $acrBuildArgs += @("--build-arg", "APPLICATIONINSIGHTS_FRONTEND_CONNECTION_STRING=$appInsightsConnStrEscaped") } + $buildOutput = az acr build --registry $acrName --image "web:$imageTag" ` + @acrBuildArgs ` + --file deployment/docker/frontend.Dockerfile . ` + --no-logs --only-show-errors 2>&1 + + if ($LASTEXITCODE -ne 0) { + Write-Host "Build output: $buildOutput" -ForegroundColor Red + throw "ACR build failed" + } + Write-Host "[OK] ACR build completed" -ForegroundColor Green + } + Write-Host "[OK] Image built: $imageName" -ForegroundColor Green + + # Update Container App (skip if doesn't exist yet - first azd up uses placeholder) + if ($containerApp -and $resourceGroup) { + $exists = az containerapp show --name $containerApp --resource-group $resourceGroup --query name -o tsv 2>$null + if ($exists) { + Write-Host "Updating Container App..." -ForegroundColor Cyan + az containerapp update --name $containerApp --resource-group $resourceGroup ` + --image $imageName --output none + if ($LASTEXITCODE -ne 0) { throw "Container App update failed" } + Write-Host "[OK] Container App updated" -ForegroundColor Green + } else { + Write-Host "[SKIP] Container App not yet provisioned (first run)" -ForegroundColor Yellow + } + } + + azd env set SERVICE_WEB_IMAGE_NAME $imageName 2>$null +} finally { + Pop-Location +} + +if ($script:HookLogFile) { + Write-Host "[LOG] Log file: $script:HookLogFile" -ForegroundColor DarkGray +} +Stop-HookLog diff --git a/deployment/hooks/preprovision.ps1 b/deployment/hooks/preprovision.ps1 index 89197cd..b83ada9 100644 --- a/deployment/hooks/preprovision.ps1 +++ b/deployment/hooks/preprovision.ps1 @@ -1,457 +1,242 @@ #!/usr/bin/env pwsh +# Pre-provision: Discovers AI Foundry resources and configures agent +# Entra app registration is handled declaratively by Bicep (infra/entra-app.bicep) -# Set environment variable to fix Azure CLI Unicode encoding issues +$ErrorActionPreference = "Stop" $env:PYTHONIOENCODING = "utf-8" +. "$PSScriptRoot/modules/HookLogging.ps1" +Start-HookLog -HookName "preprovision" -EnvironmentName $env:AZURE_ENV_NAME -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "Pre-Provision: Entra ID App Registration" -ForegroundColor Cyan -Write-Host "========================================" -ForegroundColor Cyan -Write-Host "" +Write-Host "Pre-Provision: AI Foundry Discovery" -ForegroundColor Cyan -# Validate prerequisites -Write-Host "Validating prerequisites..." -ForegroundColor Cyan - -if (-not (Get-Command az -ErrorAction SilentlyContinue)) { - Write-Error "Azure CLI not found. Install from https://aka.ms/azure-cli" - exit 1 +# Check prerequisites +foreach ($cmd in @('pwsh', 'az')) { + if (-not (Get-Command $cmd -EA SilentlyContinue)) { + Write-Host "[ERROR] $cmd not found. See: https://learn.microsoft.com/cli/azure/install-azure-cli" -ForegroundColor Red + exit 1 + } } $account = az account show 2>$null | ConvertFrom-Json if (-not $account) { - Write-Error "Not logged in to Azure. Run 'azd auth login' or 'az login'" + Write-Host "[ERROR] Not logged in to Azure. Run 'azd auth login'" -ForegroundColor Red exit 1 } +Write-Host "[OK] Azure CLI: $($account.user.name)" -ForegroundColor Green -Write-Host "[OK] Azure CLI authenticated as: $($account.user.name)" -ForegroundColor Green - -# Get environment variables from azd +# Get environment $envName = (azd env get-value AZURE_ENV_NAME 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 -$envName = if ($envName) { $envName.ToString().Trim() } else { $null } - +if ([string]::IsNullOrWhiteSpace($envName)) { $envName = $env:AZURE_ENV_NAME } if ([string]::IsNullOrWhiteSpace($envName)) { - # Fallback to environment variable for backward compatibility - $envName = $env:AZURE_ENV_NAME -} - -if ([string]::IsNullOrWhiteSpace($envName)) { - Write-Error "AZURE_ENV_NAME not set. Run 'azd init' first." + Write-Host "[ERROR] AZURE_ENV_NAME not set. Run 'azd init' first." -ForegroundColor Red exit 1 } +# Auto-detect tenant if not set $tenantId = (azd env get-value ENTRA_TENANT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 -$tenantId = if ($tenantId) { $tenantId.ToString().Trim() } else { $null } - if ([string]::IsNullOrWhiteSpace($tenantId)) { - # Auto-detect from current Azure CLI session - Write-Host "Auto-detecting tenant ID from Azure CLI..." -ForegroundColor Cyan $tenantId = $account.tenantId - if ($tenantId) { - # Save to azd environment for future use - azd env set ENTRA_TENANT_ID $tenantId - Write-Host "[OK] Detected and saved tenant ID: $tenantId" -ForegroundColor Green - } else { - Write-Error "Could not detect tenant ID. Run 'azd env set ENTRA_TENANT_ID '" - exit 1 - } -} else { - Write-Host "[OK] Using configured tenant ID: $tenantId" -ForegroundColor Green + azd env set ENTRA_TENANT_ID $tenantId } +Write-Host "[OK] Tenant: $tenantId" -ForegroundColor Green Write-Host "[OK] Environment: $envName" -ForegroundColor Green -# Create or update app registration (localhost only at this stage) -$appName = "ai-foundry-agent-$envName" - -Write-Host "" -Write-Host "Creating app registration with localhost redirect URIs..." -ForegroundColor Cyan -Write-Host "(Production URL will be added after infrastructure deployment)" -ForegroundColor Gray - -try { - # Optional: Service Management Reference for organizations with custom app registration policies - # Can be set via environment variable if your organization requires it - $serviceManagementRef = $env:ENTRA_SERVICE_MANAGEMENT_REFERENCE - - if (-not [string]::IsNullOrWhiteSpace($serviceManagementRef)) { - Write-Host "Using Service Management Reference from environment variable" -ForegroundColor Gray +# Map portal variables (AZURE_EXISTING_*) to app variables if present +# The AI Foundry portal's "View sample app code" emits these when linking to this repo. +# Users may paste them into azd env (.azure//.env) or a root .env file. +$portalEndpoint = (azd env get-value AZURE_EXISTING_AIPROJECT_ENDPOINT 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 +$portalAgentId = (azd env get-value AZURE_EXISTING_AGENT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 +$portalResourceId = (azd env get-value AZURE_EXISTING_RESOURCE_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 + +# Also check for a root .env file — the portal says "put in your .env file" without +# specifying which one, so many users create one at the repo root. +$rootEnvFile = Join-Path $PSScriptRoot "../../.env" +if (Test-Path $rootEnvFile) { + $rootEnvVars = @{} + foreach ($line in Get-Content $rootEnvFile) { + $line = $line.Trim() + if ($line -and -not $line.StartsWith('#')) { + $eqIdx = $line.IndexOf('=') + if ($eqIdx -gt 0) { + $key = $line.Substring(0, $eqIdx).Trim() + $value = $line.Substring($eqIdx + 1).Trim().Trim('"') + $rootEnvVars[$key] = $value + } + } } - - # Call app registration script with optional Service Management Reference - $params = @{ - AppName = $appName - TenantId = $tenantId + if (-not $portalEndpoint -and $rootEnvVars['AZURE_EXISTING_AIPROJECT_ENDPOINT']) { + $portalEndpoint = $rootEnvVars['AZURE_EXISTING_AIPROJECT_ENDPOINT'] } - - if (-not [string]::IsNullOrWhiteSpace($serviceManagementRef)) { - $params.ServiceManagementReference = $serviceManagementRef + if (-not $portalAgentId -and $rootEnvVars['AZURE_EXISTING_AGENT_ID']) { + $portalAgentId = $rootEnvVars['AZURE_EXISTING_AGENT_ID'] } - - $clientId = & "$PSScriptRoot/modules/New-EntraAppRegistration.ps1" @params - - if (-not $clientId) { - Write-Error "Failed to create/retrieve app registration" - exit 1 + if (-not $portalResourceId -and $rootEnvVars['AZURE_EXISTING_RESOURCE_ID']) { + $portalResourceId = $rootEnvVars['AZURE_EXISTING_RESOURCE_ID'] } +} - # Store client ID in azd environment - Write-Host "Saving client ID to azd environment..." -ForegroundColor Cyan - azd env set ENTRA_SPA_CLIENT_ID $clientId - - Write-Host "[OK] Client ID saved: $clientId" -ForegroundColor Green +if ($portalEndpoint -or $portalAgentId -or $portalResourceId) { + Write-Host "Mapping portal variables (AZURE_EXISTING_*)..." -ForegroundColor Cyan - # Discover AI Foundry Resource - Write-Host "" - Write-Host "Discovering Azure AI Foundry resources..." -ForegroundColor Cyan - - # Check if already configured - $existingEndpoint = (azd env get-value AI_AGENT_ENDPOINT 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - $existingResourceGroup = (azd env get-value AI_FOUNDRY_RESOURCE_GROUP 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - $existingResourceName = (azd env get-value AI_FOUNDRY_RESOURCE_NAME 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - $existingAgentId = (azd env get-value AI_AGENT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - - if (-not [string]::IsNullOrWhiteSpace($existingEndpoint) -and - -not [string]::IsNullOrWhiteSpace($existingResourceGroup) -and - -not [string]::IsNullOrWhiteSpace($existingResourceName)) { - Write-Host "[OK] Using pre-configured AI Foundry resource:" -ForegroundColor Green - Write-Host " Resource: $existingResourceName" -ForegroundColor Gray - Write-Host " Resource Group: $existingResourceGroup" -ForegroundColor Gray - Write-Host " Endpoint: $existingEndpoint" -ForegroundColor Gray - - # Validate user has permissions on the resource group for RBAC assignment - Write-Host "" - Write-Host "Validating permissions for RBAC assignment..." -ForegroundColor Cyan - $hasPermission = az role assignment list --scope "/subscriptions/$($account.id)/resourceGroups/$existingResourceGroup" ` - --assignee $account.user.name ` - --query "[?roleDefinitionName=='Owner' || roleDefinitionName=='User Access Administrator' || roleDefinitionName=='Contributor'].roleDefinitionName" ` - --output tsv 2>$null - - if ($hasPermission) { - Write-Host "[OK] Verified permissions for RBAC assignment" -ForegroundColor Green - } else { - Write-Host "[!] Warning: You may not have permissions to assign RBAC roles" -ForegroundColor Yellow - Write-Host " The deployment will configure the Container App's managed identity to access the AI Foundry resource." -ForegroundColor Gray - Write-Host " If RBAC assignment fails, ask your subscription admin to grant 'User Access Administrator' role." -ForegroundColor Gray + if ($portalEndpoint) { + $currentEndpoint = (azd env get-value AI_AGENT_ENDPOINT 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 + if ([string]::IsNullOrWhiteSpace($currentEndpoint)) { + azd env set AI_AGENT_ENDPOINT $portalEndpoint + Write-Host "[OK] Mapped AZURE_EXISTING_AIPROJECT_ENDPOINT -> AI_AGENT_ENDPOINT" -ForegroundColor Green } - - if (-not [string]::IsNullOrWhiteSpace($existingAgentId)) { - Write-Host " Agent: $existingAgentId" -ForegroundColor Gray - } else { - # Try to discover agent even with pre-configured endpoint - try { - $allAgents = & "$PSScriptRoot/modules/Get-AIFoundryAgents.ps1" -ProjectEndpoint $existingEndpoint - - if ($allAgents -and $allAgents.Count -gt 0) { - if ($allAgents.Count -eq 1) { - Write-Host " Agent: $($allAgents[0].name)" -ForegroundColor Gray - azd env set AI_AGENT_ID $allAgents[0].name - } else { - Write-Host " Found $($allAgents.Count) agents, using first: $($allAgents[0].name)" -ForegroundColor Gray - azd env set AI_AGENT_ID $allAgents[0].name - } - } - } catch { - # Silently continue if discovery fails for pre-configured resources + } + + if ($portalAgentId) { + $currentAgentId = (azd env get-value AI_AGENT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 + if ([string]::IsNullOrWhiteSpace($currentAgentId)) { + # Portal format is "name:version" (e.g. "dadjokes:2") — split and map + # If no version suffix, AI_AGENT_VERSION remains unset (defaults to latest) + $parts = $portalAgentId -split ':', 2 + $agentName = $parts[0].Trim() + azd env set AI_AGENT_ID $agentName + Write-Host "[OK] Mapped AZURE_EXISTING_AGENT_ID -> AI_AGENT_ID=$agentName" -ForegroundColor Green + + $agentVersion = if ($parts.Count -gt 1) { $parts[1].Trim() } else { '' } + if ($agentVersion) { + azd env set AI_AGENT_VERSION $agentVersion + Write-Host "[OK] Mapped agent version -> AI_AGENT_VERSION=$agentVersion" -ForegroundColor Green } } - } else { - Write-Host "Searching for AI Foundry resources (kind=AIServices) in subscription..." -ForegroundColor Cyan - - $aiFoundryResources = az cognitiveservices account list --query "[?kind=='AIServices']" | ConvertFrom-Json - - if (-not $aiFoundryResources -or $aiFoundryResources.Count -eq 0) { - Write-Host "" - Write-Error @" -No Azure AI Foundry resources found in subscription. + } -To use this application, you need an Azure AI Foundry resource with a project and agent. + if ($portalResourceId) { + $currentResourceName = (azd env get-value AI_FOUNDRY_RESOURCE_NAME 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 + if ([string]::IsNullOrWhiteSpace($currentResourceName)) { + # Extract resource name from ARM path: .../accounts/ + $resourceName = (($portalResourceId -split '/accounts/')[-1] -split '/' | Select-Object -First 1).Trim() + if ($resourceName) { + azd env set AI_FOUNDRY_RESOURCE_NAME $resourceName + Write-Host "[OK] Mapped AZURE_EXISTING_RESOURCE_ID -> AI_FOUNDRY_RESOURCE_NAME=$resourceName" -ForegroundColor Green + } + } + } +} -Option 1 - Create a new AI Foundry resource: - 1. Visit https://ai.azure.com - 2. Create a new AI Foundry resource and project - 3. Create an agent in the project - 4. Run 'azd up' again +# Discover AI Foundry resources +Write-Host "Discovering AI Foundry resources..." -ForegroundColor Cyan -Option 2 - Manually configure (if resource is in different subscription): - azd env set AI_FOUNDRY_RESOURCE_GROUP - azd env set AI_FOUNDRY_RESOURCE_NAME - azd env set AI_AGENT_ENDPOINT - azd env set AI_AGENT_ID +$existingEndpoint = (azd env get-value AI_AGENT_ENDPOINT 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 -For more information, visit: https://learn.microsoft.com/azure/ai-foundry -"@ - exit 1 +if ([string]::IsNullOrWhiteSpace($existingEndpoint)) { + $configuredResourceName = (azd env get-value AI_FOUNDRY_RESOURCE_NAME 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 + + # Auto-discover + $resources = az cognitiveservices account list --query "[?kind=='AIServices']" | ConvertFrom-Json + if (-not $resources -or $resources.Count -eq 0) { + Write-Host "[ERROR] No AI Foundry resources found. Create one at https://ai.azure.com" -ForegroundColor Red + exit 1 + } + + $selected = $null + + if ($resources.Count -eq 1) { + # Single resource - use it directly + $selected = $resources[0] + Write-Host "[OK] Found 1 AI Foundry resource: $($selected.name)" -ForegroundColor Green + } else { + # Multiple resources - use configured name or fallback to first + if (-not [string]::IsNullOrWhiteSpace($configuredResourceName)) { + $matched = @($resources | Where-Object { $_.name -eq $configuredResourceName.Trim() }) + if ($matched.Count -ge 1) { + $selected = $matched[0] + Write-Host "[OK] Using configured: $($selected.name)" -ForegroundColor Green + } else { + Write-Host "[!] AI_FOUNDRY_RESOURCE_NAME '$configuredResourceName' not found." -ForegroundColor Yellow + } } - if ($aiFoundryResources.Count -eq 1) { - $selectedResource = $aiFoundryResources[0] - Write-Host "[OK] Found 1 AI Foundry resource: $($selectedResource.name)" -ForegroundColor Green - } else { - # Multiple resources found - prompt user to select - Write-Host "Found $($aiFoundryResources.Count) AI Foundry resources:" -ForegroundColor Cyan - Write-Host "" - for ($i = 0; $i -lt $aiFoundryResources.Count; $i++) { - $res = $aiFoundryResources[$i] - Write-Host " [$($i+1)] $($res.name)" -ForegroundColor White - Write-Host " Resource Group: $($res.resourceGroup)" -ForegroundColor Gray - Write-Host " Location: $($res.location)" -ForegroundColor Gray + if (-not $selected) { + Write-Host "[!] Multiple AI Foundry resources found:" -ForegroundColor Yellow + for ($i = 0; $i -lt $resources.Count; $i++) { + $r = $resources[$i] + Write-Host " [$($i+1)] $($r.name) (RG: $($r.resourceGroup), Region: $($r.location))" -ForegroundColor White } - Write-Host "" - Write-Host "Please select which resource to use (1-$($aiFoundryResources.Count)):" -ForegroundColor Yellow -NoNewline - $selection = Read-Host " " - # Validate selection - $selectionNum = 0 - if (-not [int]::TryParse($selection, [ref]$selectionNum) -or $selectionNum -lt 1 -or $selectionNum -gt $aiFoundryResources.Count) { - Write-Error "Invalid selection. Please run 'azd up' again and select a number between 1 and $($aiFoundryResources.Count)" - exit 1 - } + # Check if running interactively + $isInteractive = [Environment]::UserInteractive -and -not [Console]::IsInputRedirected - $selectedResource = $aiFoundryResources[$selectionNum - 1] - Write-Host "[OK] Selected: $($selectedResource.name)" -ForegroundColor Green - } - - # Get projects for the selected resource - Write-Host "Discovering projects in $($selectedResource.name)..." -ForegroundColor Cyan - $resourceId = $selectedResource.id - $projectsUrl = "https://management.azure.com$resourceId/projects?api-version=2025-04-01-preview" - $projects = az rest --method get --url $projectsUrl --query "value" 2>$null | ConvertFrom-Json - - if (-not $projects -or $projects.Count -eq 0) { - Write-Host "" - Write-Error @" -No projects found in AI Foundry resource '$($selectedResource.name)'. - -To use this application, you need to create a project and agent: - 1. Visit https://ai.azure.com - 2. Open resource: $($selectedResource.name) - 3. Create a new project - 4. Create an agent in the project - 5. Run 'azd up' again - -For more information, visit: https://learn.microsoft.com/azure/ai-foundry/quickstarts/get-started-code -"@ - exit 1 - } - - $selectedProject = $projects[0] - $projectName = $selectedProject.name.Split('/')[-1] - - if ($projects.Count -eq 1) { - Write-Host "[OK] Found 1 project: $projectName" -ForegroundColor Green - } else { - Write-Host "Found $($projects.Count) projects, using first: $projectName" -ForegroundColor Yellow - } - - # Construct endpoint URL - $aiEndpoint = "https://$($selectedResource.name).services.ai.azure.com/api/projects/$projectName" - - # Save configuration - azd env set AI_FOUNDRY_RESOURCE_GROUP $selectedResource.resourceGroup - azd env set AI_FOUNDRY_RESOURCE_NAME $selectedResource.name - azd env set AI_AGENT_ENDPOINT $aiEndpoint - - Write-Host "[OK] Configured AI Foundry resource:" -ForegroundColor Green - Write-Host " Resource: $($selectedResource.name)" -ForegroundColor Gray - Write-Host " Resource Group: $($selectedResource.resourceGroup)" -ForegroundColor Gray - Write-Host " Project: $projectName" -ForegroundColor Gray - Write-Host " Endpoint: $aiEndpoint" -ForegroundColor Gray - - # Validate user has permissions on the resource group for RBAC assignment - Write-Host "" - Write-Host "Validating permissions for RBAC assignment..." -ForegroundColor Cyan - $hasPermission = az role assignment list --scope "/subscriptions/$($account.id)/resourceGroups/$($selectedResource.resourceGroup)" ` - --assignee $account.user.name ` - --query "[?roleDefinitionName=='Owner' || roleDefinitionName=='User Access Administrator' || roleDefinitionName=='Contributor'].roleDefinitionName" ` - --output tsv 2>$null - - if ($hasPermission) { - Write-Host "[OK] Verified permissions for RBAC assignment" -ForegroundColor Green - } else { - Write-Host "[!] Warning: You may not have permissions to assign RBAC roles" -ForegroundColor Yellow - Write-Host " The deployment will configure the Container App's managed identity to access the AI Foundry resource." -ForegroundColor Gray - Write-Host " If RBAC assignment fails, ask your subscription admin to grant 'User Access Administrator' role." -ForegroundColor Gray - } - - # Discover or verify agent - $aiAgentId = (azd env get-value AI_AGENT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - - if ([string]::IsNullOrWhiteSpace($aiAgentId)) { - try { - $allAgents = & "$PSScriptRoot/modules/Get-AIFoundryAgents.ps1" -ProjectEndpoint $aiEndpoint - - if ($allAgents -and $allAgents.Count -gt 0) { - if ($allAgents.Count -eq 1) { - $selectedAgent = $allAgents[0] - Write-Host "[OK] Found 1 agent: $($selectedAgent.name)" -ForegroundColor Green - $aiAgentId = $selectedAgent.name - azd env set AI_AGENT_ID $aiAgentId - } else { - Write-Host "Found $($allAgents.Count) agents:" -ForegroundColor Yellow - for ($i = 0; $i -lt [Math]::Min($allAgents.Count, 5); $i++) { - $agent = $allAgents[$i] - Write-Host " [$($i+1)] $($agent.name)" -ForegroundColor Gray - } - if ($allAgents.Count -gt 5) { - Write-Host " ... and $($allAgents.Count - 5) more" -ForegroundColor Gray - } - Write-Host "" - Write-Host "Using first agent: $($allAgents[0].name)" -ForegroundColor Yellow - Write-Host "To use a different agent, run: azd env set AI_AGENT_ID " -ForegroundColor Gray - $aiAgentId = $allAgents[0].name - azd env set AI_AGENT_ID $aiAgentId - } + if ($isInteractive) { + $choice = (Read-Host "Select (1-$($resources.Count)) or press Enter for [1]").Trim() + if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "1" } + $idx = [int]$choice - 1 + if ($idx -ge 0 -and $idx -lt $resources.Count) { + $selected = $resources[$idx] + } else { + $selected = $resources[0] } - } catch { - Write-Host "[!] Could not list agents (API error)" -ForegroundColor Yellow + Write-Host "[OK] Selected: $($selected.name)" -ForegroundColor Green + } else { + # Non-interactive: auto-select first and warn + $selected = $resources[0] + Write-Host "[!] Non-interactive mode: using first resource '$($selected.name)'" -ForegroundColor Yellow + Write-Host " To specify: azd env set AI_FOUNDRY_RESOURCE_NAME " -ForegroundColor Yellow } } - - # Final check for agent - if ([string]::IsNullOrWhiteSpace($aiAgentId)) { - Write-Host "" - Write-Host "[!] Agent not configured" -ForegroundColor Yellow - Write-Host "You need to specify an agent name to use with this application." -ForegroundColor Yellow - Write-Host "" - Write-Host "To set your agent name:" -ForegroundColor Cyan - Write-Host " 1. Visit https://ai.azure.com" -ForegroundColor Gray - Write-Host " 2. Open project: $projectName" -ForegroundColor Gray - Write-Host " 3. Go to 'Agents' and create or select an agent" -ForegroundColor Gray - Write-Host " 4. Copy the Agent Name from the agent's details" -ForegroundColor Gray - Write-Host " 5. Run: azd env set AI_AGENT_ID " -ForegroundColor Gray - Write-Host " 6. Run: azd up" -ForegroundColor Gray - Write-Host "" - Write-Error "AI_AGENT_ID is required. Please configure it and run 'azd up' again." - exit 1 - } else { - Write-Host " Agent: $aiAgentId" -ForegroundColor Gray - } } - - # Create .env file for local development (azd's standard location) - Write-Host "" - Write-Host "Creating .env file for local development..." -ForegroundColor Cyan - - $dotAzurePath = ".azure/$envName" - $envFilePath = "$dotAzurePath/.env" - # Get current values - $aiEndpoint = (azd env get-value AI_AGENT_ENDPOINT 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - $aiAgentId = (azd env get-value AI_AGENT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - $aiResourceGroup = (azd env get-value AI_FOUNDRY_RESOURCE_GROUP 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - $aiResourceName = (azd env get-value AI_FOUNDRY_RESOURCE_NAME 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - - # Load existing azd environment variables - $existingVars = @{} - if (Test-Path $envFilePath) { - foreach ($line in Get-Content $envFilePath) { - if ($line -match '^([^=]+)=(.*)$') { - $existingVars[$matches[1]] = $matches[2] - } + Write-Host "[OK] Using AI Foundry resource: $($selected.name)" -ForegroundColor Green + + # Region safety check: warn if AI Foundry resource is in a different region than deployment + $deploymentLocation = (azd env get-value AZURE_LOCATION 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 + $aiFoundryLocation = $selected.location + if (-not [string]::IsNullOrWhiteSpace($deploymentLocation) -and -not [string]::IsNullOrWhiteSpace($aiFoundryLocation)) { + if ($deploymentLocation.Replace(' ','').ToLower() -ne $aiFoundryLocation.Replace(' ','').ToLower()) { + Write-Host "[WARN] Region mismatch: deploying to '$deploymentLocation' but AI Foundry is in '$aiFoundryLocation'" -ForegroundColor Yellow + Write-Host " The user-assigned MI has isolationScope=Regional. Cross-region RBAC assignments" -ForegroundColor Yellow + Write-Host " still work, but for best resilience consider co-locating resources." -ForegroundColor Yellow + Write-Host " To change: azd env set AZURE_LOCATION $aiFoundryLocation" -ForegroundColor Gray + } else { + Write-Host "[OK] Region match: $deploymentLocation" -ForegroundColor Green } } - # Update or add our variables - $existingVars['ENTRA_SPA_CLIENT_ID'] = $clientId - $existingVars['ENTRA_TENANT_ID'] = $tenantId - - if (-not [string]::IsNullOrWhiteSpace($aiEndpoint)) { - $existingVars['AI_AGENT_ENDPOINT'] = $aiEndpoint - } - if (-not [string]::IsNullOrWhiteSpace($aiAgentId)) { - $existingVars['AI_AGENT_ID'] = $aiAgentId - } - if (-not [string]::IsNullOrWhiteSpace($aiResourceGroup)) { - $existingVars['AI_FOUNDRY_RESOURCE_GROUP'] = $aiResourceGroup - } - if (-not [string]::IsNullOrWhiteSpace($aiResourceName)) { - $existingVars['AI_FOUNDRY_RESOURCE_NAME'] = $aiResourceName - } - - # Write back to file - $envContent = "# Auto-generated - Do not commit`n# Local development environment variables`n`n" - foreach ($key in $existingVars.Keys | Sort-Object) { - $value = $existingVars[$key] - $envContent += "$key=$value`n" + # Get first project + $projectsUrl = "https://management.azure.com$($selected.id)/projects?api-version=2025-12-01" + $projects = az rest --method get --url $projectsUrl --query "value" 2>$null | ConvertFrom-Json + if (-not $projects -or $projects.Count -eq 0) { + Write-Host "[ERROR] No projects found. Create one at https://ai.azure.com" -ForegroundColor Red + exit 1 } + $projectName = $projects[0].name.Split('/')[-1] - $envContent | Out-File -FilePath $envFilePath -Encoding utf8 -Force - Write-Host "[OK] Updated $envFilePath" -ForegroundColor Green - - # Create frontend/.env.local for Vite local development - Write-Host "Creating frontend/.env.local for local development..." -ForegroundColor Cyan - $frontendEnvPath = "frontend/.env.local" - $frontendEnvContent = @" -# Auto-generated by azd preprovision hook -# Used by Vite dev server for local development -VITE_ENTRA_SPA_CLIENT_ID=$clientId -VITE_ENTRA_TENANT_ID=$tenantId -"@ - $frontendEnvContent | Out-File -FilePath $frontendEnvPath -Encoding utf8 -Force - Write-Host "[OK] Created $frontendEnvPath" -ForegroundColor Green - - # Create backend .env file for environment variables (simpler than JSON layering) - Write-Host "Creating backend .env file for local development..." -ForegroundColor Cyan - $backendEnvPath = "backend/WebApp.Api/.env" - - # Get AI Agent configuration from azd environment - $aiAgentEndpoint = (azd env get-value AI_AGENT_ENDPOINT 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 - $aiAgentId = (azd env get-value AI_AGENT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 + $aiEndpoint = "https://$($selected.name).services.ai.azure.com/api/projects/$projectName" + azd env set AI_FOUNDRY_RESOURCE_GROUP $selected.resourceGroup + azd env set AI_FOUNDRY_RESOURCE_NAME $selected.name + azd env set AI_FOUNDRY_LOCATION $selected.location + azd env set AI_AGENT_ENDPOINT $aiEndpoint - $backendEnvContent = @" -# Auto-generated by azd preprovision hook -# Used by ASP.NET Core for local development -# .NET automatically loads .env files in Development environment -AzureAd__Instance=https://login.microsoftonline.com/ -AzureAd__TenantId=$tenantId -AzureAd__ClientId=$clientId -AzureAd__Audience=api://$clientId - -# Azure AI Agent Service Configuration -AI_AGENT_ENDPOINT=$aiAgentEndpoint -AI_AGENT_ID=$aiAgentId -"@ - $backendEnvContent | Out-File -FilePath $backendEnvPath -Encoding utf8 -Force - Write-Host "[OK] Created $backendEnvPath" -ForegroundColor Green - - Write-Host "" - Write-Host "========================================" -ForegroundColor Cyan - Write-Host "Local Development Ready" -ForegroundColor Cyan - Write-Host "========================================" -ForegroundColor Cyan - Write-Host "" - Write-Host "Configuration files created for local development:" -ForegroundColor Green - Write-Host " • frontend/.env.local (Vite dev server)" -ForegroundColor Gray - Write-Host " • backend/WebApp.Api/.env (ASP.NET Core)" -ForegroundColor Gray - Write-Host "" - Write-Host "You can start local development anytime with:" -ForegroundColor Yellow - Write-Host " .\scripts\start-local-dev.ps1" -ForegroundColor White - Write-Host "" - Write-Host " Backend: http://localhost:8080" -ForegroundColor Gray - Write-Host " Frontend: http://localhost:5173" -ForegroundColor Gray - Write-Host "" - Write-Host "Proceeding with Azure infrastructure deployment..." -ForegroundColor Cyan - -} catch { - Write-Host "" - Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Red - Write-Host "Pre-Provision Failed" -ForegroundColor Red - Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Red - Write-Host "" - - # The module script already displayed detailed error information - # Just provide a brief summary and link to docs - if ($_.Exception.Message -match "App registration creation failed") { - Write-Host "App registration failed. See error details above." -ForegroundColor Yellow - } else { - Write-Error "Unexpected error: $_" - } + Write-Host "[OK] Endpoint: $aiEndpoint" -ForegroundColor Green +} else { + Write-Host "[OK] Using pre-configured endpoint" -ForegroundColor Green + $aiEndpoint = $existingEndpoint +} - Write-Host "" - Write-Host "For troubleshooting steps, see: deployment/hooks/README.md" -ForegroundColor Gray - Write-Host "" +# Discover agent if not set +$agentId = (azd env get-value AI_AGENT_ID 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 +if ([string]::IsNullOrWhiteSpace($agentId)) { + try { + $agents = & "$PSScriptRoot/modules/Get-AIFoundryAgents.ps1" -ProjectEndpoint $aiEndpoint + if ($agents -and $agents.Count -gt 0) { + $agentId = $agents[0].name + azd env set AI_AGENT_ID $agentId + Write-Host "[OK] Agent: $agentId" -ForegroundColor Green + } + } catch { } +} +if ([string]::IsNullOrWhiteSpace($agentId)) { + Write-Host "[ERROR] AI_AGENT_ID required. Run: azd env set AI_AGENT_ID " -ForegroundColor Red exit 1 } -Write-Host "" -Write-Host "========================================" -ForegroundColor Green -Write-Host "Pre-Provision Complete!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "" +Write-Host "[OK] Pre-provision complete" -ForegroundColor Green + +if ($script:HookLogFile) { + Write-Host "[LOG] Log file: $script:HookLogFile" -ForegroundColor DarkGray +} +Stop-HookLog diff --git a/deployment/scripts/build-and-deploy-container.ps1 b/deployment/scripts/build-and-deploy-container.ps1 deleted file mode 100644 index 4333137..0000000 --- a/deployment/scripts/build-and-deploy-container.ps1 +++ /dev/null @@ -1,193 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Shared module for building and deploying container images - -.DESCRIPTION - This module contains the core logic for: - 1. Building Docker images (local or ACR cloud build) - 2. Pushing to Azure Container Registry - 3. Updating Azure Container Apps - - Used by both postprovision.ps1 (azd up) and deploy.ps1 (standalone deployment) - -.PARAMETER ClientId - Entra SPA Client ID to embed in the frontend build - -.PARAMETER TenantId - Entra Tenant ID to embed in the frontend build - -.PARAMETER ResourceGroup - Azure Resource Group containing the Container App - -.PARAMETER ContainerApp - Azure Container App name - -.PARAMETER AcrName - Azure Container Registry name - -.EXAMPLE - .\build-and-deploy-container.ps1 -ClientId "xxx" -TenantId "yyy" -ResourceGroup "rg-name" -ContainerApp "ca-name" -AcrName "acr-name" -#> - -param( - [Parameter(Mandatory=$true)] - [string]$ClientId, - - [Parameter(Mandatory=$true)] - [string]$TenantId, - - [Parameter(Mandatory=$true)] - [string]$ResourceGroup, - - [Parameter(Mandatory=$true)] - [string]$ContainerApp, - - [Parameter(Mandatory=$true)] - [string]$AcrName -) - -$ErrorActionPreference = "Stop" - -# Generate unique image tag -$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() -$imageTag = "azd-deploy-$timestamp" -$imageName = "$AcrName.azurecr.io/ai-foundry-agent/web-dev:$imageTag" - -Write-Host "Image: $imageName" -ForegroundColor White - -# Check if Docker is available and running -$dockerAvailable = $false -$dockerCommand = Get-Command docker -ErrorAction SilentlyContinue - -if ($dockerCommand) { - # Docker command exists, verify daemon is running - $dockerVersion = docker version --format '{{.Server.Version}}' 2>$null - if ($LASTEXITCODE -eq 0 -and $dockerVersion) { - $dockerAvailable = $true - Write-Host "[OK] Docker daemon is running (version: $dockerVersion)" -ForegroundColor Gray - } else { - Write-Host "[!] Docker is installed but not running. Using ACR cloud build instead..." -ForegroundColor Yellow - } -} else { - Write-Host "[!] Docker not installed, using ACR cloud build..." -ForegroundColor Gray -} -Write-Host "" - -# Get project root (script is in deployment/scripts) -$projectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - -if ($dockerAvailable) { - # Local Docker build - Write-Host "Building Docker image locally..." -ForegroundColor Cyan - - Push-Location $projectRoot - try { - docker build ` - --build-arg ENTRA_SPA_CLIENT_ID=$ClientId ` - --build-arg ENTRA_TENANT_ID=$TenantId ` - -f .\deployment\docker\frontend.Dockerfile ` - -t $imageName ` - . 2>&1 | Out-Host - - if ($LASTEXITCODE -ne 0) { - throw "Docker build failed" - } - - Write-Host "[OK] Docker image built successfully" -ForegroundColor Green - Write-Host "" - - # Push to ACR - Write-Host "Pushing image to Azure Container Registry..." -ForegroundColor Cyan - - az acr login --name $AcrName | Out-Null - - docker push $imageName 2>&1 | Out-Host - - if ($LASTEXITCODE -ne 0) { - throw "Docker push failed" - } - - Write-Host "[OK] Image pushed to ACR" -ForegroundColor Green - } - finally { - Pop-Location - } -} else { - # ACR cloud build - Write-Host "Using Azure Container Registry cloud build..." -ForegroundColor Cyan - Write-Host "(This may take 3-5 minutes - showing live logs)" -ForegroundColor Gray - Write-Host "" - - Push-Location $projectRoot - try { - # Use ACR build without streaming logs to avoid Windows encoding issues - Write-Host "Starting ACR build (build ID will be shown)..." -ForegroundColor Gray - Write-Host "" - - # Capture the build output to get the run ID - $buildOutput = az acr build ` - --registry $AcrName ` - --image "ai-foundry-agent/web-dev:$imageTag" ` - --build-arg ENTRA_SPA_CLIENT_ID=$ClientId ` - --build-arg ENTRA_TENANT_ID=$TenantId ` - --file .\deployment\docker\frontend.Dockerfile ` - --no-logs ` - . 2>&1 - - if ($LASTEXITCODE -ne 0) { - Write-Host $buildOutput -ForegroundColor Red - throw "ACR build failed with exit code $LASTEXITCODE" - } - - # Extract run ID from output - $runId = ($buildOutput | Select-String -Pattern "Queued a build with ID: (\w+)" | ForEach-Object { $_.Matches.Groups[1].Value }) - - if ($runId) { - Write-Host "ACR Build ID: $runId" -ForegroundColor Cyan - Write-Host "Logs: az acr task logs -r $AcrName --run-id $runId" -ForegroundColor Gray - } - - Write-Host "" - Write-Host "[OK] Image built and pushed to ACR" -ForegroundColor Green - } - finally { - Pop-Location - } -} -Write-Host "" - -# Update Container App -Write-Host "Updating Container App with new image..." -ForegroundColor Cyan - -az containerapp update ` - --name $ContainerApp ` - --resource-group $ResourceGroup ` - --image $imageName ` - --output none - -if ($LASTEXITCODE -ne 0) { - throw "Container app update failed" -} - -Write-Host "[OK] Container App updated" -ForegroundColor Green -Write-Host "" - -# Wait for deployment to stabilize -Write-Host "Waiting for deployment to stabilize..." -ForegroundColor Cyan -Start-Sleep -Seconds 15 - -Write-Host "[OK] Deployment complete" -ForegroundColor Green -Write-Host "" - -# Return the Container App URL for the caller -$containerAppUrl = az containerapp show ` - --name $ContainerApp ` - --resource-group $ResourceGroup ` - --query "properties.configuration.ingress.fqdn" ` - -o tsv - -if ($containerAppUrl) { - $fullUrl = "https://$containerAppUrl" - Write-Output $fullUrl -} diff --git a/deployment/scripts/deploy.ps1 b/deployment/scripts/deploy.ps1 index 6caff95..b4113f3 100644 --- a/deployment/scripts/deploy.ps1 +++ b/deployment/scripts/deploy.ps1 @@ -4,19 +4,23 @@ Deploy code updates to Azure Container Apps .DESCRIPTION - This script rebuilds and deploys the Docker image to Azure without re-provisioning infrastructure. + This script is a convenience wrapper around 'azd deploy'. - What it does: - 1. Gets Container App configuration from azd environment - 2. Builds Docker image with embedded Client ID - 3. Pushes image to Azure Container Registry - 4. Updates Container App with new image + azd deploy handles: + 1. Building the Docker image (locally or via ACR remote build) + 2. Passing build args (ENTRA_SPA_CLIENT_ID, ENTRA_TENANT_ID) from azd environment + 3. Pushing to Azure Container Registry + 4. Updating the Container App with the new image - Use this for code-only deployments (faster than azd up). + This is faster than 'azd up' as it skips infrastructure provisioning. For infrastructure changes, use: azd up .EXAMPLE .\deployment\scripts\deploy.ps1 + +.EXAMPLE + # Or use azd directly: + azd deploy #> $ErrorActionPreference = "Stop" @@ -26,76 +30,41 @@ Write-Host "Deploy to Azure Container Apps" -ForegroundColor Cyan Write-Host "========================================" -ForegroundColor Cyan Write-Host "" -# Get environment variables from azd +# Verify azd environment exists $envName = azd env get-value AZURE_ENV_NAME 2>&1 if ($LASTEXITCODE -ne 0 -or -not $envName) { - Write-Error "AZURE_ENV_NAME not set. Have you run 'azd up' yet?" + Write-Error "No azd environment found. Have you run 'azd up' yet?" exit 1 } -$clientId = azd env get-value ENTRA_SPA_CLIENT_ID 2>&1 -if ($LASTEXITCODE -ne 0 -or -not $clientId) { - Write-Error "ENTRA_SPA_CLIENT_ID not set. Run 'azd up' to configure." - exit 1 -} +Write-Host "Environment: $envName" -ForegroundColor Green +Write-Host "" -$tenantId = azd env get-value ENTRA_TENANT_ID 2>&1 -if ($LASTEXITCODE -ne 0 -or -not $tenantId) { - Write-Error "ENTRA_TENANT_ID not set. Run 'azd up' to configure." - exit 1 -} +# Run azd deploy +Write-Host "Running azd deploy..." -ForegroundColor Cyan +Write-Host "(Uses local Docker if available; otherwise ACR cloud build)" -ForegroundColor Gray +Write-Host "" -$resourceGroup = azd env get-value AZURE_RESOURCE_GROUP_NAME 2>&1 -if ($LASTEXITCODE -ne 0 -or -not $resourceGroup) { - Write-Error "AZURE_RESOURCE_GROUP_NAME not set. Run 'azd up' to provision infrastructure." - exit 1 -} +azd deploy -$containerApp = azd env get-value AZURE_CONTAINER_APP_NAME 2>&1 -if ($LASTEXITCODE -ne 0 -or -not $containerApp) { - Write-Error "AZURE_CONTAINER_APP_NAME not set. Run 'azd up' to provision infrastructure." +if ($LASTEXITCODE -ne 0) { + Write-Error "Deployment failed" exit 1 } -$acrName = azd env get-value AZURE_CONTAINER_REGISTRY_NAME 2>&1 -if ($LASTEXITCODE -ne 0 -or -not $acrName) { - Write-Error "AZURE_CONTAINER_REGISTRY_NAME not set. Run 'azd up' to provision infrastructure." - exit 1 +# Get the deployed URL +$containerAppUrl = azd env get-value WEB_ENDPOINT 2>&1 +if ($containerAppUrl -and $LASTEXITCODE -eq 0) { + Write-Host "" + Write-Host "========================================" -ForegroundColor Green + Write-Host "Deployment Complete!" -ForegroundColor Green + Write-Host "========================================" -ForegroundColor Green + Write-Host "" + Write-Host "Application URL: $containerAppUrl" -ForegroundColor Cyan + Write-Host "" + Write-Host "Commands:" -ForegroundColor Yellow + Write-Host " • Deploy again: azd deploy" -ForegroundColor Gray + Write-Host " • Full redeploy: azd up" -ForegroundColor Gray + Write-Host " • View logs: az containerapp logs show -g `$(azd env get-value AZURE_RESOURCE_GROUP_NAME) -n `$(azd env get-value AZURE_CONTAINER_APP_NAME) --follow" -ForegroundColor Gray + Write-Host "" } - -Write-Host "Environment: $envName" -ForegroundColor Green -Write-Host "Resource Group: $resourceGroup" -ForegroundColor Green -Write-Host "Container App: $containerApp" -ForegroundColor Green -Write-Host "" - -# Call shared deployment module -try { - $containerAppUrl = & "$PSScriptRoot\build-and-deploy-container.ps1" ` - -ClientId $clientId ` - -TenantId $tenantId ` - -ResourceGroup $resourceGroup ` - -ContainerApp $containerApp ` - -AcrName $acrName - - if ($LASTEXITCODE -ne 0) { - Write-Error "Deployment failed" - exit 1 - } -} -catch { - Write-Error "Deployment failed: $_" - exit 1 -} - -Write-Host "" -Write-Host "========================================" -ForegroundColor Green -Write-Host "Deployment Complete!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "" -Write-Host "Application URL: $containerAppUrl" -ForegroundColor Cyan -Write-Host "" -Write-Host "Next steps:" -ForegroundColor Yellow -Write-Host " • Test: $containerAppUrl" -ForegroundColor Gray -Write-Host " • Logs: az containerapp logs show -n $containerApp -g $resourceGroup --follow" -ForegroundColor Gray -Write-Host " • Deploy again: .\deployment\scripts\deploy.ps1" -ForegroundColor Gray -Write-Host "" diff --git a/deployment/scripts/list-agents.ps1 b/deployment/scripts/list-agents.ps1 index 5f26d8b..ce002b1 100644 --- a/deployment/scripts/list-agents.ps1 +++ b/deployment/scripts/list-agents.ps1 @@ -1,97 +1,38 @@ #!/usr/bin/env pwsh -<# -.SYNOPSIS - List agents in an Azure AI Foundry project +# List agents in a Microsoft Foundry project -.DESCRIPTION - Uses the Azure AI Foundry REST API (v2025-11-15-preview) to enumerate agents in a project. - Handles pagination automatically to list all agents. - -.PARAMETER ProjectEndpoint - The Azure AI Foundry project endpoint (e.g., https://myresource.services.ai.azure.com/api/projects/myproject) - -.EXAMPLE - .\list-agents.ps1 - -.EXAMPLE - .\list-agents.ps1 -ProjectEndpoint "https://v2agents-resource.services.ai.azure.com/api/projects/v2agents" -#> - -param( - [Parameter(Mandatory=$false)] - [string]$ProjectEndpoint -) - -# Get endpoint from parameter, environment, or azd -if ([string]::IsNullOrWhiteSpace($ProjectEndpoint)) { - $ProjectEndpoint = $env:AI_AGENT_ENDPOINT -} +param([string]$ProjectEndpoint) +# Get endpoint from param, env, or azd +if ([string]::IsNullOrWhiteSpace($ProjectEndpoint)) { $ProjectEndpoint = $env:AI_AGENT_ENDPOINT } if ([string]::IsNullOrWhiteSpace($ProjectEndpoint)) { - $ProjectEndpoint = (azd env get-value AI_AGENT_ENDPOINT 2>&1) | Where-Object { $_ -notmatch 'ERROR|WARNING' } | Select-Object -First 1 + $ProjectEndpoint = (azd env get-value AI_AGENT_ENDPOINT 2>&1) | Where-Object { $_ -notmatch 'ERROR' } | Select-Object -First 1 } - if ([string]::IsNullOrWhiteSpace($ProjectEndpoint)) { - Write-Error @" -Project endpoint not found. Provide via: - - Parameter: .\list-agents.ps1 -ProjectEndpoint - - Environment: Set AI_AGENT_ENDPOINT environment variable - - Azure Developer CLI: azd env set AI_AGENT_ENDPOINT -"@ + Write-Host "[ERROR] No endpoint. Run: azd env set AI_AGENT_ENDPOINT " -ForegroundColor Red exit 1 } -Write-Host "Project Endpoint: $ProjectEndpoint" -ForegroundColor Cyan -Write-Host "" +Write-Host "Endpoint: $ProjectEndpoint" -ForegroundColor Cyan -# Call the module to get agents try { - $allAgents = & "$PSScriptRoot\..\hooks\modules\Get-AIFoundryAgents.ps1" -ProjectEndpoint $ProjectEndpoint + $agents = & "$PSScriptRoot\..\hooks\modules\Get-AIFoundryAgents.ps1" -ProjectEndpoint $ProjectEndpoint -Quiet - if ($allAgents.Count -eq 0) { - Write-Host "No agents found in project" -ForegroundColor Yellow - Write-Host "" - Write-Host "To create an agent:" -ForegroundColor Cyan - Write-Host " 1. Visit https://ai.azure.com" -ForegroundColor Gray - Write-Host " 2. Open your project" -ForegroundColor Gray - Write-Host " 3. Navigate to 'Agents' and create a new agent" -ForegroundColor Gray + if ($agents.Count -eq 0) { + Write-Host "No agents found. Create one at https://ai.azure.com" -ForegroundColor Yellow exit 0 } - Write-Host "" - Write-Host "Found $($allAgents.Count) agent(s):" -ForegroundColor Green - Write-Host "" - foreach ($agent in $allAgents) { - Write-Host " Name: $($agent.name)" -ForegroundColor White - Write-Host " ID: $($agent.id)" -ForegroundColor Gray - - if ($agent.versions -and $agent.versions.latest) { - $latest = $agent.versions.latest - if ($latest.definition.kind) { - Write-Host " Type: $($latest.definition.kind)" -ForegroundColor Gray - } - if ($latest.definition.model) { - Write-Host " Model: $($latest.definition.model)" -ForegroundColor Gray - } - if ($latest.version) { - Write-Host " Version: $($latest.version)" -ForegroundColor Gray - } - if ($latest.metadata.description) { - Write-Host " Desc: $($latest.metadata.description)" -ForegroundColor Gray - } + $agents | ForEach-Object { + [PSCustomObject]@{ + Name = $_.name + ID = $_.id + Model = $_.versions.latest.definition.model } - - Write-Host "" - } - - Write-Host "To use an agent:" -ForegroundColor Cyan - Write-Host " azd env set AI_AGENT_ID " -ForegroundColor White - Write-Host "" + } | Format-Table -AutoSize + Write-Host "To use: azd env set AI_AGENT_ID " -ForegroundColor Gray } catch { - Write-Error "Failed to list agents: $_" - Write-Host "" - Write-Host "Error details:" -ForegroundColor Red - Write-Host $_.Exception.Message + Write-Host "[ERROR] $_" -ForegroundColor Red exit 1 } diff --git a/deployment/scripts/smoke-test.js b/deployment/scripts/smoke-test.js new file mode 100644 index 0000000..92d61c0 --- /dev/null +++ b/deployment/scripts/smoke-test.js @@ -0,0 +1,177 @@ +#!/usr/bin/env node +/** + * Smoke test for the AI Agent Web App. + * Usage: node deployment/scripts/smoke-test.js [url] + * Default URL: http://localhost:5173 + * + * Tests ALL features: auth, chat, tokens, theme, cancel, sidebar, markdown, starter prompts. + * Outputs JSON report to stdout. Exit code 0 = pass, 1 = fail. + * Takes a screenshot at the end for visual review. + * + * Prerequisites: npx playwright install chromium + */ + +const { chromium } = require('playwright'); + +const URL = process.argv[2] || 'http://localhost:5173'; +const TIMEOUT = 30000; +const results = []; + +function log(test, pass, detail = '') { + const status = pass ? 'PASS' : 'FAIL'; + results.push({ test, status, detail }); + process.stderr.write(` ${pass ? '✅' : '❌'} ${test}${detail ? ': ' + detail : ''}\n`); +} + +async function run() { + process.stderr.write(`\nSmoke Test: ${URL}\n${'─'.repeat(50)}\n`); + + let browser, page; + try { + browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ ignoreHTTPSErrors: true }); + page = await context.newPage(); + + // Collect console errors + const consoleErrors = []; + page.on('console', msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); }); + + // 1. Health check (backend) + try { + const healthUrl = URL.replace(/:\d+/, ':8080') + '/api/health'; + const health = await page.request.get(healthUrl); + log('Health endpoint', health.status() === 200, `${health.status()}`); + } catch (e) { + log('Health endpoint', false, e.message); + } + + // 2. Navigate and wait for auth + await page.goto(URL, { timeout: TIMEOUT }); + try { + await page.waitForSelector('[role="log"]', { timeout: TIMEOUT }); + log('Auth + page load', true); + } catch { + log('Auth + page load', false, 'Page did not load within timeout — may need interactive MSAL login'); + } + + // 3. Agent metadata + const title = await page.title(); + const hasAgentName = !title.includes('AI Agent') || title.length > 'AI Agent'.length; + log('Agent metadata', title.includes('Azure AI Agent'), `Title: "${title}"`); + + // 4. Starter prompts + const starters = await page.locator('[role="list"] button').count(); + log('Starter prompts', starters >= 1, `${starters} prompts found`); + + // 5. Send message via first starter prompt + if (starters > 0) { + await page.locator('[role="list"] button').first().click(); + try { + await page.waitForSelector('text=tokens', { timeout: 30000 }); + log('Chat streaming', true, 'Response received with token count'); + } catch { + log('Chat streaming', false, 'No token count visible after 30s'); + } + } else { + log('Chat streaming', false, 'No starter prompts to click'); + } + + // 6. Token usage + const tokenButton = page.locator('button:has-text("token usage")'); + const hasTokens = await tokenButton.count() > 0; + log('Token usage display', hasTokens); + if (hasTokens) { + await tokenButton.first().click(); + await page.waitForTimeout(500); + const hasInputTokens = await page.locator('text=Input').count() > 0; + log('Token usage expandable', hasInputTokens); + } + + // 7. Theme toggle + const settingsBtn = page.locator('button[aria-label="Settings"]'); + if (await settingsBtn.count() > 0) { + await settingsBtn.click(); + await page.waitForTimeout(500); + const themeDropdown = page.locator('[role="combobox"]'); + if (await themeDropdown.count() > 0) { + await themeDropdown.click(); + const darkOption = page.locator('[role="option"]:has-text("Dark")'); + if (await darkOption.count() > 0) { + await darkOption.click(); + log('Theme toggle', true, 'Dark theme selected'); + // Revert to System + await themeDropdown.click(); + await page.locator('[role="option"]:has-text("System")').click(); + } else { + log('Theme toggle', false, 'Dark option not found'); + } + } else { + log('Theme toggle', false, 'Theme dropdown not found'); + } + // Close settings + await page.locator('button[aria-label="Close"]').click(); + } else { + log('Theme toggle', false, 'Settings button not found'); + } + + // 8. Conversation sidebar + const sidebarBtn = page.locator('button[aria-label="Conversation history"]'); + if (await sidebarBtn.count() > 0) { + await sidebarBtn.click(); + await page.waitForTimeout(1000); + const dialog = page.locator('[role="dialog"]'); + const hasDialog = await dialog.count() > 0; + log('Conversation sidebar', hasDialog, hasDialog ? 'Sidebar opened' : 'Dialog not found'); + if (hasDialog) { + const items = await page.locator('[role="listitem"]').count(); + log('Conversation list', items >= 0, `${items} conversations loaded`); + // Close sidebar + await page.locator('button[aria-label="Close sidebar"]').click(); + } + } else { + log('Conversation sidebar', false, 'History button not found'); + } + + // 9. New chat button + const newChatBtn = page.locator('button[aria-label="New chat"]'); + if (await newChatBtn.count() > 0) { + const isEnabled = await newChatBtn.isEnabled(); + if (isEnabled) { + await newChatBtn.click(); + await page.waitForTimeout(500); + const startersAfterClear = await page.locator('[role="list"] button').count(); + log('New chat', startersAfterClear >= 1, 'Messages cleared, starters visible'); + } else { + log('New chat', true, 'Button disabled (no messages) — correct'); + } + } + + // 10. Console errors + const relevantErrors = consoleErrors.filter(e => !e.includes('Avatar_Default.svg') && !e.includes('favicon')); + log('No console errors', relevantErrors.length === 0, + relevantErrors.length > 0 ? `${relevantErrors.length} errors` : ''); + + // 11. Screenshot + const screenshotPath = `smoke-test-${Date.now()}.png`; + await page.screenshot({ path: screenshotPath, fullPage: false }); + log('Screenshot saved', true, screenshotPath); + + } catch (e) { + log('Fatal error', false, e.message); + } finally { + if (browser) await browser.close(); + } + + // Output JSON report + const passed = results.filter(r => r.status === 'PASS').length; + const failed = results.filter(r => r.status === 'FAIL').length; + const report = { url: URL, timestamp: new Date().toISOString(), passed, failed, total: results.length, results }; + + process.stderr.write(`\n${'─'.repeat(50)}\n`); + process.stderr.write(`Results: ${passed}/${results.length} passed${failed > 0 ? `, ${failed} FAILED` : ''}\n`); + process.stdout.write(JSON.stringify(report, null, 2) + '\n'); + + process.exit(failed > 0 ? 1 : 0); +} + +run(); diff --git a/deployment/scripts/start-local-dev.ps1 b/deployment/scripts/start-local-dev.ps1 index 15d6876..bc605dc 100644 --- a/deployment/scripts/start-local-dev.ps1 +++ b/deployment/scripts/start-local-dev.ps1 @@ -1,276 +1,77 @@ #!/usr/bin/env pwsh -<# -.SYNOPSIS - Starts native local development (backend + frontend with hot reload) - -.DESCRIPTION - This script: - - Checks prerequisites (Node.js, .NET, npm packages) - - Starts ASP.NET Core backend on port 8080 (watch mode) - - Starts React frontend on port 5173 (HMR) - - Opens browser to http://localhost:5173 - - Prerequisites: - - .NET 9 SDK - - Node.js 18+ - - frontend/.env.local must exist (created by azd up) - -.EXAMPLE - .\deployment\scripts\start-local-dev.ps1 -#> +# Starts local dev servers (backend + frontend with hot reload) +# Prerequisites: .NET 10 SDK, Node.js 18+, frontend/.env.local (from azd up) param( - [switch]$SkipBrowser, # Skip opening browser automatically - [switch]$NonInteractive # Run without strict health checks (for azd hooks) + [switch]$SkipBrowser ) -# Use Continue instead of Stop to be more resilient in automation scenarios -$ErrorActionPreference = "Continue" - -# --- Helper Functions --- - -function Write-Status { - param([string]$Message, [string]$Color = "Cyan") - Write-Host "`n=== $Message ===" -ForegroundColor $Color -} - -function Write-Success { - param([string]$Message) - Write-Host "[OK] $Message" -ForegroundColor Green -} - -function Write-Warning { - param([string]$Message) - Write-Host "[!] $Message" -ForegroundColor Yellow -} - -function Write-Error { - param([string]$Message) - Write-Host "[ERROR] $Message" -ForegroundColor Red -} - -function Test-Command { - param([string]$Command) - $null = Get-Command $Command -ErrorAction SilentlyContinue - return $? -} - -# --- Prerequisites Check --- - -Write-Status "Checking Prerequisites" +$ErrorActionPreference = "Stop" +$projectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -# Check .NET SDK -if (-not (Test-Command "dotnet")) { - Write-Error ".NET SDK not found. Install from: https://dotnet.microsoft.com/download" - exit 1 +# Check prerequisites +foreach ($cmd in @('dotnet', 'node', 'npm')) { + if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { + Write-Host "[ERROR] $cmd not found" -ForegroundColor Red + exit 1 + } } -$dotnetVersion = dotnet --version -Write-Success ".NET SDK: $dotnetVersion" +Write-Host "[OK] Prerequisites: dotnet $(dotnet --version), node $(node -v)" -ForegroundColor Green -# Check Node.js -if (-not (Test-Command "node")) { - Write-Error "Node.js not found. Install from: https://nodejs.org/" - exit 1 +# Install frontend deps if missing +$frontendPath = Join-Path $projectRoot "frontend" +$nodeModules = Join-Path $frontendPath "node_modules" +if (-not (Test-Path (Join-Path $nodeModules "@azure/msal-react"))) { + Write-Host "Installing frontend dependencies..." -ForegroundColor Cyan + Push-Location $frontendPath + npm install + if ($LASTEXITCODE -ne 0) { Pop-Location; exit 1 } + Pop-Location } -$nodeVersion = node --version -Write-Success "Node.js: $nodeVersion" +Write-Host "[OK] Frontend dependencies" -ForegroundColor Green -# Check npm -if (-not (Test-Command "npm")) { - Write-Error "npm not found. Install Node.js from: https://nodejs.org/" +# Validate config +$envLocal = Join-Path $frontendPath ".env.local" +if (-not (Test-Path $envLocal)) { + Write-Host "[ERROR] frontend/.env.local not found. Run 'azd up' first." -ForegroundColor Red exit 1 } -$npmVersion = npm --version -Write-Success "npm: $npmVersion" - -# Check frontend dependencies -Write-Status "Checking Frontend Dependencies" -$projectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$frontendPath = Join-Path $projectRoot "frontend" -$nodeModulesPath = Join-Path $frontendPath "node_modules" +Write-Host "[OK] Configuration validated" -ForegroundColor Green -if (-not (Test-Path $nodeModulesPath)) { - Write-Warning "Frontend dependencies not installed. Installing..." - Write-Host "This may take a few minutes on first run..." -ForegroundColor Gray - Push-Location $frontendPath - try { - npm install --legacy-peer-deps 2>&1 | Out-Host - if ($LASTEXITCODE -ne 0) { - Write-Error "npm install failed with exit code $LASTEXITCODE" - Write-Host "`nTry running manually:" -ForegroundColor Yellow - Write-Host " cd frontend" -ForegroundColor White - Write-Host " npm install --legacy-peer-deps" -ForegroundColor White - Pop-Location - exit 1 - } - Write-Success "Frontend dependencies installed" - } - catch { - Write-Error "Failed to install frontend dependencies: $_" - Pop-Location - exit 1 - } - finally { - Pop-Location - } -} else { - # Verify node_modules is valid by checking if a key package exists - $msalPackage = Join-Path $nodeModulesPath "@azure" "msal-react" - if (-not (Test-Path $msalPackage)) { - Write-Warning "node_modules appears incomplete or corrupted. Reinstalling..." - Write-Host "This may take a few minutes..." -ForegroundColor Gray - Push-Location $frontendPath - try { - Remove-Item -Path $nodeModulesPath -Recurse -Force -ErrorAction SilentlyContinue - npm install --legacy-peer-deps 2>&1 | Out-Host - if ($LASTEXITCODE -ne 0) { - Write-Error "npm install failed with exit code $LASTEXITCODE" - Write-Host "`nTry running manually:" -ForegroundColor Yellow - Write-Host " cd frontend" -ForegroundColor White - Write-Host " rm -r -fo node_modules" -ForegroundColor White - Write-Host " npm install --legacy-peer-deps" -ForegroundColor White - Pop-Location - exit 1 - } - Write-Success "Frontend dependencies installed" - } - catch { - Write-Error "Failed to reinstall frontend dependencies: $_" - Pop-Location - exit 1 - } - finally { - Pop-Location +# Kill existing processes on our ports +foreach ($port in @(8080, 5173)) { + if ($IsWindows) { + $processIds = netstat -ano | Select-String ":$port\s.*LISTENING" | ForEach-Object { + if ($_ -match '\s(\d+)\s*$') { [int]$Matches[1] } } } else { - Write-Success "Frontend dependencies found" - } -} - -# Validate configuration using dedicated validator -Write-Status "Validating Configuration" -$validatorScript = Join-Path $PSScriptRoot "validate-config.ps1" -if (Test-Path $validatorScript) { - & $validatorScript - if ($LASTEXITCODE -ne 0) { - Write-Host "`nConfiguration validation failed. Cannot start local development." -ForegroundColor Red - exit 1 - } -} else { - # Fallback: Basic check if validator script doesn't exist - $envLocalPath = Join-Path $projectRoot "frontend" ".env.local" - if (-not (Test-Path $envLocalPath)) { - Write-Error "frontend/.env.local not found." - Write-Host "`nThis file is created by 'azd up'. Please run:" -ForegroundColor Yellow - Write-Host " azd up" -ForegroundColor White - exit 1 + $processIds = lsof -i ":$port" -sTCP:LISTEN -t 2>$null | Where-Object { $_ -match '^\d+$' } } - Write-Success "Configuration file found: frontend/.env.local" -} - -# --- Port Cleanup --- - -function Stop-ProcessOnPort { - param([int]$Port, [string]$ServiceName) - - # Find process using the port - $connections = netstat -ano | Select-String ":$Port\s" | Select-String "LISTENING" - - if ($connections) { - foreach ($connection in $connections) { - # Extract PID from netstat output (last column) - if ($connection -match '\s+(\d+)\s*$') { - $processId = $Matches[1] - try { - $process = Get-Process -Id $processId -ErrorAction SilentlyContinue - if ($process) { - Write-Host " Found $ServiceName process on port $Port (PID: $processId, Name: $($process.Name))" -ForegroundColor Yellow - Write-Host " Stopping process..." -ForegroundColor Gray - Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue - Start-Sleep -Milliseconds 500 - Write-Success "Stopped process $processId" - } - } - catch { - Write-Warning "Could not stop process $processId : $_" - } - } - } + foreach ($processId in $processIds) { + Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue + Write-Host "Stopped process $processId on port $port" -ForegroundColor Yellow } } - -Write-Status "Checking for Port Conflicts" - -# Check and clean up port 8080 (backend) -Stop-ProcessOnPort -Port 8080 -ServiceName "backend" - -# Check and clean up port 5173 (frontend) -Stop-ProcessOnPort -Port 5173 -ServiceName "frontend" - -# Verify ports are free Start-Sleep -Seconds 1 -$port8080Free = -not (netstat -ano | Select-String ":8080\s" | Select-String "LISTENING") -$port5173Free = -not (netstat -ano | Select-String ":5173\s" | Select-String "LISTENING") -if ($port8080Free -and $port5173Free) { - Write-Success "Ports 8080 and 5173 are available" +# Start servers +$backendPath = Join-Path $projectRoot "backend/WebApp.Api" +if ($IsWindows) { + Start-Process pwsh -ArgumentList "-NoExit", "-Command", "cd '$backendPath'; dotnet watch run --no-hot-reload" + Start-Process pwsh -ArgumentList "-NoExit", "-Command", "cd '$frontendPath'; npm run dev" } else { - if (-not $port8080Free) { Write-Warning "Port 8080 may still be in use" } - if (-not $port5173Free) { Write-Warning "Port 5173 may still be in use" } + Start-Job { param($p) Set-Location $p; dotnet watch run --no-hot-reload 2>&1 } -Arg $backendPath | Out-Null + Start-Job { param($p) Set-Location $p; npm run dev 2>&1 } -Arg $frontendPath | Out-Null + Write-Host "Use 'Get-Job' to view background jobs, 'Stop-Job *' to stop" -ForegroundColor Gray } -# --- Start Services --- - -Write-Status "Starting Local Development" "Green" - -# Get project root -$projectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - -# Start both servers in parallel -Write-Host "`nStarting backend (ASP.NET Core on port 8080)..." -ForegroundColor Cyan -$backendPath = Join-Path $projectRoot "backend" "WebApp.Api" -Start-Process pwsh -ArgumentList "-NoExit", "-Command", "cd '$backendPath'; Write-Host '=== Backend (ASP.NET Core) ===' -ForegroundColor Green; dotnet watch run --no-hot-reload" - -Write-Host "Starting frontend (React with HMR on port 5173)..." -ForegroundColor Cyan -$frontendPath = Join-Path $projectRoot "frontend" -Start-Process pwsh -ArgumentList "-NoExit", "-Command", "cd '$frontendPath'; Write-Host '=== Frontend (React + Vite) ===' -ForegroundColor Blue; npm run dev" - -# Give processes a moment to start, then open browser -Write-Host "`nBoth servers starting in parallel..." -ForegroundColor Gray Start-Sleep -Seconds 3 - -# Open browser if (-not $SkipBrowser) { - Write-Host "Opening browser..." -ForegroundColor Cyan - Start-Process "http://localhost:5173" + if ($IsWindows) { Start-Process "http://localhost:5173" } + elseif ($IsMacOS) { open "http://localhost:5173" } + elseif (Get-Command xdg-open -EA SilentlyContinue) { xdg-open "http://localhost:5173" } } -# --- Success Message --- - -Write-Host "`n" -NoNewline -Write-Host "=====================================" -ForegroundColor Green -Write-Host " Local Development Started!" -ForegroundColor Green -Write-Host "=====================================" -ForegroundColor Green -Write-Host "`nApplication: " -NoNewline -ForegroundColor Cyan -Write-Host "http://localhost:5173" -ForegroundColor White -Write-Host "Backend API: " -NoNewline -ForegroundColor Cyan -Write-Host "http://localhost:8080/api/*" -ForegroundColor White -Write-Host "Backend Root: " -NoNewline -ForegroundColor Cyan -Write-Host "http://localhost:8080/" -ForegroundColor White - -Write-Host "`nAuthenticated APIs require MSAL-issued tokens (scope: Chat.ReadWrite)." -ForegroundColor Gray - -Write-Host "`nFeatures:" -ForegroundColor Yellow -Write-Host " • React Hot Module Replacement (instant updates)" -ForegroundColor Gray -Write-Host " • .NET watch mode (auto-recompile)" -ForegroundColor Gray -Write-Host " • MSAL authentication with your Entra app" -ForegroundColor Gray -Write-Host " • AI Agent Service integration" -ForegroundColor Gray - -Write-Host "`nTo deploy to Azure:" -ForegroundColor Yellow -Write-Host " azd deploy" -ForegroundColor White - -Write-Host "`nTo stop:" -ForegroundColor Yellow -Write-Host " Close the backend and frontend terminal windows" -ForegroundColor White -Write-Host " Or press Ctrl+C in each terminal" -ForegroundColor White - -Write-Host "`n" +Write-Host "`n[OK] Dev servers started" -ForegroundColor Green +Write-Host " Frontend: http://localhost:5173" -ForegroundColor Cyan +Write-Host " Backend: http://localhost:8080" -ForegroundColor Cyan diff --git a/deployment/scripts/test-cli-compatibility.ps1 b/deployment/scripts/test-cli-compatibility.ps1 new file mode 100644 index 0000000..5fb6fd0 --- /dev/null +++ b/deployment/scripts/test-cli-compatibility.ps1 @@ -0,0 +1,121 @@ +#!/usr/bin/env pwsh +# Tests Copilot CLI compatibility with this repo's skills and MCP servers. +# Usage: ./deployment/scripts/test-cli-compatibility.ps1 [-Verbose] [-SkipMcp] + +param( + [switch]$SkipMcp, + [int]$TimeoutSeconds = 60 +) + +$ErrorActionPreference = "Stop" + +# --- Helpers --- + +function Write-TestHeader { param([string]$Name); Write-Host "`n[$Name]" -ForegroundColor Cyan } +function Write-Pass { param([string]$Msg); Write-Host " PASS: $Msg" -ForegroundColor Green } +function Write-Fail { param([string]$Msg); Write-Host " FAIL: $Msg" -ForegroundColor Red; $script:failures++ } +function Write-Info { param([string]$Msg); Write-Host " INFO: $Msg" -ForegroundColor DarkGray } + +function Invoke-CopilotPrompt { + param( + [Parameter(Mandatory)][string]$Prompt, + [switch]$AllowTools + ) + $args_ = @("-p", $Prompt, "-s", "--no-auto-update") + if ($AllowTools) { $args_ += "--allow-all-tools" } + $output = & copilot @args_ 2>&1 | Out-String + return $output.Trim() +} + +# --- Pre-flight --- + +$script:failures = 0 + +Write-Host "=== Copilot CLI Compatibility Test ===" -ForegroundColor White +Write-Host "Repo: foundry-agent-webapp" -ForegroundColor DarkGray +Write-Host "Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor DarkGray + +# Check copilot is installed +if (-not (Get-Command copilot -EA SilentlyContinue)) { + Write-Host "`n[FATAL] 'copilot' command not found. Install: https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli" -ForegroundColor Red + exit 1 +} + +# --- Test 1: Version --- + +Write-TestHeader "1. CLI Version" +$version = copilot --version 2>&1 | Select-String "GitHub Copilot CLI" | ForEach-Object { $_.ToString().Trim() } +if ($version) { + Write-Pass $version +} else { + Write-Fail "Could not determine version" +} + +# --- Test 2: Custom Instructions --- + +Write-TestHeader "2. Custom Instructions" +$response = Invoke-CopilotPrompt "COMPATIBILITY TEST: Do you see custom instructions for this repo? Reply with ONLY the project name from the instructions, nothing else." +Write-Info "Response: $response" +if ($response -match "foundry-agent-webapp") { + Write-Pass "Custom instructions loaded (.github/copilot-instructions.md)" +} else { + Write-Fail "Custom instructions not detected in response" +} + +# --- Test 3: Skills --- + +Write-TestHeader "3. Skills" +# Dynamic skill discovery +$skillCount = (Get-ChildItem ".github/skills/*/SKILL.md" -ErrorAction SilentlyContinue).Count +$expectedSkills = (Get-ChildItem ".github/skills/*/SKILL.md" -ErrorAction SilentlyContinue) | ForEach-Object { $_.Directory.Name } +$response = Invoke-CopilotPrompt "COMPATIBILITY TEST: List all skill folder names from .github/skills/. Reply with ONLY the folder names, one per line, no numbering." +Write-Info "Response: $response" +$found = 0 +foreach ($skill in $expectedSkills) { + if ($response -match [regex]::Escape($skill)) { + $found++ + } else { + Write-Fail "Skill not found: $skill" + } +} +if ($found -eq $skillCount) { + Write-Pass "All $found/$skillCount skills visible" +} else { + Write-Fail "Only $found/$skillCount skills visible" +} + +# --- Test 4: MCP Servers --- + +if (-not $SkipMcp) { + Write-TestHeader "4. MCP Servers" + $expectedServers = @("playwright", "microsoftdocs") + $response = Invoke-CopilotPrompt -Prompt "COMPATIBILITY TEST: List ALL MCP server names you have access to. Reply with ONLY the server names, one per line, no descriptions." -AllowTools + Write-Info "Response: $response" + $found = 0 + foreach ($server in $expectedServers) { + if ($response -match $server) { + $found++ + } else { + Write-Fail "MCP server not found: $server (run syncing-mcp-servers skill first)" + } + } + if ($found -eq $expectedServers.Count) { + Write-Pass "All $found/$($expectedServers.Count) repo MCP servers connected" + } else { + Write-Fail "Only $found/$($expectedServers.Count) repo MCP servers connected" + } +} else { + Write-TestHeader "4. MCP Servers (SKIPPED)" + Write-Info "Use -SkipMcp:$false to include MCP tests" +} + +# --- Summary --- + +Write-Host "`n=== Results ===" -ForegroundColor White +if ($script:failures -eq 0) { + Write-Host "ALL TESTS PASSED" -ForegroundColor Green + exit 0 +} else { + Write-Host "$($script:failures) FAILURE(S)" -ForegroundColor Red + exit 1 +} diff --git a/deployment/scripts/validate-config.ps1 b/deployment/scripts/validate-config.ps1 index 83df33f..e31f714 100644 --- a/deployment/scripts/validate-config.ps1 +++ b/deployment/scripts/validate-config.ps1 @@ -1,132 +1,50 @@ #!/usr/bin/env pwsh -<# -.SYNOPSIS - Validates local development configuration files - -.DESCRIPTION - Checks that frontend/.env.local and backend/WebApp.Api/.env - exist and contain valid (non-placeholder) configuration values. - - Returns exit code 0 on success, 1 on failure. - -.EXAMPLE - .\scripts\validate-config.ps1 -#> +# Validates local development configuration files $ErrorActionPreference = "Stop" - -$validationErrors = @() $projectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent - -# --- Helper Functions --- - -function Write-ValidationError { - param([string]$Message) - Write-Host " [ERROR] $Message" -ForegroundColor Red -} - -function Write-ValidationSuccess { - param([string]$Message) - Write-Host " [OK] $Message" -ForegroundColor Green -} - -# --- Check Frontend Configuration --- - -Write-Host "`nValidating frontend configuration..." -ForegroundColor Cyan - -$frontendEnv = Join-Path $projectRoot "frontend" ".env.local" -if (-not (Test-Path $frontendEnv)) { - $validationErrors += "Missing: frontend/.env.local" - Write-ValidationError "frontend/.env.local not found" -} else { - $content = Get-Content $frontendEnv -Raw - - # Check VITE_ENTRA_SPA_CLIENT_ID - if ($content -match "VITE_ENTRA_SPA_CLIENT_ID=(\S+)") { - $clientId = $matches[1] - if ([string]::IsNullOrWhiteSpace($clientId) -or $clientId -match "PLACEHOLDER") { - $validationErrors += "frontend/.env.local has invalid VITE_ENTRA_SPA_CLIENT_ID" - Write-ValidationError "VITE_ENTRA_SPA_CLIENT_ID is invalid or placeholder" - } else { - Write-ValidationSuccess "VITE_ENTRA_SPA_CLIENT_ID is valid" - } +$validationErrors = [System.Collections.ArrayList]::new() + +function Test-EnvVar { + param([string]$content, [string]$name, [string]$file) + $pattern = "$name=(\S+)" + if ($content -match $pattern -and $matches[1] -notmatch "PLACEHOLDER") { + Write-Host (" OK: " + $name) -ForegroundColor Green + return $true } else { - $validationErrors += "frontend/.env.local missing VITE_ENTRA_SPA_CLIENT_ID" - Write-ValidationError "VITE_ENTRA_SPA_CLIENT_ID not found" - } - - # Check VITE_ENTRA_TENANT_ID - if ($content -match "VITE_ENTRA_TENANT_ID=(\S+)") { - $tenantId = $matches[1] - if ([string]::IsNullOrWhiteSpace($tenantId) -or $tenantId -match "PLACEHOLDER") { - $validationErrors += "frontend/.env.local has invalid VITE_ENTRA_TENANT_ID" - Write-ValidationError "VITE_ENTRA_TENANT_ID is invalid or placeholder" - } else { - Write-ValidationSuccess "VITE_ENTRA_TENANT_ID is valid" - } - } else { - $validationErrors += "frontend/.env.local missing VITE_ENTRA_TENANT_ID" - Write-ValidationError "VITE_ENTRA_TENANT_ID not found" + Write-Host (" ERROR: " + $name) -ForegroundColor Red + return $false } } -# --- Check Backend Configuration --- - -Write-Host "`nValidating backend configuration..." -ForegroundColor Cyan - -$backendEnv = Join-Path $projectRoot "backend" "WebApp.Api" ".env" -if (-not (Test-Path $backendEnv)) { - $validationErrors += "Missing: backend/WebApp.Api/.env" - Write-ValidationError "backend/WebApp.Api/.env not found" +# Frontend +Write-Host "Frontend config:" -ForegroundColor Cyan +$frontendEnv = Join-Path $projectRoot "frontend/.env.local" +if (Test-Path $frontendEnv) { + $content = Get-Content $frontendEnv -Raw + if (-not (Test-EnvVar $content "VITE_ENTRA_SPA_CLIENT_ID" "frontend/.env.local")) { $null = $validationErrors.Add("VITE_ENTRA_SPA_CLIENT_ID") } + if (-not (Test-EnvVar $content "VITE_ENTRA_TENANT_ID" "frontend/.env.local")) { $null = $validationErrors.Add("VITE_ENTRA_TENANT_ID") } } else { - $content = Get-Content $backendEnv -Raw - - # Check AzureAd__TenantId (double underscore is .NET environment variable format for nested config) - if ($content -match "AzureAd__TenantId=(\S+)") { - $tenantId = $matches[1] - if ([string]::IsNullOrWhiteSpace($tenantId) -or $tenantId -match "PLACEHOLDER") { - $validationErrors += "backend/WebApp.Api/.env has invalid AzureAd__TenantId" - Write-ValidationError "AzureAd__TenantId is invalid or placeholder" - } else { - Write-ValidationSuccess "AzureAd__TenantId is valid" - } - } else { - $validationErrors += "backend/WebApp.Api/.env missing AzureAd__TenantId" - Write-ValidationError "AzureAd__TenantId not found" - } - - # Check AzureAd__ClientId - if ($content -match "AzureAd__ClientId=(\S+)") { - $clientId = $matches[1] - if ([string]::IsNullOrWhiteSpace($clientId) -or $clientId -match "PLACEHOLDER") { - $validationErrors += "backend/WebApp.Api/.env has invalid AzureAd__ClientId" - Write-ValidationError "AzureAd__ClientId is invalid or placeholder" - } else { - Write-ValidationSuccess "AzureAd__ClientId is valid" - } - } else { - $validationErrors += "backend/WebApp.Api/.env missing AzureAd__ClientId" - Write-ValidationError "AzureAd__ClientId not found" - } + $null = $validationErrors.Add("frontend/.env.local not found") + Write-Host " [ERROR] File not found" -ForegroundColor Red } -# --- Report Results --- +# Backend +Write-Host "Backend config:" -ForegroundColor Cyan +$backendEnv = Join-Path $projectRoot "backend/WebApp.Api/.env" +if (Test-Path $backendEnv) { + $content = Get-Content $backendEnv -Raw + if (-not (Test-EnvVar $content "AzureAd__TenantId" "backend/.env")) { $null = $validationErrors.Add("AzureAd__TenantId") } + if (-not (Test-EnvVar $content "AzureAd__ClientId" "backend/.env")) { $null = $validationErrors.Add("AzureAd__ClientId") } +} else { + $null = $validationErrors.Add("backend/.env not found") + Write-Host " [ERROR] File not found" -ForegroundColor Red +} -Write-Host "" if ($validationErrors.Count -gt 0) { - Write-Host "❌ Configuration Validation Failed" -ForegroundColor Red - Write-Host "" - Write-Host "Errors found:" -ForegroundColor Yellow - foreach ($validationError in $validationErrors) { - Write-Host " • $validationError" -ForegroundColor Red - } - Write-Host "" - Write-Host "To fix this, run:" -ForegroundColor Yellow - Write-Host " azd up" -ForegroundColor White - Write-Host "" + Write-Host "`n[ERROR] Validation failed. Run 'azd up' to fix." -ForegroundColor Red exit 1 } -Write-Host "✅ Configuration validated successfully" -ForegroundColor Green -Write-Host "" +Write-Host "`n[OK] Configuration valid" -ForegroundColor Green exit 0 diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..a3214a7 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,3 @@ +# React 19 peer-dep declarations lag behind actual compatibility; without this, npm install fails. +# See README.md "Known Limitations" for details. +legacy-peer-deps=true diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md deleted file mode 100644 index 07f659e..0000000 --- a/frontend/AGENTS.md +++ /dev/null @@ -1,202 +0,0 @@ -# Frontend - React + TypeScript + Vite - -**Context**: See `.github/copilot-instructions.md` for overall architecture. - -This folder contains the single-page chat interface served by the ASP.NET Core backend. Technology stack: - -- **React 19** + TypeScript -- **Fluent UI v9** and Fluent Copilot chat components -- **MSAL.js** for authentication (PKCE flow, redirect pattern) -- **Vite** for dev server with HMR - -## Architecture - -| Concern | Implementation | -|---------|----------------| -| **State Management** | Centralized Context + `useReducer` (`AppContext`) with discriminated action union | -| **Authentication** | MSAL redirect flow; silent token refresh; `useAuth` hook for token acquisition | -| **Chat Streaming** | Server-Sent Events (SSE) in `ChatService` with abort controllers for cancellation | -| **Accessibility** | Live region (`aria-live`) for assistant updates, proper aria labels, focus management | -| **Error Handling** | Error boundary (`ErrorBoundary`) + structured error actions with retry support | -| **Logging** | Dev-only diff-based logger (no verbose production console noise) | -| **Performance** | Memoized message components and stable service instances via `useMemo` | - -## Environment Variables - -Required at build/runtime (auto-generated in `.env` after `azd up`): - -```bash -VITE_ENTRA_SPA_CLIENT_ID=... -VITE_ENTRA_TENANT_ID=... -VITE_API_URL=/api -``` - -**Critical**: Access `import.meta.env.*` at module level only (not inside functions). - -## Key Components - -| Component | Purpose | -|-----------|---------|| -| `AgentPreview.tsx` | Container wiring chat state to controlled `ChatInterface` | -| `ChatInterface.v2.tsx` | Stateless controlled UI; renders messages, input, errors | -| `chat/AssistantMessage.tsx` | Memoized assistant message with streaming support | -| `chat/UserMessage.tsx` | Memoized user message with image thumbnail previews | -| `chat/ChatInput.tsx` | File uploads (select + paste + drag), character counter, cancel streaming button, focus management | -| `chat/FilePreview.tsx` | Thumbnail preview grid for attached files before sending | -| `core/ErrorBoundary.tsx` | Catches runtime errors and displays fallback UI | -| `core/Markdown.tsx` | Sanitized markdown rendering with syntax highlighting | -| `core/AgentIcon.tsx` | Agent avatar with optional custom logo URL support | - -## File Upload Validation - -**Limits**: 5MB per file, max 5 files total - -**Supported formats**: PNG, JPEG, GIF, WebP - -**See**: -- `frontend/src/utils/fileAttachments.ts` for `validateImageFile()` and `validateFileCount()` functions -- `frontend/src/components/chat/ChatInput.tsx` for usage in file select, paste, and drag handlers - -**Key points**: -- User-friendly error messages (e.g., "file.jpg is 8.5MB. Maximum file size is 5MB") -- Toast notifications for validation feedback -- Separate validation functions for count and individual files - -## Character Counter - -**See**: `frontend/src/components/chat/ChatInput.tsx` - -**Thresholds**: 3000 (warning), 3500 (danger), 4000 (recommended max) - -**Behavior**: -- Counter appears at 3000+ characters -- Color changes from yellow to orange as limit approaches -- Informational only - doesn't block submission -- Linked to input via `aria-describedby` for accessibility - -## Agent Logo Support - -**See**: -- `frontend/src/components/core/AgentIcon.tsx` for logo rendering logic -- `frontend/src/components/AgentPreview.tsx`, `ChatInterface.tsx`, `StarterMessages.tsx`, `AssistantMessage.tsx` for prop threading - -**Pattern**: Optional `agentLogo` URL passed through component tree, falls back to default bot icon if not provided. - -## MSAL Configuration - -**See**: -- `frontend/src/config/authConfig.ts` for MSAL configuration -- `frontend/src/hooks/useAuth.ts` for token acquisition pattern -- `.github/instructions/typescript.instructions.md` for detailed patterns - -**Key points**: -- Environment variables accessed at module level only -- Token acquisition: silent first, fallback to popup -- Authorization header format: `Bearer ${token}` - -## Action Flow (Send Message) - -``` -CHAT_SEND_MESSAGE - → CHAT_ADD_ASSISTANT_MESSAGE - → CHAT_START_STREAM - → (repeat CHAT_STREAM_CHUNK) - → CHAT_STREAM_COMPLETE -``` - -If user cancels: `CHAT_CANCEL_STREAM` sets status back to `idle` and re-enables input. - -## Adding a New Feature - -1. **Extend state**: Add discriminated action to `AppAction` union in `types/appState.ts` -2. **Handle in reducer**: Update `appReducer.ts` (keep pure, no side effects) -3. **Create service method**: Add to `ChatService` if network interaction needed -4. **Wire container**: Update `AgentPreview.tsx` to dispatch actions -5. **Update UI**: Pass callbacks to controlled component (`ChatInterface.v2.tsx`) -6. **Test**: Validate with local dev and check console diff logs - -## Error Handling - -All recoverable errors dispatch `CHAT_ERROR` with `AppError` containing: -- Error message -- Optional retry action -- Timestamp - -Clear errors with `CHAT_CLEAR_ERROR`. - -## Dev Logging - -Visible only in development mode. Each state change prints: - -``` -🔄 [HH:MM:SS] ACTION_TYPE -Action: { … } -Changes: { field: before → after } -``` - -## Accessibility Checklist - -- ✅ Live region announces latest assistant message -- ✅ Live region announces streaming status changes ("Assistant is responding") -- ✅ `aria-busy` attribute on messages container during streaming -- ✅ Buttons have `aria-label` when icon-only -- ✅ Focus returns to input after sending -- ✅ File removal buttons announce target file name -- ✅ Loading states announced to screen readers -- ✅ Character counter linked via `aria-describedby` -- ✅ File preview list has `role="list"` and `aria-label` - -## Local Development - -Use unified script (recommended): - -```powershell -.\deployment\scripts\start-local-dev.ps1 -``` - -Or manually: - -```powershell -# Backend -cd backend/WebApp.Api -dotnet run - -# Frontend (separate terminal) -cd frontend -npm run dev -``` - -Open `http://localhost:5173` (Vite proxies `/api` to `http://localhost:8080`). - -## Vite Proxy Configuration - -```typescript -export default defineConfig({ - server: { - port: 5173, - proxy: { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true - } - } - } -}); -``` - -## Building for Production - -```powershell -npm run build -``` - -Outputs static assets to `dist/` (copied to `wwwroot` in Docker build). - -## Contributing - -Follow existing patterns: -- **Controlled components** for UI -- **Context-driven state** management -- **Pure reducer** functions -- **Service isolation** for API calls -- Test streaming scenarios before committing diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..146e689 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,176 @@ +# Frontend - React + TypeScript + Vite + +**AI Assistance**: See `.github/skills/writing-typescript-code/SKILL.md` for coding patterns. + +## Overview + +React 19 single-page application with: +- TypeScript for type safety +- Vite for fast development (HMR) and optimized builds +- MSAL.js for Microsoft Entra ID authentication (PKCE flow) +- Server-Sent Events (SSE) for real-time chat streaming +- CSS Modules for scoped styling + +## Key Features + +### Chat Experience +- **SSE Streaming** — Real-time token-by-token response streaming with cancellation support +- **Stream Retry** — Automatic retry (3×) with exponential backoff; failed messages restore to input +- **Message Queue** — Type-ahead during streaming; queued messages shown as dismissible chips, auto-sent when stream completes +- **Smart Auto-Scroll** — Auto-scrolls only when near bottom; "↓ New messages" pill when scrolled up +- **Tool-Use Indicators** — Inline "Searching files...", "Running code..." during agent tool execution + +### Message Actions +- **Copy** — Copy full assistant response to clipboard +- **Regenerate** — Resend last message for a new response (↻ button) +- **Edit** — Edit last user message (removes messages, restores text to input with undo) +- **Feedback** — 👍👎 per-message rating tracked to Application Insights + +### Input +- **File Attachments** — Images and documents via paste (Ctrl+V), drag-and-drop, or file picker +- **Voice Input** — Web Speech API microphone with feature detection and error feedback +- **Keyboard Shortcuts** — ⌨️ button shows shortcuts; Ctrl+N new chat, Escape cancel +- **Toolbar** — Primary actions always visible (attach, cancel, voice, new chat); secondary actions (history, export, shortcuts, settings) in ⋯ overflow menu + +### Conversations +- **History Sidebar** — Browse and resume past conversations; accessed via ⋯ menu +- **Search** — Filter conversations by title in sidebar +- **Export** — Download conversation as Markdown; accessed via ⋯ menu +- **Delete** — Remove conversations from history + +### Foundation +- **Authentication** — MSAL.js with silent token refresh + popup fallback +- **Accessibility** — ARIA live regions, keyboard navigation, focus management, screen reader announcements +- **Performance** — React 19 `useDeferredValue` for responsive input during streaming +- **Theming** — Light/dark/system theme with Fluent UI design tokens +- **Citations** — Inline citation markers with footnote navigation and source links + +## Project Structure + +``` +frontend/src/ +├── App.tsx # Root with MsalProvider +├── main.tsx # Entry point +├── components/ +│ ├── AgentChat.tsx # Container (state → ChatInterface) +│ ├── ChatInterface.tsx # Controlled chat UI +│ └── chat/ # Chat subcomponents +│ ├── AssistantMessage.tsx # Streaming message + citations +│ ├── UserMessage.tsx # User message + image previews +│ ├── ChatInput.tsx # Input + file upload + cancel +│ └── CitationMarker.tsx # Inline citation badge +├── services/ +│ └── chatService.ts # SSE streaming + API calls +├── contexts/ +│ └── AppContext.tsx # Centralized state (useReducer) +├── hooks/ +│ ├── useAuth.ts # MSAL token acquisition +│ └── useAppState.ts # State access hook +├── reducers/ +│ └── appReducer.ts # State transitions +├── config/ +│ └── authConfig.ts # MSAL configuration +├── types/ # TypeScript interfaces +└── utils/ # Helpers (citations, files, etc.) +``` + +## Running Locally + +### Prerequisites +- Node.js 18+ +- `.env.local` file generated (run `azd up` first) + +### Start + +**Option 1: VS Code task (recommended)** +- Run task `Frontend: React Vite` - dependencies are installed automatically + +**Option 2: Manual** +```powershell +cd frontend +npm install # .npmrc sets legacy-peer-deps automatically +npm run dev +``` + +Frontend runs at http://localhost:5173 with Hot Module Replacement (HMR). + +### Configuration + +`.env.local` file (auto-generated by `azd up`): +``` +VITE_ENTRA_SPA_CLIENT_ID=... +VITE_ENTRA_TENANT_ID=... +``` + +**CRITICAL**: Environment variables are replaced at build time. Access them at module level only: +```typescript +// ✅ Correct - module level +const clientId = import.meta.env.VITE_ENTRA_SPA_CLIENT_ID; + +// ❌ Wrong - inside function (won't work after build) +function getClientId() { + return import.meta.env.VITE_ENTRA_SPA_CLIENT_ID; +} +``` + +## Development Tips + +- **Hot reload**: Vite HMR updates browser instantly on save +- **React 19**: `.npmrc` handles peer-dep conflicts automatically — just run `npm install` from `frontend/` +- **State debugging**: Console shows `🔄 ACTION_TYPE` for each state change (dev only) +- **CORS**: Backend allows `http://localhost:5173` in dev mode + +## Building + +```powershell +npm run build # Production build → dist/ +npm run preview # Preview production build locally +``` + +Production builds are created in Docker multi-stage builds and served from ASP.NET Core's `wwwroot`. + +## Key Dependencies + +| Package | Purpose | +|---------|---------| +| react | UI framework | +| @azure/msal-react | Entra ID authentication | +| @azure/msal-browser | MSAL.js browser library | +| @fluentui/react-components | Fluent UI components | +| vite | Build tool + dev server | + +See `package.json` for current versions. + +## Component Architecture + +| Component | Role | Pattern | +|-----------|------|---------| +| `AgentChat` | Container | Wires state to controlled component | +| `ChatInterface` | Presentation | Stateless, receives props + callbacks | +| `ChatInput` | Controlled input | Manages local input state, calls parent handlers | +| `AssistantMessage` | Memoized | Renders streaming text + citations | + +## State Flow + +``` +User Action + → dispatch(action) + → appReducer (pure function) + → new state + → React re-render + → UI update +``` + +**Action types**: `CHAT_SEND_MESSAGE` → `CHAT_START_STREAM` → `CHAT_STREAM_CHUNK` (×N) → `CHAT_STREAM_COMPLETE` + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Module not found | Run `npm install` from `frontend/` (`.npmrc` handles peer deps) | +| Auth popup blocked | Allow popups for localhost in browser | +| Stale cache | Delete `node_modules/.vite` and restart | +| HMR not working | Check Vite terminal for errors | +| 401 on API calls | Verify `.env.local` has correct client ID | + +For AI-assisted development, see `.github/skills/writing-typescript-code/SKILL.md` and `.github/skills/implementing-chat-streaming/SKILL.md`. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index b19330b..166a18c 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -12,9 +12,15 @@ export default defineConfig([ extends: [ js.configs.recommended, tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], reactRefresh.configs.vite, ], + plugins: { + 'react-hooks': reactHooks, + }, + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, languageOptions: { ecmaVersion: 2020, globals: globals.browser, diff --git a/frontend/index.html b/frontend/index.html index 2b8cc54..da70aca 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,9 +3,9 @@ - + - Azure AI Agent + AI Agent
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e45ab00..67ee62f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,75 +8,108 @@ "name": "ai-foundry-agent-frontend", "version": "0.0.0", "dependencies": { - "@azure/msal-browser": "^4.26.0", - "@azure/msal-react": "^3.0.21", - "@fluentui-copilot/react-copilot": "0.30.0", - "@fluentui-copilot/react-copilot-chat": "0.13.0", - "@fluentui/react-components": "^9.72.4", - "@fluentui/react-icons": "^2.0.313", + "@azure/msal-browser": "^4.27.0", + "@azure/msal-react": "^3.0.23", + "@fluentui-copilot/react-copilot": "0.30.5", + "@fluentui-copilot/react-copilot-chat": "0.13.2", + "@fluentui/react-components": "^9.73.7", + "@fluentui/react-icons": "^2.0.324", + "@microsoft/applicationinsights-web": "^3.4.1", "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "date-fns": "^4.1.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", + "react": "^19.2.5", + "react-dom": "^19.2.5", "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^16.1.0", + "react-syntax-highlighter": "^16.1.1", "rehype-highlight": "^7.0.2", "rehype-sanitize": "^6.0.0", "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "yjs": "^13.6.30" }, "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", + "@eslint/js": "^10.0.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.22", - "globals": "^16.5.0", + "@vitejs/plugin-react": "^5.2.0", + "@vitest/coverage-v8": "^4.1.5", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "jsdom": "^26.1.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.46.3", - "vite": "^7.2.0" + "typescript-eslint": "^8.59.0", + "vite": "^7.2.6", + "vitest": "^4.1.5" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@azure/msal-browser": { - "version": "4.26.0", + "version": "4.29.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.29.0.tgz", + "integrity": "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w==", "license": "MIT", - "peer": true, "dependencies": { - "@azure/msal-common": "15.13.1" + "@azure/msal-common": "15.15.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "15.13.1", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.15.0.tgz", + "integrity": "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw==", "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-react": { - "version": "3.0.21", + "version": "3.0.27", + "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-3.0.27.tgz", + "integrity": "sha512-EKXCyUM2Yye7w3D50FCD19YO7dVkoTJAeTRtMaPKlh5K9oH94ded27sxAgI177COLaN/ZaHHSm8fmvv3kIYH4w==", "license": "MIT", "engines": { "node": ">=10" }, "peerDependencies": { - "@azure/msal-browser": "^4.26.0", - "react": "^16.8.0 || ^17 || ^18 || ^19" + "@azure/msal-browser": "^4.29.0", + "react": "^16.8.0 || ^17 || ^18 || ^19.2.1" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -85,7 +118,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -93,20 +128,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -123,12 +159,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -138,11 +176,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -154,6 +194,8 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { @@ -161,25 +203,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -189,7 +235,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -198,6 +246,8 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -206,6 +256,8 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -214,6 +266,8 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -221,23 +275,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -248,6 +306,8 @@ }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "license": "MIT", "dependencies": { @@ -262,6 +322,8 @@ }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "license": "MIT", "dependencies": { @@ -275,36 +337,42 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.4", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.27.2", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -312,7 +380,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -323,8 +393,135 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", "license": "MIT", "engines": { "node": ">=10" @@ -332,12 +529,14 @@ }, "node_modules/@emotion/hash": { "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -352,9 +551,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -369,9 +568,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -386,9 +585,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -403,9 +602,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -420,9 +619,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -437,9 +636,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -454,9 +653,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -471,9 +670,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -488,9 +687,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -505,9 +704,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -522,9 +721,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -539,9 +738,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -556,9 +755,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -573,9 +772,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -590,9 +789,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -607,9 +806,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -624,9 +823,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -641,9 +840,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -658,9 +857,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -675,9 +874,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -692,9 +891,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -709,9 +908,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -726,9 +925,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -743,9 +942,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -760,7 +959,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -775,7 +976,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -793,6 +996,8 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -804,6 +1009,8 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -811,226 +1018,210 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/js": { - "version": "9.39.1", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/devtools": { "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.2.3.tgz", + "integrity": "sha512-ZTcxTvgo9CRlP7vJV62yCxdqmahHTGpSTi5QaTDgGoyQq0OyjaVZhUhXv/qdkQFOI3Sxlfmz0XGG4HaZMsDf8Q==", "license": "MIT", "peerDependencies": { "@floating-ui/dom": "^1.0.0" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "license": "MIT", - "peer": true, - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "license": "MIT" - }, - "node_modules/@fluentui-copilot/chat-input-plugins": { - "version": "0.5.3", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@fluentui-copilot/text-editor": "^0.3.3", - "@swc/helpers": "^0.5.1" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, - "node_modules/@fluentui-copilot/flair": { - "version": "0.5.0", + "node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", "license": "MIT", "dependencies": { - "@fluentui-contrib/houdini-utils": "^0.5.1", - "@fluentui-copilot/tokens": "^0.3.15", - "@swc/helpers": "^0.5.1" + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" } }, - "node_modules/@fluentui-copilot/flair/node_modules/@fluentui-contrib/houdini-utils": { - "version": "0.5.1", + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@swc/helpers": "~0.5.11" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@fluentui-copilot/flair/node_modules/@types/react": { - "version": "18.3.26", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", - "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" }, - "node_modules/@fluentui-copilot/flair/node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "node_modules/@fluentui-contrib/houdini-utils": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@fluentui-contrib/houdini-utils/-/houdini-utils-0.5.2.tgz", + "integrity": "sha512-8Vrv9P/c4SGUT40jlVEXL8F/05eKJWTQFNA0xciXIezbJxHVdUM7hc+0wcRW1Xum6fgtOh5S5R7IShfD/HPV2w==", "license": "MIT", - "peer": true, + "dependencies": { + "@swc/helpers": "~0.5.11" + }, "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": ">=16.8.0 <20.0.0", + "@types/react-dom": ">=16.8.0 <20.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" } }, - "node_modules/@fluentui-copilot/flair/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "node_modules/@fluentui-copilot/chat-input-plugins": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/chat-input-plugins/-/chat-input-plugins-0.5.4.tgz", + "integrity": "sha512-Du5TNiwo4j8VM4Joa8WEejPte30tmTjqhFnYBoqruRsd1/6hg58Yg1uYN4HBwIF9IsNLusfNYowo4gVQNi0vXQ==", "license": "MIT", - "peer": true, "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" + "@fluentui-copilot/text-editor": "^0.3.4", + "@swc/helpers": "^0.5.1" } }, - "node_modules/@fluentui-copilot/flair/node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "node_modules/@fluentui-copilot/flair": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/flair/-/flair-0.5.1.tgz", + "integrity": "sha512-wsf4y/gqsMJTiABpqaXzzh5RzGtY7cQsHYOSWbGhn63Tl5u8fpHMfe9QLk67w9XoGqP1jptxpwMZ4/wHN2G/TQ==", "license": "MIT", - "peer": true, "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" + "@fluentui-contrib/houdini-utils": "^0.5.1", + "@fluentui-copilot/tokens": "^0.3.15", + "@swc/helpers": "^0.5.1" } }, - "node_modules/@fluentui-copilot/flair/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "node_modules/@fluentui-copilot/pulsing-dot": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/pulsing-dot/-/pulsing-dot-0.1.2.tgz", + "integrity": "sha512-Bg/lFwvQ38yJcIWE9JFl2uAZzfktzz1sOz8jOrdAt/gT3sVSAdWNsB8zXeDOTtxuRbTYM1yIJPJLu0f9t9gQdg==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0" + "@fluentui-contrib/houdini-utils": "^0.5.1", + "@fluentui-copilot/tokens": "^0.3.15", + "@swc/helpers": "^0.5.1" } }, "node_modules/@fluentui-copilot/react-announce": { - "version": "0.5.11", + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-announce/-/react-announce-0.5.12.tgz", + "integrity": "sha512-h/dWpdvSNDNQt3H+2YmB6/1ji0N02H0lNTrEbM4tGqsDihPMytynSj/r3bqZzMuWZwO0gISKE/dkvvkC2iMfOw==", "license": "MIT", "dependencies": { "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-shared-contexts": ">=9.25.1 <10.0.0", "@fluentui/react-utilities": ">=9.24.1 <10.0.0", @@ -1041,17 +1232,19 @@ } }, "node_modules/@fluentui-copilot/react-attachments": { - "version": "0.13.7", + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-attachments/-/react-attachments-0.13.8.tgz", + "integrity": "sha512-a5fDmv0rYJUJi5it7H3mPTLm2lL+15SeKz8RMGhl02qnGNK397jMWfDvb4cml8/KvSV7+UrT1HbgjB80yotFlA==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-utilities": "~0.0.11", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-utilities": "~0.0.12", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1064,18 +1257,21 @@ } }, "node_modules/@fluentui-copilot/react-capability-picker": { - "version": "0.1.0", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-capability-picker/-/react-capability-picker-0.1.1.tgz", + "integrity": "sha512-bCtUHiEHp+c6HSyBIbPl2/8j3APdu5aGPVUbNlgdfJB2TljVPWQZAP9J78P0kVReNkRDc+TYP7XxDtcxsyqqsg==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/react-aria": ">=9.17.0 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", + "@fluentui/react-utilities": ">=9.24.1 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.8 <20.0.0", "react": ">=16.14.0 <20.0.0", @@ -1083,23 +1279,25 @@ } }, "node_modules/@fluentui-copilot/react-chat-input": { - "version": "0.16.0", - "license": "MIT", - "dependencies": { - "@fluentui-copilot/chat-input-plugins": "^0.5.3", - "@fluentui-copilot/react-chat-input-plugins": "^0.5.7", - "@fluentui-copilot/react-editor-input": "^0.6.0", - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-send-button": "^0.1.6", - "@fluentui-copilot/react-text-editor": "^0.4.3", - "@fluentui-copilot/react-utilities": "~0.0.11", - "@fluentui-copilot/text-editor": "^0.3.3", + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-chat-input/-/react-chat-input-0.16.3.tgz", + "integrity": "sha512-Dy0tu/sAfNxEoHwExX6oUg7IyiMDHzx/uXEDWXA0aTbmNnFd7VslH5PsxSLTp0H7g3Z28EhlpVHmw5l7yr9fFA==", + "license": "MIT", + "dependencies": { + "@fluentui-copilot/chat-input-plugins": "^0.5.4", + "@fluentui-copilot/react-chat-input-plugins": "^0.5.9", + "@fluentui-copilot/react-editor-input": "^0.6.2", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-send-button": "^0.1.7", + "@fluentui-copilot/react-text-editor": "^0.4.4", + "@fluentui-copilot/react-utilities": "~0.0.12", + "@fluentui-copilot/text-editor": "^0.3.4", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1113,17 +1311,19 @@ } }, "node_modules/@fluentui-copilot/react-chat-input-plugins": { - "version": "0.5.7", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-chat-input-plugins/-/react-chat-input-plugins-0.5.9.tgz", + "integrity": "sha512-n3Bp/fTM/I22pot7KO7dT0I/u0IIkaA+bBraRwlCofPfHq1yqJWUljZN6igJ6k7yUORC2DDBkUBsyGBID04QwA==", "license": "MIT", "dependencies": { - "@fluentui-copilot/chat-input-plugins": "^0.5.3", - "@fluentui-copilot/react-text-editor": "^0.4.3", - "@fluentui-copilot/text-editor": "^0.3.3", + "@fluentui-copilot/chat-input-plugins": "^0.5.4", + "@fluentui-copilot/react-text-editor": "^0.4.4", + "@fluentui-copilot/text-editor": "^0.3.4", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-utilities": ">=9.24.1 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", @@ -1133,47 +1333,49 @@ } }, "node_modules/@fluentui-copilot/react-copilot": { - "version": "0.30.0", - "license": "MIT", - "dependencies": { - "@fluentui-copilot/chat-input-plugins": "^0.5.3", - "@fluentui-copilot/react-attachments": "^0.13.7", - "@fluentui-copilot/react-capability-picker": "^0.1.0", - "@fluentui-copilot/react-chat-input": "^0.16.0", - "@fluentui-copilot/react-chat-input-plugins": "^0.5.7", - "@fluentui-copilot/react-copilot-chat": "^0.13.0", - "@fluentui-copilot/react-copilot-nav": "^0.1.3", - "@fluentui-copilot/react-copilot-theme": "^0.1.10", - "@fluentui-copilot/react-editor-input": "^0.6.0", - "@fluentui-copilot/react-entity-cards": "^0.4.7", - "@fluentui-copilot/react-feedback-buttons": "^0.12.6", - "@fluentui-copilot/react-first-run-experience": "^0.9.6", - "@fluentui-copilot/react-flair": "^0.6.0", - "@fluentui-copilot/react-grounding-menu": "^0.2.7", - "@fluentui-copilot/react-latency": "^0.11.7", - "@fluentui-copilot/react-output-card": "^0.13.0", - "@fluentui-copilot/react-preview": "^0.8.6", - "@fluentui-copilot/react-prompt-input": "^0.11.0", - "@fluentui-copilot/react-prompt-listbox": "^0.11.0", - "@fluentui-copilot/react-prompt-starter": "^0.10.8", - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-reference": "^0.16.6", - "@fluentui-copilot/react-response-count": "^0.2.20", - "@fluentui-copilot/react-send-button": "^0.1.6", - "@fluentui-copilot/react-sensitivity-label": "^0.8.6", - "@fluentui-copilot/react-snippet": "^0.2.22", - "@fluentui-copilot/react-suggestions": "^0.13.6", - "@fluentui-copilot/react-text": "^0.1.8", - "@fluentui-copilot/react-text-editor": "^0.4.3", - "@fluentui-copilot/react-textarea": "^0.11.6", - "@fluentui-copilot/text-editor": "^0.3.3", + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-copilot/-/react-copilot-0.30.5.tgz", + "integrity": "sha512-B6jjLi//R3pibh1M21DGe/I94mQWnPj/hYTDyhfhUCZWQJGRfVMog5mprGNYijfuAm6sMG5YRMAWNge1aYvcog==", + "license": "MIT", + "dependencies": { + "@fluentui-copilot/chat-input-plugins": "^0.5.4", + "@fluentui-copilot/react-attachments": "^0.13.8", + "@fluentui-copilot/react-capability-picker": "^0.1.1", + "@fluentui-copilot/react-chat-input": "^0.16.3", + "@fluentui-copilot/react-chat-input-plugins": "^0.5.9", + "@fluentui-copilot/react-copilot-chat": "^0.13.2", + "@fluentui-copilot/react-copilot-nav": "^0.1.5", + "@fluentui-copilot/react-copilot-theme": "^0.1.11", + "@fluentui-copilot/react-editor-input": "^0.6.2", + "@fluentui-copilot/react-entity-cards": "^0.4.8", + "@fluentui-copilot/react-feedback-buttons": "^0.12.7", + "@fluentui-copilot/react-first-run-experience": "^0.9.7", + "@fluentui-copilot/react-flair": "^0.6.2", + "@fluentui-copilot/react-grounding-menu": "^0.2.8", + "@fluentui-copilot/react-latency": "^0.11.9", + "@fluentui-copilot/react-output-card": "^0.13.2", + "@fluentui-copilot/react-preview": "^0.8.7", + "@fluentui-copilot/react-prompt-input": "^0.11.2", + "@fluentui-copilot/react-prompt-listbox": "^0.11.2", + "@fluentui-copilot/react-prompt-starter": "^0.10.9", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-reference": "^0.16.7", + "@fluentui-copilot/react-response-count": "^0.2.21", + "@fluentui-copilot/react-send-button": "^0.1.7", + "@fluentui-copilot/react-sensitivity-label": "^0.8.7", + "@fluentui-copilot/react-snippet": "^0.2.23", + "@fluentui-copilot/react-suggestions": "^0.13.7", + "@fluentui-copilot/react-text": "^0.1.9", + "@fluentui-copilot/react-text-editor": "^0.4.4", + "@fluentui-copilot/react-textarea": "^0.11.7", + "@fluentui-copilot/text-editor": "^0.3.4", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", "@fluentui/react-aria": ">=9.17.0 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1189,16 +1391,18 @@ } }, "node_modules/@fluentui-copilot/react-copilot-chat": { - "version": "0.13.0", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-copilot-chat/-/react-copilot-chat-0.13.2.tgz", + "integrity": "sha512-wzGjGSFPXxk9YYnLzvS6L4Q0wwvFzRaia2RxJsJBJAEKL6VIqOdQOIsIZdSb5wVhRyAlMhTbzBNcZEyKsBf/tQ==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-output-card": "^0.13.0", - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-output-card": "^0.13.2", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1213,19 +1417,22 @@ } }, "node_modules/@fluentui-copilot/react-copilot-nav": { - "version": "0.1.3", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-copilot-nav/-/react-copilot-nav-0.1.5.tgz", + "integrity": "sha512-ntpfINB1zaQCH9GDFbjzq6jpgbLG/9XNHNU3/0zmGiWO3TR6JQ9epT2b4pCkIGHLtYe4FMpyOFXos6EuZqHIJg==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-utilities": "~0.0.11", + "@fluentui-copilot/react-utilities": "~0.0.12", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-motion": ">=9.10.4 <10.0.0", "@fluentui/react-tabster": ">=9.26.5 <10.0.0", + "@fluentui/react-utilities": ">=9.24.1 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.8 <20.0.0", "react": ">=16.14.0 <20.0.0", @@ -1233,14 +1440,16 @@ } }, "node_modules/@fluentui-copilot/react-copilot-theme": { - "version": "0.1.10", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-copilot-theme/-/react-copilot-theme-0.1.11.tgz", + "integrity": "sha512-OmduXWOxadZWgis9YzqZt+xKtalBZDI3JhcpDqEDeuqyr8Pf3W4GQkBJus/oiB8msrr9wuW55j0xuSYgekhO5Q==", "license": "MIT", "dependencies": { "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-shared-contexts": ">=9.25.1 <10.0.0", "@fluentui/react-theme": ">=9.2.0 <10.0.0", @@ -1252,17 +1461,19 @@ } }, "node_modules/@fluentui-copilot/react-editor-input": { - "version": "0.6.0", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-editor-input/-/react-editor-input-0.6.2.tgz", + "integrity": "sha512-SrwLwUDjdn7RDsrk9I96tmoIBbtxiYZO6KeBqkp1RLif6ZzY1Ycdfl0fMmKnJr8HBM/JKfUd7SHLmOcV5B1aTA==", "license": "MIT", "dependencies": { - "@fluentui-copilot/chat-input-plugins": "^0.5.3", - "@fluentui-copilot/react-chat-input-plugins": "^0.5.7", - "@fluentui-copilot/react-text-editor": "^0.4.3", + "@fluentui-copilot/chat-input-plugins": "^0.5.4", + "@fluentui-copilot/react-chat-input-plugins": "^0.5.9", + "@fluentui-copilot/react-text-editor": "^0.4.4", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-motion": ">=9.10.4 <10.0.0", "@fluentui/react-utilities": ">=9.24.1 <10.0.0", @@ -1273,15 +1484,17 @@ } }, "node_modules/@fluentui-copilot/react-entity-cards": { - "version": "0.4.7", + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-entity-cards/-/react-entity-cards-0.4.8.tgz", + "integrity": "sha512-4DJhKqPXxrHWTDWPLMtOyFppmxjin4fYjFouKpg8WggogM3VRHq5UB3sOqLm/qmnaHlkXWabMkl5T5Uc1EVkcA==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1293,15 +1506,17 @@ } }, "node_modules/@fluentui-copilot/react-feedback-buttons": { - "version": "0.12.6", + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-feedback-buttons/-/react-feedback-buttons-0.12.7.tgz", + "integrity": "sha512-DoG37O8SOHPv+T6mDUlWJ/sdxcdzJENhCa9IrKVmYl8ZrllAb6ojW+FP8zSJY+SOBjRG2M5HgmKcX2pyoEJEjQ==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1314,15 +1529,17 @@ } }, "node_modules/@fluentui-copilot/react-first-run-experience": { - "version": "0.9.6", + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-first-run-experience/-/react-first-run-experience-0.9.7.tgz", + "integrity": "sha512-MoRd6zXFbuZweN8zyGPOsS4ElOn+RDNHF4uMRb/HfTABebuHpvcBWZ6Mc4y3JUTPfyastz2dIwQ6Rk98h9osHA==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-utilities": ">=9.24.1 <10.0.0", @@ -1333,49 +1550,40 @@ } }, "node_modules/@fluentui-copilot/react-flair": { - "version": "0.6.0", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-flair/-/react-flair-0.6.2.tgz", + "integrity": "sha512-XNm4yyE2MpxPLM99A7VSiXbQxT+JvPn1DxTM7wlz7ZvCU8QIxrBx/6rRAvFX+kQbiPWc6kCkRhpG88BYEyKuYg==", "license": "MIT", "dependencies": { "@fluentui-contrib/houdini-utils": "^0.5.1", - "@fluentui-copilot/flair": "^0.5.0", + "@fluentui-copilot/flair": "^0.5.1", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.8 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui-copilot/react-flair/node_modules/@fluentui-contrib/houdini-utils": { - "version": "0.5.1", - "license": "MIT", - "dependencies": { - "@swc/helpers": "~0.5.11" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, "node_modules/@fluentui-copilot/react-grounding-menu": { - "version": "0.2.7", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-grounding-menu/-/react-grounding-menu-0.2.8.tgz", + "integrity": "sha512-ltnMCP7eruW4SqIVldvlFJ/OnJI57sOUZsj5xX4WR5ANM3xZqCKJnuCnsJkq8YkiyCuyf19/hxnKiLX2Oq1QMA==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-input-listbox": "^0.4.6", - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-utilities": "~0.0.11", + "@fluentui-copilot/react-input-listbox": "^0.4.7", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-utilities": "~0.0.12", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", "@fluentui/react-aria": ">=9.17.0 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-positioning": ">=9.20.5 <10.0.0", @@ -1388,18 +1596,20 @@ } }, "node_modules/@fluentui-copilot/react-input-listbox": { - "version": "0.4.6", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-input-listbox/-/react-input-listbox-0.4.7.tgz", + "integrity": "sha512-LoI6ki8qgBR2vsITHre0qamP/0FthbVWvcAbUcL+yYPEGkLwgN3k/kWEtlw8b0P4S0KHbmOrN8w69oYZSpt5ug==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-utilities": "~0.0.11", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-utilities": "~0.0.12", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", "@fluentui/react-aria": ">=9.17.0 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1414,16 +1624,19 @@ } }, "node_modules/@fluentui-copilot/react-latency": { - "version": "0.11.7", + "version": "0.11.9", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-latency/-/react-latency-0.11.9.tgz", + "integrity": "sha512-v8DHNyE3KwIPbVciftw0g7AlW7EMyEU2UBbXQR/j4HaIdC3Dv9/L2WQJvp7cDM8WHpn/CwgmsnPQRm6csdQbYA==", "license": "MIT", "dependencies": { "@fluentui-contrib/houdini-utils": "^0.5.1", - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/pulsing-dot": "^0.1.2", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1435,30 +1648,19 @@ "react-dom": ">=16.14.0 <20.0.0" } }, - "node_modules/@fluentui-copilot/react-latency/node_modules/@fluentui-contrib/houdini-utils": { - "version": "0.5.1", - "license": "MIT", - "dependencies": { - "@swc/helpers": "~0.5.11" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, "node_modules/@fluentui-copilot/react-output-card": { - "version": "0.13.0", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-output-card/-/react-output-card-0.13.2.tgz", + "integrity": "sha512-zJn7YyCupljafejD1viuuniAniegyVH9RD3IWmwPO74c2TF3BbEWddM9+7W5cGzMRtcLS+/zeM4wle3+su2YuA==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-flair": "^0.6.0", - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-flair": "^0.6.2", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-shared-contexts": ">=9.25.1 <10.0.0", @@ -1470,16 +1672,18 @@ } }, "node_modules/@fluentui-copilot/react-preview": { - "version": "0.8.6", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-preview/-/react-preview-0.8.7.tgz", + "integrity": "sha512-KGo8+KupuznLmB1wIQpnrn6P68Oxegr3bcMQobgdKqgOdPhruTmEGl1hgFJU0l8aYIKDPfHzVz8oxvxZWcdm5w==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-utilities": ">=9.24.1 <10.0.0", @@ -1490,23 +1694,25 @@ } }, "node_modules/@fluentui-copilot/react-prompt-input": { - "version": "0.11.0", - "license": "MIT", - "dependencies": { - "@fluentui-copilot/chat-input-plugins": "^0.5.3", - "@fluentui-copilot/react-chat-input-plugins": "^0.5.7", - "@fluentui-copilot/react-editor-input": "^0.6.0", - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-send-button": "^0.1.6", - "@fluentui-copilot/react-text-editor": "^0.4.3", - "@fluentui-copilot/react-utilities": "~0.0.11", - "@fluentui-copilot/text-editor": "^0.3.3", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-prompt-input/-/react-prompt-input-0.11.2.tgz", + "integrity": "sha512-I6QakmKFj1nbL9xdOS9HpQYRBxEcobw0T7DfndPC8ZY4jRibk8e29YO5DP4R4CDPnXLqsPDICa/aZlYFLUskoQ==", + "license": "MIT", + "dependencies": { + "@fluentui-copilot/chat-input-plugins": "^0.5.4", + "@fluentui-copilot/react-chat-input-plugins": "^0.5.9", + "@fluentui-copilot/react-editor-input": "^0.6.2", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-send-button": "^0.1.7", + "@fluentui-copilot/react-text-editor": "^0.4.4", + "@fluentui-copilot/react-utilities": "~0.0.12", + "@fluentui-copilot/text-editor": "^0.3.4", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-shared-contexts": ">=9.25.1 <10.0.0", @@ -1519,17 +1725,19 @@ } }, "node_modules/@fluentui-copilot/react-prompt-listbox": { - "version": "0.11.0", - "license": "MIT", - "dependencies": { - "@fluentui-copilot/chat-input-plugins": "^0.5.3", - "@fluentui-copilot/react-chat-input-plugins": "^0.5.7", - "@fluentui-copilot/react-editor-input": "^0.6.0", - "@fluentui-copilot/react-input-listbox": "^0.4.6", - "@fluentui-copilot/react-prompt-input": "^0.11.0", - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-text-editor": "^0.4.3", - "@fluentui-copilot/text-editor": "^0.3.3", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-prompt-listbox/-/react-prompt-listbox-0.11.2.tgz", + "integrity": "sha512-oIQ+GGmqW5kIfMNBwBynsQXeleYU95V7brY1tZ5N1JTG/gnzum4XvGeKOQ/36hFAcos7PD6Sscp3eI4vB466mA==", + "license": "MIT", + "dependencies": { + "@fluentui-copilot/chat-input-plugins": "^0.5.4", + "@fluentui-copilot/react-chat-input-plugins": "^0.5.9", + "@fluentui-copilot/react-editor-input": "^0.6.2", + "@fluentui-copilot/react-input-listbox": "^0.4.7", + "@fluentui-copilot/react-prompt-input": "^0.11.2", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-text-editor": "^0.4.4", + "@fluentui-copilot/text-editor": "^0.3.4", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, @@ -1537,7 +1745,7 @@ "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", "@fluentui/react-aria": ">=9.17.0 <10.0.0", "@fluentui/react-combobox": ">=9.16.6 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-motion": ">=9.10.4 <10.0.0", @@ -1552,15 +1760,17 @@ } }, "node_modules/@fluentui-copilot/react-prompt-starter": { - "version": "0.10.8", + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-prompt-starter/-/react-prompt-starter-0.10.9.tgz", + "integrity": "sha512-oKHM/29Hm3c0XftlC+iSnj5iVEMDWmWIpKIZs0JS9Lk11oDHCKy9T7N34dYZNE1Ja1GW2ppo3gyg/9Xq+HhX5A==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1573,15 +1783,17 @@ } }, "node_modules/@fluentui-copilot/react-provider": { - "version": "0.12.6", + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-provider/-/react-provider-0.12.7.tgz", + "integrity": "sha512-jxFx6ASNzazeyQulRFJ10ofJvitqbrlFM+R3sobei6W3ckNXa53O6fRhcpAiS3N/VS1dq0ZwJQCZa1vWSIqcQg==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-announce": "^0.5.11", + "@fluentui-copilot/react-announce": "^0.5.12", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-shared-contexts": ">=9.25.1 <10.0.0", @@ -1593,19 +1805,21 @@ } }, "node_modules/@fluentui-copilot/react-reference": { - "version": "0.16.6", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-reference/-/react-reference-0.16.7.tgz", + "integrity": "sha512-MDNBGe6puRx5FDDUSqMA9p9Uf8ukZjFeb/AbIYi3YHjoMfxb1WPk/xexvQAojkt1GgqAopPd+QyREECt8phW/g==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-preview": "^0.8.6", - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-sensitivity-label": "^0.8.6", - "@fluentui-copilot/react-utilities": "~0.0.11", + "@fluentui-copilot/react-preview": "^0.8.7", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-sensitivity-label": "^0.8.7", + "@fluentui-copilot/react-utilities": "~0.0.12", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1618,14 +1832,16 @@ } }, "node_modules/@fluentui-copilot/react-response-count": { - "version": "0.2.20", + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-response-count/-/react-response-count-0.2.21.tgz", + "integrity": "sha512-PnbWYjV06OsNiZDHexrbwy5DsFKlnQzBaAc/ZilYBG2DsrxDYKZ39z7fjqmCSHxWp2t0K/KN5buobsTd6H3DGA==", "license": "MIT", "dependencies": { "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.8 <20.0.0", @@ -1634,20 +1850,23 @@ } }, "node_modules/@fluentui-copilot/react-send-button": { - "version": "0.1.6", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-send-button/-/react-send-button-0.1.7.tgz", + "integrity": "sha512-G8nGs1Iw9bnTR9N62BY0Q7JCPeuR9UoL1NuR7kjeCaGzRLOzwH32NhdmdGmoOc2C5A5k2B6/Y9EVwP6A0Rcu3g==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", - "@fluentui-copilot/react-utilities": "~0.0.11", + "@fluentui-copilot/react-provider": "^0.12.7", + "@fluentui-copilot/react-utilities": "~0.0.12", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-motion": ">=9.10.4 <10.0.0", "@fluentui/react-shared-contexts": ">=9.25.1 <10.0.0", + "@fluentui/react-utilities": ">=9.24.1 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.8 <20.0.0", "react": ">=16.14.0 <20.0.0", @@ -1655,17 +1874,19 @@ } }, "node_modules/@fluentui-copilot/react-sensitivity-label": { - "version": "0.8.6", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-sensitivity-label/-/react-sensitivity-label-0.8.7.tgz", + "integrity": "sha512-7/MdKuZN6wYEt7/aNtfdLazsZIoh7/1gn6b02XDD44aK5QbavpdO/pk3ovmAMp0z2kUXEcG4w9elTj4lNecnoQ==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-preview": "^0.8.6", - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-preview": "^0.8.7", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1677,15 +1898,18 @@ } }, "node_modules/@fluentui-copilot/react-snippet": { - "version": "0.2.22", + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-snippet/-/react-snippet-0.2.23.tgz", + "integrity": "sha512-9w1Hq3qFclh2K6cxAtWLPwUVEvjEsuwPLg0N9bz359s3HpiAt6nj/RX2NXwbDQy2Qxj4zSjgG0dZ0eOJSAG1GA==", "license": "MIT", "dependencies": { "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", + "@fluentui/react-utilities": ">=9.24.1 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.8 <20.0.0", "react": ">=16.14.0 <20.0.0", @@ -1693,15 +1917,17 @@ } }, "node_modules/@fluentui-copilot/react-suggestions": { - "version": "0.13.6", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-suggestions/-/react-suggestions-0.13.7.tgz", + "integrity": "sha512-roNP/GKUcVN29E6ifwnZKlSCHJbvEP6Y6gb7HZi8/yi4xD2pLmkz7U6hF2IHdYsuJUFFuPZsyvo++2rHS7XH1Q==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1714,15 +1940,18 @@ } }, "node_modules/@fluentui-copilot/react-text": { - "version": "0.1.8", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-text/-/react-text-0.1.9.tgz", + "integrity": "sha512-A+cT5/O5YhWtkk9Vl9uBEgXDv5AEsO0KJ1f9qGDCgTMYtinTs7k8GykT8GXvbriEYJgKE8bOy+yoKgLGB0RJSA==", "license": "MIT", "dependencies": { "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", + "@fluentui/react-utilities": ">=9.24.1 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.8 <20.0.0", "react": ">=16.14.0 <20.0.0", @@ -1730,25 +1959,29 @@ } }, "node_modules/@fluentui-copilot/react-text-editor": { - "version": "0.4.3", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-text-editor/-/react-text-editor-0.4.4.tgz", + "integrity": "sha512-HPIimosZ2LcadAZ9nu1bL6Bz49ikYqKFJ93OscLxAQbVZ6v8p7o9OEx8TeAE3pk696jiUaF8fMJgLkbUOVoQhw==", "license": "MIT", "dependencies": { - "@fluentui-copilot/text-editor": "^0.3.3", - "@lexical/react": "^0.12.6", + "@fluentui-copilot/text-editor": "^0.3.4", + "@lexical/react": "^0.39.0", "@swc/helpers": "^0.5.1" } }, "node_modules/@fluentui-copilot/react-textarea": { - "version": "0.11.6", + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-textarea/-/react-textarea-0.11.7.tgz", + "integrity": "sha512-QH3hxLWesNh8Pm4llFZrOrg4zGEpe5th4fEZX6cFrpUjoGUzb0s3GFEpccuUGJxqbaTllalBhNkd9shfg1f7pw==", "license": "MIT", "dependencies": { - "@fluentui-copilot/react-provider": "^0.12.6", + "@fluentui-copilot/react-provider": "^0.12.7", "@fluentui-copilot/tokens": "^0.3.15", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@fluentui/keyboard-keys": ">=9.0.8 <10.0.0", - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-context-selector": ">=9.2.7 <10.0.0", "@fluentui/react-icons": ">=2.0.303 <3.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", @@ -1761,13 +1994,15 @@ } }, "node_modules/@fluentui-copilot/react-utilities": { - "version": "0.0.11", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/react-utilities/-/react-utilities-0.0.12.tgz", + "integrity": "sha512-a3uWleNheXF3BUbo+Qf6S4nVucKXAZOVzBmY79CmCiLZK2TZ8JJmRE6OOG613pZnSyBOdUZ7c0C6Be5vVgozBg==", "license": "MIT", "dependencies": { "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "@fluentui/react-components": ">=9.70.0 <10.0.0", + "@fluentui/react-components": ">=9.69.0 <10.0.0", "@fluentui/react-jsx-runtime": ">=9.2.0 <10.0.0", "@fluentui/react-utilities": ">=9.24.1 <10.0.0", "@types/react": ">=16.14.0 <20.0.0", @@ -1777,22 +2012,26 @@ } }, "node_modules/@fluentui-copilot/text-editor": { - "version": "0.3.3", - "license": "MIT", - "dependencies": { - "@lexical/headless": "^0.12.6", - "@lexical/list": "^0.12.6", - "@lexical/plain-text": "^0.12.6", - "@lexical/rich-text": "^0.12.6", - "@lexical/selection": "^0.12.6", - "@lexical/text": "^0.12.6", - "@lexical/utils": "^0.12.6", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/text-editor/-/text-editor-0.3.4.tgz", + "integrity": "sha512-QC3EsIJQNUtS/P6L3lOuAlvyV/bIXP4z65XTKLsM4SWXyRVJiHbe6/PSpMXfEmhrWgLVW2jTMDsxL7eezybDXA==", + "license": "MIT", + "dependencies": { + "@lexical/headless": "^0.39.0", + "@lexical/list": "^0.39.0", + "@lexical/plain-text": "^0.39.0", + "@lexical/rich-text": "^0.39.0", + "@lexical/selection": "^0.39.0", + "@lexical/text": "^0.39.0", + "@lexical/utils": "^0.39.0", "@swc/helpers": "^0.5.1", - "lexical": "^0.12.6" + "lexical": "^0.39.0" } }, "node_modules/@fluentui-copilot/tokens": { "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@fluentui-copilot/tokens/-/tokens-0.3.15.tgz", + "integrity": "sha512-5CUr8WBySiVRiMaADFlCSsroSCkxxJbHO5h9E0fu1vS61UoMD3h0Bt8PsScfWdrApPZrPq86iJVj6RTqBAcFbg==", "license": "MIT", "dependencies": { "@swc/helpers": "^0.5.1" @@ -1803,34 +2042,39 @@ }, "node_modules/@fluentui/keyboard-keys": { "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.8.tgz", + "integrity": "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw==", "license": "MIT", - "peer": true, "dependencies": { "@swc/helpers": "^0.5.1" } }, "node_modules/@fluentui/priority-overflow": { - "version": "9.2.1", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.3.0.tgz", + "integrity": "sha512-yaBC0R4e+4ZlCWDulB5S+xBrlnLwfzdg68GaarCqQO8OHjLg7Ah05xTj7PsAYcoHeEg/9vYeBwGXBpRO8+Tjqw==", "license": "MIT", "dependencies": { "@swc/helpers": "^0.5.1" } }, "node_modules/@fluentui/react-accordion": { - "version": "9.8.11", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.10.0.tgz", + "integrity": "sha512-EwjRfBdC3esMEP++PddyF7bVMSv9+t2W8AY5GkNcwDsqAW3D4zhlvxXBAb3qmpgXy4qMxRWGL8cEaiWgMpH1sg==", "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-motion": "^9.11.2", - "@fluentui/react-motion-components-preview": "^0.12.0", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1841,17 +2085,19 @@ } }, "node_modules/@fluentui/react-alert": { - "version": "9.0.0-beta.127", + "version": "9.0.0-beta.138", + "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.138.tgz", + "integrity": "sha512-mE3nMx1ngevvmFcp/2sePyJrdE8nme7eqCv1ppUT+mTIA1RYkR8hzBld1+DV1qJYc+F6DCeg4gImuQuu1OXiGA==", "license": "MIT", "dependencies": { - "@fluentui/react-avatar": "^9.9.10", - "@fluentui/react-button": "^9.6.10", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-icons": "^2.0.239", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1862,15 +2108,16 @@ } }, "node_modules/@fluentui/react-aria": { - "version": "9.17.4", + "version": "9.17.10", + "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.17.10.tgz", + "integrity": "sha512-KqS2XcdN84XsgVG4fAESyOBfixN7zbObWfQVLNZ2gZrp2b1hPGVYfQ6J4WOO0vXMKYp0rre/QMOgDm6/srL0XQ==", "license": "MIT", - "peer": true, "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-utilities": "^9.25.2", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-utilities": "^9.26.2", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1881,20 +2128,22 @@ } }, "node_modules/@fluentui/react-avatar": { - "version": "9.9.10", + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.11.0.tgz", + "integrity": "sha512-3MogJIiOGilKh9y/sWy0Cali1tpvWQNwcs2ryL7EVXi5xwTfKQM/WEgEnW2z+KtumDQUsRqlCHCSoi+x+BF8Qg==", "license": "MIT", "dependencies": { - "@fluentui/react-badge": "^9.4.9", - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-badge": "^9.5.1", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-popover": "^9.12.10", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-tooltip": "^9.8.9", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-popover": "^9.14.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-tooltip": "^9.10.0", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1905,15 +2154,17 @@ } }, "node_modules/@fluentui/react-badge": { - "version": "9.4.9", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.5.1.tgz", + "integrity": "sha512-OHS15ovGFPShrAA9U+hCyloJEyffC9gdif0a27AOIB9aVlF/hTzG7toxxulcg4ar4F9X3xXk/uccCCa2kzK0Gw==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1924,19 +2175,21 @@ } }, "node_modules/@fluentui/react-breadcrumb": { - "version": "9.3.10", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.4.0.tgz", + "integrity": "sha512-QpCjYlM3JTMnNwh/sDehDbuAVjTcgSfjkPdSmFaPk2lPHpER32CBcJVhheP9en2U5NbW1e+Gtvq8y06RN8FCWw==", "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-button": "^9.6.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-link": "^9.6.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-link": "^9.8.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1947,18 +2200,20 @@ } }, "node_modules/@fluentui/react-button": { - "version": "9.6.10", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.9.0.tgz", + "integrity": "sha512-aH3aSjKyxIiNb9jJOUaaIq47w7jP5ESFSRzvMjcWOETvlWo4QgNqEOOsYqpcltM1OrQZ0sTy/isxppRcyMDlcQ==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", + "@fluentui/react-aria": "^9.17.10", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1969,17 +2224,19 @@ } }, "node_modules/@fluentui/react-card": { - "version": "9.5.4", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.6.0.tgz", + "integrity": "sha512-vgBvhtSzQDa01aOP9zdhJXFLsZAiDVslRfX3HmlIo1pAMt8w+PBq+ypDp1wxM7HPFpj9+RYcERRKtf4MSNP9Nw==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-text": "^9.6.9", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-text": "^9.6.15", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -1990,20 +2247,22 @@ } }, "node_modules/@fluentui/react-carousel": { - "version": "9.8.10", + "version": "9.9.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.9.6.tgz", + "integrity": "sha512-Ae7DKwQsidRBjUQeiXffRUi8i/26jMgJd24rDVLeQUvoUhs+z/SA9iZN/QMuNl02E291MAEruENKzzkshvfYfg==", "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-button": "^9.6.10", - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-tooltip": "^9.8.9", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-tooltip": "^9.10.0", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", "embla-carousel": "^8.5.1", "embla-carousel-autoplay": "^8.5.1", @@ -2017,18 +2276,20 @@ } }, "node_modules/@fluentui/react-checkbox": { - "version": "9.5.9", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.6.0.tgz", + "integrity": "sha512-GMgB1Yx2WP6cISIZoRTyXp2VkJBR8t1+wRyY63RRcofL/ziqqBhz++kl317lbVv7QxnXZh6KlVuoPROWFDQuaw==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.9", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-label": "^9.3.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2039,17 +2300,19 @@ } }, "node_modules/@fluentui/react-color-picker": { - "version": "9.2.9", + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-color-picker/-/react-color-picker-9.2.15.tgz", + "integrity": "sha512-RMmawl7g4gUYLuTQG2QwCcR9fGC+vDD+snsBlXtObpj/cKpeDmYif46g88pYv86jeIXY1zsjINmLpELmz+uFmw==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^3.3.4", - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2060,23 +2323,24 @@ } }, "node_modules/@fluentui/react-combobox": { - "version": "9.16.10", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.17.0.tgz", + "integrity": "sha512-04JTIrXCAbG8HnczFVzJsUJO+NJQ2d/JPynXlmTq7KCMw0BssiF//7IAPFnTiMYmS7jcwc9Uh4ZeFrw+czA79g==", "license": "MIT", - "peer": true, "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-field": "^9.4.9", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-positioning": "^9.20.8", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2087,71 +2351,72 @@ } }, "node_modules/@fluentui/react-components": { - "version": "9.72.4", - "license": "MIT", - "peer": true, - "dependencies": { - "@fluentui/react-accordion": "^9.8.11", - "@fluentui/react-alert": "9.0.0-beta.127", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-avatar": "^9.9.10", - "@fluentui/react-badge": "^9.4.9", - "@fluentui/react-breadcrumb": "^9.3.10", - "@fluentui/react-button": "^9.6.10", - "@fluentui/react-card": "^9.5.4", - "@fluentui/react-carousel": "^9.8.10", - "@fluentui/react-checkbox": "^9.5.9", - "@fluentui/react-color-picker": "^9.2.9", - "@fluentui/react-combobox": "^9.16.10", - "@fluentui/react-dialog": "^9.16.0", - "@fluentui/react-divider": "^9.4.9", - "@fluentui/react-drawer": "^9.10.6", - "@fluentui/react-field": "^9.4.9", - "@fluentui/react-image": "^9.3.9", - "@fluentui/react-infobutton": "9.0.0-beta.105", - "@fluentui/react-infolabel": "^9.4.10", - "@fluentui/react-input": "^9.7.9", - "@fluentui/react-label": "^9.3.9", - "@fluentui/react-link": "^9.6.9", - "@fluentui/react-list": "^9.6.4", - "@fluentui/react-menu": "^9.20.3", - "@fluentui/react-message-bar": "^9.6.11", - "@fluentui/react-motion": "^9.11.2", - "@fluentui/react-nav": "^9.3.11", - "@fluentui/react-overflow": "^9.6.3", - "@fluentui/react-persona": "^9.5.10", - "@fluentui/react-popover": "^9.12.10", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-positioning": "^9.20.8", - "@fluentui/react-progress": "^9.4.9", - "@fluentui/react-provider": "^9.22.9", - "@fluentui/react-radio": "^9.5.9", - "@fluentui/react-rating": "^9.3.9", - "@fluentui/react-search": "^9.3.9", - "@fluentui/react-select": "^9.4.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-skeleton": "^9.4.9", - "@fluentui/react-slider": "^9.5.9", - "@fluentui/react-spinbutton": "^9.5.9", - "@fluentui/react-spinner": "^9.7.9", - "@fluentui/react-swatch-picker": "^9.4.9", - "@fluentui/react-switch": "^9.4.9", - "@fluentui/react-table": "^9.19.3", - "@fluentui/react-tabs": "^9.10.5", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-tag-picker": "^9.7.10", - "@fluentui/react-tags": "^9.7.10", - "@fluentui/react-teaching-popover": "^9.6.10", - "@fluentui/react-text": "^9.6.9", - "@fluentui/react-textarea": "^9.6.9", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-toast": "^9.7.6", - "@fluentui/react-toolbar": "^9.6.10", - "@fluentui/react-tooltip": "^9.8.9", - "@fluentui/react-tree": "^9.15.3", - "@fluentui/react-utilities": "^9.25.2", - "@fluentui/react-virtualizer": "9.0.0-alpha.105", - "@griffel/react": "^1.5.22", + "version": "9.73.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.73.7.tgz", + "integrity": "sha512-hLxXEAiiMEMmFR3jEYgFPOV5lnNzu6SJU0NtyMCn1Tf4HXgCfy4h700e+GzuAsL1RlQAYC35HplcZHcEffwTIQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-accordion": "^9.10.0", + "@fluentui/react-alert": "9.0.0-beta.138", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-badge": "^9.5.1", + "@fluentui/react-breadcrumb": "^9.4.0", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-card": "^9.6.0", + "@fluentui/react-carousel": "^9.9.6", + "@fluentui/react-checkbox": "^9.6.0", + "@fluentui/react-color-picker": "^9.2.15", + "@fluentui/react-combobox": "^9.17.0", + "@fluentui/react-dialog": "^9.17.3", + "@fluentui/react-divider": "^9.7.0", + "@fluentui/react-drawer": "^9.11.6", + "@fluentui/react-field": "^9.5.0", + "@fluentui/react-image": "^9.4.0", + "@fluentui/react-infobutton": "9.0.0-beta.114", + "@fluentui/react-infolabel": "^9.4.19", + "@fluentui/react-input": "^9.8.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-link": "^9.8.0", + "@fluentui/react-list": "^9.6.13", + "@fluentui/react-menu": "^9.24.0", + "@fluentui/react-message-bar": "^9.6.23", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-nav": "^9.3.23", + "@fluentui/react-overflow": "^9.7.1", + "@fluentui/react-persona": "^9.7.2", + "@fluentui/react-popover": "^9.14.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-progress": "^9.5.0", + "@fluentui/react-provider": "^9.22.15", + "@fluentui/react-radio": "^9.6.1", + "@fluentui/react-rating": "^9.4.0", + "@fluentui/react-search": "^9.4.1", + "@fluentui/react-select": "^9.5.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-skeleton": "^9.7.1", + "@fluentui/react-slider": "^9.6.1", + "@fluentui/react-spinbutton": "^9.6.1", + "@fluentui/react-spinner": "^9.8.1", + "@fluentui/react-swatch-picker": "^9.5.1", + "@fluentui/react-switch": "^9.7.1", + "@fluentui/react-table": "^9.19.14", + "@fluentui/react-tabs": "^9.12.0", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-tag-picker": "^9.8.5", + "@fluentui/react-tags": "^9.8.0", + "@fluentui/react-teaching-popover": "^9.6.20", + "@fluentui/react-text": "^9.6.15", + "@fluentui/react-textarea": "^9.7.1", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-toast": "^9.7.16", + "@fluentui/react-toolbar": "^9.7.7", + "@fluentui/react-tooltip": "^9.10.0", + "@fluentui/react-tree": "^9.15.16", + "@fluentui/react-utilities": "^9.26.2", + "@fluentui/react-virtualizer": "9.0.0-alpha.111", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2162,11 +2427,12 @@ } }, "node_modules/@fluentui/react-context-selector": { - "version": "9.2.10", + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.2.15.tgz", + "integrity": "sha512-QymBntFLJNZ9VfTOaBn2ApUSSSC5UuDW8ZcgPJPA+06XEFH+U9Zny2d9QAg1xYNYwIGWahWGQ+7ATOuLxtB8Jw==", "license": "MIT", - "peer": true, "dependencies": { - "@fluentui/react-utilities": "^9.25.2", + "@fluentui/react-utilities": "^9.26.2", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2178,22 +2444,24 @@ } }, "node_modules/@fluentui/react-dialog": { - "version": "9.16.0", + "version": "9.17.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.17.3.tgz", + "integrity": "sha512-rF5l8n5yhaB//ZHns0my3Tviir7R8NVyRgTtvV2gLhG58YM7qpm54oraG83uwlXCcZp0wlg2LuIe1cZ559ex1A==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-motion": "^9.11.2", - "@fluentui/react-motion-components-preview": "^0.12.0", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2204,14 +2472,16 @@ } }, "node_modules/@fluentui/react-divider": { - "version": "9.4.9", + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.7.0.tgz", + "integrity": "sha512-U8Nhrghjeh+XCGM4B7aHYosd6fXaxHC3MpZi7DB0xQ20ljn5cSTpBt4Yvl+tB9ld2+/eM8wekx1GVKyI4yWa3g==", "license": "MIT", "dependencies": { - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2222,18 +2492,21 @@ } }, "node_modules/@fluentui/react-drawer": { - "version": "9.10.6", - "license": "MIT", - "dependencies": { - "@fluentui/react-dialog": "^9.16.0", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-motion": "^9.11.2", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "version": "9.11.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.11.6.tgz", + "integrity": "sha512-E+k3eKVb/xKPm2RH5Q1xBjL89NeB1GXtYHO6qRlhQ9auYVTlaBCR7f/ZfIIJJ2x8MzfntQljyl94VARtmZYnyA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-dialog": "^9.17.3", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2244,17 +2517,19 @@ } }, "node_modules/@fluentui/react-field": { - "version": "9.4.9", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.5.0.tgz", + "integrity": "sha512-yGjB9RXqKrolkkjyAsKVdrH2Xeinj+vromrSCJelgMJ3Q3D6YkExHQzgtdzqo0fVPppnEA4oDKL3Vqqnz/G5Ug==", "license": "MIT", "dependencies": { - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-label": "^9.3.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2265,11 +2540,12 @@ } }, "node_modules/@fluentui/react-icons": { - "version": "2.0.313", + "version": "2.0.324", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.324.tgz", + "integrity": "sha512-wbtIQWwoTWNU6KyuF59zZ1viFv1i68iwVa1+so/QnfNKNHIXa2MEZ375Vg/pcubFBqlTxsKMrCBFtHEIzBHG/Q==", "license": "MIT", - "peer": true, "dependencies": { - "@griffel/react": "^1.0.0", + "@griffel/react": "^1.6.1", "tslib": "^2.1.0" }, "peerDependencies": { @@ -2277,14 +2553,16 @@ } }, "node_modules/@fluentui/react-image": { - "version": "9.3.9", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.4.0.tgz", + "integrity": "sha512-BpcBlmkukm7YYf6PTCbAIMkeCXc8+7aq2eMADsxF5gFD8j3d5lBY3cKByOWRM1NvXcMXmqXr/hQP+ovqNAHzEA==", "license": "MIT", "dependencies": { - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2295,17 +2573,19 @@ } }, "node_modules/@fluentui/react-infobutton": { - "version": "9.0.0-beta.105", + "version": "9.0.0-beta.114", + "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.114.tgz", + "integrity": "sha512-3mqnlIcRc0PuW7rsxLFjzqnI/IITZIrHRt8Zwcm8NX7XZIK3wfODb9ytmQDYU/5IfwiSXC+xozqhI6kttaE3iw==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.237", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-label": "^9.3.9", - "@fluentui/react-popover": "^9.12.10", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-popover": "^9.14.1", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2316,18 +2596,20 @@ } }, "node_modules/@fluentui/react-infolabel": { - "version": "9.4.10", + "version": "9.4.19", + "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.19.tgz", + "integrity": "sha512-b/3ETF5DPgHcRUcj85iGyiEXUFozFq+IY6tPcyCiUcmIoKScD8McFaHozjpaVqngLbCz0uKNNA0JDy1x/T2ItQ==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-label": "^9.3.9", - "@fluentui/react-popover": "^9.12.10", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-popover": "^9.14.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2338,15 +2620,17 @@ } }, "node_modules/@fluentui/react-input": { - "version": "9.7.9", + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.8.1.tgz", + "integrity": "sha512-ZlMeYBf1EQg4alI5+9gfx3Icmq3xibPiIYeARtFzOKJ2XzpnD4d/yswx3IDkzXCbqw9rSHtHV03vEeYLUPPTGw==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-field": "^9.5.0", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2357,13 +2641,13 @@ } }, "node_modules/@fluentui/react-jsx-runtime": { - "version": "9.3.1", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.4.1.tgz", + "integrity": "sha512-ZodSm7jRa4kaLKDi+emfHFMP/IDnYwFQQAI2BdtKbVrvfwvzPRprGcnTgivnqKBT1ROvKOCY2ddz7+yZzesnNw==", "license": "MIT", - "peer": true, "dependencies": { - "@fluentui/react-utilities": "^9.25.2", - "@swc/helpers": "^0.5.1", - "react-is": "^17.0.2" + "@fluentui/react-utilities": "^9.26.2", + "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", @@ -2371,14 +2655,16 @@ } }, "node_modules/@fluentui/react-label": { - "version": "9.3.9", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.4.0.tgz", + "integrity": "sha512-joQ7YNz2dgwDd134sc7e8/vxfFKBUT5AdWx0apT0ohWKgh7RBjB3AdXsaJ8FaMKMNZIGTxZVsP4hHcGsWMTAFw==", "license": "MIT", "dependencies": { - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2389,16 +2675,18 @@ } }, "node_modules/@fluentui/react-link": { - "version": "9.6.9", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.8.0.tgz", + "integrity": "sha512-TH5LS4iuQ4jYzlR84A4n7lQTKaJuiuuGFHMIxoEqtKeMoL9F5AiabuBs6m7Q7clSdTrrcRMNzXLuEFarQrzGTQ==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2409,18 +2697,20 @@ } }, "node_modules/@fluentui/react-list": { - "version": "9.6.4", + "version": "9.6.13", + "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.13.tgz", + "integrity": "sha512-MIP0XKxU68m8VsBCyNBame46nnZ94FCNUArw9T2JuumyKMgV07C+sNhXCe9BCVpUr8e2Hfofo7CZjAsXWDZ0nw==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-checkbox": "^9.5.9", - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-checkbox": "^9.6.0", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2431,21 +2721,25 @@ } }, "node_modules/@fluentui/react-menu": { - "version": "9.20.3", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.24.0.tgz", + "integrity": "sha512-HqIwEM6lPropSHUnbPFufLYdkAIVca87XbNQHCTes4QSLeaF4oEjlBH60rIqQ52k78FwZuUFIciWkSChxJ9ekg==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-positioning": "^9.20.8", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2456,19 +2750,21 @@ } }, "node_modules/@fluentui/react-message-bar": { - "version": "9.6.11", + "version": "9.6.23", + "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.6.23.tgz", + "integrity": "sha512-mGnFmYWx6tq36OMTdVtJmxyn3j0p+Shll3+w4W2fW8fcOVSeyrnZ++HLmpurUkVzwI2xR2lL842kxC3GtbwmNw==", "license": "MIT", "dependencies": { - "@fluentui/react-button": "^9.6.10", + "@fluentui/react-button": "^9.9.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-link": "^9.6.9", - "@fluentui/react-motion": "^9.11.2", - "@fluentui/react-motion-components-preview": "^0.12.0", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-link": "^9.8.0", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2479,12 +2775,13 @@ } }, "node_modules/@fluentui/react-motion": { - "version": "9.11.2", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.14.0.tgz", + "integrity": "sha512-gOy8+fUP1KQRM/J6mRhioCMmUrHW9jbLF0DZ9T8nKPQsLrLaSXHxnnI8DcKZjlYc2fKuZitBnbpximgff6HajQ==", "license": "MIT", - "peer": true, "dependencies": { - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-utilities": "^9.25.2", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-utilities": "^9.26.2", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2495,10 +2792,13 @@ } }, "node_modules/@fluentui/react-motion-components-preview": { - "version": "0.12.0", + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.15.3.tgz", + "integrity": "sha512-dUH2+GmEWX9q2ojx70VfFLRqzA9fR4YISC6daXkz3iPx4PtesTDn7jwsuXXquaAhltJeBptJ8+K4jbtBrwCMYQ==", "license": "MIT", "dependencies": { "@fluentui/react-motion": "*", + "@fluentui/react-utilities": "*", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2509,23 +2809,26 @@ } }, "node_modules/@fluentui/react-nav": { - "version": "9.3.11", + "version": "9.3.23", + "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.23.tgz", + "integrity": "sha512-Z9hA70n5i62sO9IJItkX5+v1F7Lo/539joPaHCLHHca+rySQQZKqy8zLRIfLbh/qF8Nm04ywY19Qt14XjI59cQ==", "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-button": "^9.6.10", - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-divider": "^9.4.9", - "@fluentui/react-drawer": "^9.10.6", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-divider": "^9.7.0", + "@fluentui/react-drawer": "^9.11.6", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-motion": "^9.11.2", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-tooltip": "^9.8.9", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-tooltip": "^9.10.0", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2536,14 +2839,16 @@ } }, "node_modules/@fluentui/react-overflow": { - "version": "9.6.3", + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.7.1.tgz", + "integrity": "sha512-Ml1GlcLrAUv31d9WN15WGOZv32gzDtZD5Mp1MOQ3ichDfTtxrswIch7MDzZ8hLMGf/7Y2IzBpV8iFR1XdSrGBA==", "license": "MIT", "dependencies": { - "@fluentui/priority-overflow": "^9.2.1", - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/priority-overflow": "^9.3.0", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2554,16 +2859,18 @@ } }, "node_modules/@fluentui/react-persona": { - "version": "9.5.10", - "license": "MIT", - "dependencies": { - "@fluentui/react-avatar": "^9.9.10", - "@fluentui/react-badge": "^9.4.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "version": "9.7.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.7.2.tgz", + "integrity": "sha512-u6buhC6Haf8YewBnZAzi49YCwiC8vt0O0YPADemk+4uJ8bhCnayzLxMYGuQ95XO4HFhvVnSPEYjMDdKrMO1hIw==", + "license": "MIT", + "dependencies": { + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-badge": "^9.5.1", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2574,20 +2881,24 @@ } }, "node_modules/@fluentui/react-popover": { - "version": "9.12.10", + "version": "9.14.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.14.1.tgz", + "integrity": "sha512-EODa5yWSfDLPDurjWoZXfkf2ccnbQQbk3s1XYRzxA6RDfdVqUI5W64RJzHWBiNhOLzQEhd6Qb4e6Mshj4FSbdQ==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-positioning": "^9.20.8", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2598,13 +2909,15 @@ } }, "node_modules/@fluentui/react-portal": { - "version": "9.8.6", + "version": "9.8.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.8.11.tgz", + "integrity": "sha512-2eg4MdW7e2UGRYWPg05GCytAjWYNd55YOP9+iUDINoQwwto9oeFTtZRyn08HYw37cSNqoH24qGz/VBctzTkqDA==", "license": "MIT", "dependencies": { - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2615,16 +2928,17 @@ } }, "node_modules/@fluentui/react-positioning": { - "version": "9.20.8", + "version": "9.22.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.22.0.tgz", + "integrity": "sha512-i3DLC4jd4MoYSZMYLKQNUTpkjKAJ0snIcihvkrjt2jpvv34CifKJhqVtjFQ470pRW4XNx/pBBX07vdXpA3poxA==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/devtools": "^0.2.3", "@floating-ui/dom": "^1.6.12", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", "use-sync-external-store": "^1.2.0" }, @@ -2636,15 +2950,18 @@ } }, "node_modules/@fluentui/react-progress": { - "version": "9.4.9", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.4.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.5.0.tgz", + "integrity": "sha512-VcWXI6UJfBkrDuC/e9oR4YBlpnLUE+FqRRjMG4mVXV+AJzFiljF3mQkFAj94G6dsr54TcoDXC6oydgXLCOTW2A==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.5.0", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2655,17 +2972,19 @@ } }, "node_modules/@fluentui/react-provider": { - "version": "9.22.9", + "version": "9.22.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.22.15.tgz", + "integrity": "sha512-a+ImgL9DOlylDM4UYPnxQTA3yXxbVj+O0iNEyTZ6fMzdMsHzpALU4GAq6tOyW4L7RaQtRBmNpVfwTCEKpqaTJQ==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", "@griffel/core": "^1.16.0", - "@griffel/react": "^1.5.22", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2676,17 +2995,19 @@ } }, "node_modules/@fluentui/react-radio": { - "version": "9.5.9", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.4.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-label": "^9.3.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.6.1.tgz", + "integrity": "sha512-QBoV6l8fVLP+H9Tigq/Y6boiEqMDRhhVMkIfUiWFbnsU/Uc7J5fxW8GoNqzMmoOmC7yvQ/g4jsoTQF27+PzK5w==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.5.0", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2697,16 +3018,18 @@ } }, "node_modules/@fluentui/react-rating": { - "version": "9.3.9", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.4.0.tgz", + "integrity": "sha512-qVesFNgQ7uuX8z9d8xqxIXn5ax06xffgBr/eAuZfqVYZG5aRrPHHRoiWf0HDrYD4Lb/HRBLPtbNihNxhXj/LEA==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2717,16 +3040,18 @@ } }, "node_modules/@fluentui/react-search": { - "version": "9.3.9", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.4.1.tgz", + "integrity": "sha512-Lv2zhPad7SDhMd5NeabXluw4y0Gov9YxDkJhjShMnkiN3yCOA5tlVviNvRXOXxy0gS//d8CiGJ5mBT1bzz2Rrw==", "license": "MIT", "dependencies": { "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-input": "^9.7.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-input": "^9.8.1", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2737,16 +3062,18 @@ } }, "node_modules/@fluentui/react-select": { - "version": "9.4.9", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.5.0.tgz", + "integrity": "sha512-pGOD6MBwQsiHKkEdNmVrTavcfC9pOjt4nz/DRlFD444j6iR1PALlus5cNOp7A0JOnGDDvW+1afIvgySCqN0oiA==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.9", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2757,11 +3084,12 @@ } }, "node_modules/@fluentui/react-shared-contexts": { - "version": "9.25.2", + "version": "9.26.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.26.2.tgz", + "integrity": "sha512-upKXkwlIp5oIhELr4clAZXQkuCd4GDXM6GZEz8BOmRO+PnxyqmycCXvxDxsmi6XN+0vkGM4joiIgkB14o/FctQ==", "license": "MIT", - "peer": true, "dependencies": { - "@fluentui/react-theme": "^9.2.0", + "@fluentui/react-theme": "^9.2.1", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2770,15 +3098,17 @@ } }, "node_modules/@fluentui/react-skeleton": { - "version": "9.4.9", + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.7.1.tgz", + "integrity": "sha512-9WniFEe6gbhkZuBurpQNFmMMhP/Ox84Xm9/iu6q8OmnRkFCyZrEuCFlWGDffnBREKIJqE0VJn5ZrUYWMMh45KA==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-field": "^9.5.0", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2789,16 +3119,18 @@ } }, "node_modules/@fluentui/react-slider": { - "version": "9.5.9", - "license": "MIT", - "dependencies": { - "@fluentui/react-field": "^9.4.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.6.1.tgz", + "integrity": "sha512-ytF1gOEho8DrI817H8WCBsck1RXOlW7JRXYtu9VwH3SnDRM2Jz1CNxbou80+BpvyR1KKkvCc/JSgREgUAnkRAQ==", + "license": "MIT", + "dependencies": { + "@fluentui/react-field": "^9.5.0", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2809,17 +3141,19 @@ } }, "node_modules/@fluentui/react-spinbutton": { - "version": "9.5.9", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.6.1.tgz", + "integrity": "sha512-szqGlEfeJYkBzszEWBjj7ux522ckw9YtKAH0CS0Npd0xcY1GFkdywPwJMOoRUhsO08BOhv6P70Wlx0eYqURgIA==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-field": "^9.4.9", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2830,15 +3164,17 @@ } }, "node_modules/@fluentui/react-spinner": { - "version": "9.7.9", + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.8.1.tgz", + "integrity": "sha512-vSM5FwjASEor8NBOJx/1MLp8VCw7+pOJqZSvMn29LrUmMbgSZ6CifZFx0GfiX+1fM0EZ2/pqJzFFHpoQQubAyw==", "license": "MIT", "dependencies": { - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-label": "^9.3.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2849,18 +3185,20 @@ } }, "node_modules/@fluentui/react-swatch-picker": { - "version": "9.4.9", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.5.1.tgz", + "integrity": "sha512-7rs4dgnFMV2m/2A1tkevrVfThVEJs9crnVWCiSE4XADb9hFp7mqVyN8dKbQCJJMXODLF/Bc90nTCtLV8WaEj4Q==", "license": "MIT", "dependencies": { - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-field": "^9.4.9", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2871,18 +3209,20 @@ } }, "node_modules/@fluentui/react-switch": { - "version": "9.4.9", + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.7.1.tgz", + "integrity": "sha512-61zJhxG9UBcZ+5T/Dk9yzOJDCOc2ZMZef/ImgIMB4lVsyWs/3n/ec/PKPwjp9SNz2FhQvayhMytEbGzri00jGw==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.9", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-label": "^9.3.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-label": "^9.4.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2893,22 +3233,24 @@ } }, "node_modules/@fluentui/react-table": { - "version": "9.19.3", + "version": "9.19.14", + "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.14.tgz", + "integrity": "sha512-IZ3tDqlQDC+R6nzX4thU8A7Aw3BMhbBZ5tgMOHnW733Xfton7wqKiumjsGJBnef3I48mqnBHJZQEzWBgzLsdqg==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-avatar": "^9.9.10", - "@fluentui/react-checkbox": "^9.5.9", - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-checkbox": "^9.6.0", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-radio": "^9.5.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-radio": "^9.6.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2919,16 +3261,18 @@ } }, "node_modules/@fluentui/react-tabs": { - "version": "9.10.5", - "license": "MIT", - "dependencies": { - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.12.0.tgz", + "integrity": "sha512-gKCi1XNDYRvF6R5wETeQptzQRVBlM7VETaQHS/ue1x7+Vo42MbWMtYOmvqeg5CPjqy2hAwch0IA9bzWEQAm2ZA==", + "license": "MIT", + "dependencies": { + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2939,14 +3283,15 @@ } }, "node_modules/@fluentui/react-tabster": { - "version": "9.26.8", + "version": "9.26.13", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.26.13.tgz", + "integrity": "sha512-uOuJj7jn1ME52Vc685/Ielf6srK/sfFQA5zBIbXIvy2Eisfp7R1RmJe2sXWoszz/Fu/XDkPwdM/GLv23N3vrvQ==", "license": "MIT", - "peer": true, "dependencies": { - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", "keyborg": "^2.6.0", "tabster": "^8.5.5" @@ -2959,24 +3304,26 @@ } }, "node_modules/@fluentui/react-tag-picker": { - "version": "9.7.10", + "version": "9.8.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.8.5.tgz", + "integrity": "sha512-uhZUWDdg7zmQNjb1/5YI3l6agSDg/yFFaYZDH4eQDOmKIm35jAT2GmEMZVomZZVW/dDhZpezfMWZA5r442cZYQ==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-combobox": "^9.16.10", - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-field": "^9.4.9", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-combobox": "^9.17.0", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-field": "^9.5.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-positioning": "^9.20.8", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-tags": "^9.7.10", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-tags": "^9.8.0", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -2987,19 +3334,21 @@ } }, "node_modules/@fluentui/react-tags": { - "version": "9.7.10", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.8.0.tgz", + "integrity": "sha512-O/Kf8pFgS0/eguzDCPm8FmrPG64dU36xTI1uYKwgF6iVOpmWFjk+7aPQtkoFHQzVwl1iLUL4mQFSutR4A8s38Q==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-avatar": "^9.9.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-avatar": "^9.11.0", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3010,20 +3359,22 @@ } }, "node_modules/@fluentui/react-teaching-popover": { - "version": "9.6.10", + "version": "9.6.20", + "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.20.tgz", + "integrity": "sha512-XB/SJXdJabulcDBp6z4NNSFOcAnaOoIUZdmzqpx09UxtQwU/eFnYvZw/k1SI8Nc7IpHBgjzId8gHy6jvaN8JHw==", "license": "MIT", "dependencies": { - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-button": "^9.6.10", - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-popover": "^9.12.10", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-popover": "^9.14.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", "use-sync-external-store": "^1.2.0" }, @@ -3035,14 +3386,16 @@ } }, "node_modules/@fluentui/react-text": { - "version": "9.6.9", + "version": "9.6.15", + "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.6.15.tgz", + "integrity": "sha512-YB1azhq8MGfnYTGlEAX1mzcFZ6CvqkkaxaCogU4TM9BtPgQ1YUAxE01RMenl8VVi8W9hNbJKkuc8R8GzYwzT4Q==", "license": "MIT", "dependencies": { - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3053,15 +3406,17 @@ } }, "node_modules/@fluentui/react-textarea": { - "version": "9.6.9", + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.7.1.tgz", + "integrity": "sha512-YG0j202PRLDLZZDn8QQgREd4Ery2fDYMYb2HUvFdfo6MuSXMvv0RCKEUBCgajIXsHwT31Hsg5+xzM40X4jlOBg==", "license": "MIT", "dependencies": { - "@fluentui/react-field": "^9.4.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-field": "^9.5.0", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3072,30 +3427,33 @@ } }, "node_modules/@fluentui/react-theme": { - "version": "9.2.0", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.2.1.tgz", + "integrity": "sha512-lJxfz7LmmglFz+c9C41qmMqaRRZZUPtPPl9DWQ79vH+JwZd4dkN7eA78OTRwcGCOTPEKoLTX72R+EFaWEDlX+w==", "license": "MIT", - "peer": true, "dependencies": { - "@fluentui/tokens": "1.0.0-alpha.22", + "@fluentui/tokens": "1.0.0-alpha.23", "@swc/helpers": "^0.5.1" } }, "node_modules/@fluentui/react-toast": { - "version": "9.7.6", + "version": "9.7.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.16.tgz", + "integrity": "sha512-Yq4yJboYqtdL5pNJBIYlSdT/kR6m449O95taJCh/msXJyRgqQZ46EmpTcwsxu3D55LTHbqI6Vxu+AikDYH1W7w==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", + "@fluentui/react-aria": "^9.17.10", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-motion": "^9.11.2", - "@fluentui/react-motion-components-preview": "^0.12.0", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3106,19 +3464,21 @@ } }, "node_modules/@fluentui/react-toolbar": { - "version": "9.6.10", - "license": "MIT", - "dependencies": { - "@fluentui/react-button": "^9.6.10", - "@fluentui/react-context-selector": "^9.2.10", - "@fluentui/react-divider": "^9.4.9", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-radio": "^9.5.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "version": "9.7.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.7.7.tgz", + "integrity": "sha512-49nrRvGqJfdXhwaKZfNIcTiZSqTbThNG8uCa0FvJ88cO11PRPGcr5s6u3plUVxDXUKXpZJ7PKr/TTA0MvP7yIg==", + "license": "MIT", + "dependencies": { + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-context-selector": "^9.2.15", + "@fluentui/react-divider": "^9.7.0", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-radio": "^9.6.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3129,18 +3489,20 @@ } }, "node_modules/@fluentui/react-tooltip": { - "version": "9.8.9", + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.10.0.tgz", + "integrity": "sha512-+aM0S1mcXy8XKKWgU3TocqTxHjcai7fHns3KwONLJPTp3jXTjyqEoj/o4XX1ka2IM3gdOFfyUU0Gfvw708dn9w==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-portal": "^9.8.6", - "@fluentui/react-positioning": "^9.20.8", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3151,25 +3513,27 @@ } }, "node_modules/@fluentui/react-tree": { - "version": "9.15.3", + "version": "9.15.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.15.16.tgz", + "integrity": "sha512-WP4WjbF/UWCp0JKaZsMFtah/kXu+mxqN8/kghppRYfVHWzLiMgFAPB/OzrGejLNwx+ai3t2dHOIHxXHnR1jYHA==", "license": "MIT", "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-aria": "^9.17.4", - "@fluentui/react-avatar": "^9.9.10", - "@fluentui/react-button": "^9.6.10", - "@fluentui/react-checkbox": "^9.5.9", - "@fluentui/react-context-selector": "^9.2.10", + "@fluentui/react-aria": "^9.17.10", + "@fluentui/react-avatar": "^9.11.0", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-checkbox": "^9.6.0", + "@fluentui/react-context-selector": "^9.2.15", "@fluentui/react-icons": "^2.0.245", - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-motion": "^9.11.2", - "@fluentui/react-motion-components-preview": "^0.12.0", - "@fluentui/react-radio": "^9.5.9", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-tabster": "^9.26.8", - "@fluentui/react-theme": "^9.2.0", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-motion": "^9.14.0", + "@fluentui/react-motion-components-preview": "^0.15.3", + "@fluentui/react-radio": "^9.6.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3180,12 +3544,13 @@ } }, "node_modules/@fluentui/react-utilities": { - "version": "9.25.2", + "version": "9.26.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.26.2.tgz", + "integrity": "sha512-Yp2GGNoWifj8Z/VVir4HyRumRsqXnLJd4IP/Y70vEm9ruAvyqUvfn+1lQUuA+k/Reqw8GI+Ix7FTo3rogixZBg==", "license": "MIT", - "peer": true, "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", - "@fluentui/react-shared-contexts": "^9.25.2", + "@fluentui/react-shared-contexts": "^9.26.2", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3194,13 +3559,15 @@ } }, "node_modules/@fluentui/react-virtualizer": { - "version": "9.0.0-alpha.105", + "version": "9.0.0-alpha.111", + "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.111.tgz", + "integrity": "sha512-yku++0779Ve1RNz6y/HWjlXKd2x1wCSbWMydT2IdCICBVwolXjPYMpkqqZUSjbJ0N9gl6BfsCBpU9Dfe2bR8Zg==", "license": "MIT", "dependencies": { - "@fluentui/react-jsx-runtime": "^9.3.1", - "@fluentui/react-shared-contexts": "^9.25.2", - "@fluentui/react-utilities": "^9.25.2", - "@griffel/react": "^1.5.22", + "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-utilities": "^9.26.2", + "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { @@ -3211,19 +3578,22 @@ } }, "node_modules/@fluentui/tokens": { - "version": "1.0.0-alpha.22", + "version": "1.0.0-alpha.23", + "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", + "integrity": "sha512-uxrzF9Z+J10naP0pGS7zPmzSkspSS+3OJDmYIK3o1nkntQrgBXq3dBob4xSlTDm5aOQ0kw6EvB9wQgtlyy4eKQ==", "license": "MIT", - "peer": true, "dependencies": { "@swc/helpers": "^0.5.1" } }, "node_modules/@griffel/core": { - "version": "1.19.2", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.20.1.tgz", + "integrity": "sha512-ld1mX04zpmeHn8agx4slSEh8kJ+8or3Y0x9gsJNKSKn6GdCkZBSiGUh+oBXCBn8RKzz8l60TA9IhVSStnyKekA==", "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.0", - "@griffel/style-types": "^1.3.0", + "@griffel/style-types": "^1.4.0", "csstype": "^3.1.3", "rtl-css-js": "^1.16.1", "stylis": "^4.2.0", @@ -3231,10 +3601,12 @@ } }, "node_modules/@griffel/react": { - "version": "1.5.30", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.6.1.tgz", + "integrity": "sha512-mNM4/+dIXzqeHboWpVZ1/jiwTAYNc5/8y/V/HasnQ2QXnV6gSUYpeUk/0n6IFU3NJmVJly9JrLSfNo0hM/IFeA==", "license": "MIT", "dependencies": { - "@griffel/core": "^1.19.2", + "@griffel/core": "^1.20.1", "tslib": "^2.1.0" }, "peerDependencies": { @@ -3242,7 +3614,9 @@ } }, "node_modules/@griffel/style-types": { - "version": "1.3.0", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.4.0.tgz", + "integrity": "sha512-vNDfOGV7RN/XkA7vxgf7Z5HgW8eiBm5cHT9wQPhsKB4pxWom5u6eQ9CkYE5mCCTSPl9H6Nd1NBai04d4P6BD7Q==", "license": "MIT", "dependencies": { "csstype": "^3.1.3" @@ -3250,6 +3624,8 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3258,6 +3634,8 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3270,6 +3648,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3282,6 +3662,8 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3294,6 +3676,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -3303,6 +3687,8 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3312,6 +3698,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -3320,11 +3708,15 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -3333,273 +3725,458 @@ } }, "node_modules/@lexical/clipboard": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.39.0.tgz", + "integrity": "sha512-ylrHy8M+I5EH4utwqivslugqQhvgLTz9VEJdrb2RjbhKQEXwMcqKCRWh6cRfkYx64onE2YQE0nRIdzHhExEpLQ==", "license": "MIT", - "peer": true, "dependencies": { - "@lexical/html": "0.12.6", - "@lexical/list": "0.12.6", - "@lexical/selection": "0.12.6", - "@lexical/utils": "0.12.6" - }, - "peerDependencies": { - "lexical": "0.12.6" + "@lexical/html": "0.39.0", + "@lexical/list": "0.39.0", + "@lexical/selection": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/code": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.39.0.tgz", + "integrity": "sha512-3tqFOOzP5Z9nRkZPHZYmIyLXd28gMMrlAD3k2zxiH5vGnAqiYTezR24CpRDw1BaF2c8vCgY/9CNobZzjXivpIA==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.39.0", + "lexical": "0.39.0", + "prismjs": "^1.30.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.39.0.tgz", + "integrity": "sha512-2ET2nFeRhcc2YMrn184wxoEOTLl3UOlugi8ozuZFa6F4UDMXPq7nZRhiQNgYzhE6Z7NLMFrcmghvx652JbEowg==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.12.6", - "prismjs": "^1.27.0" + "@lexical/html": "0.39.0", + "@lexical/link": "0.39.0", + "@lexical/mark": "0.39.0", + "@lexical/table": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" }, "peerDependencies": { - "lexical": "0.12.6" + "react": ">=17.x", + "react-dom": ">=17.x" } }, "node_modules/@lexical/dragon": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.39.0.tgz", + "integrity": "sha512-JkcBAYPZGzfs29gtkePeJG9US1uwKW6PkUt8G4QZkMTt4QMDnadqXauFE+30rbpvRdeNcR7s+/jOuRHd5SurDQ==", "license": "MIT", - "peerDependencies": { - "lexical": "0.12.6" + "dependencies": { + "@lexical/extension": "0.39.0", + "lexical": "0.39.0" } }, - "node_modules/@lexical/hashtag": { - "version": "0.12.6", + "node_modules/@lexical/extension": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.39.0.tgz", + "integrity": "sha512-mp/WcF8E53FWPiUHgHQz382J7u7C4+cELYNkC00dKaymf8NhS6M65Y8tyDikNGNUcLXSzaluwK0HkiKjTYGhVQ==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.12.6" - }, - "peerDependencies": { - "lexical": "0.12.6" + "@lexical/utils": "0.39.0", + "@preact/signals-core": "^1.11.0", + "lexical": "0.39.0" + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.39.0.tgz", + "integrity": "sha512-CFLNB74a607nC2GGcjKNPbo/ZnehnR3zz9+S5bfUg5dblSGKdCfxHiyr2cDwHY3dfOTu+qtimfh2Zqxz4dfghA==", + "license": "MIT", + "dependencies": { + "@lexical/text": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/headless": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/headless/-/headless-0.39.0.tgz", + "integrity": "sha512-i/rZhFxP2nCF9rakOr3HTpsE7kYCLJv1/HUfOV0KHEHewivpRQdn/mtaQPqouQR9/gx0ZN8AnJLnYlbLzM367g==", "license": "MIT", - "peerDependencies": { - "lexical": "0.12.6" + "dependencies": { + "happy-dom": "^20.0.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/history": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.39.0.tgz", + "integrity": "sha512-kuctleDime0tRDxQNDW8i5d6D/ys5Npp2yoCBmdKS8HfS/jz7uPumfZcX7wvUvNAEVExh+bY9IxqIexyGkNUtA==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.12.6" - }, - "peerDependencies": { - "lexical": "0.12.6" + "@lexical/extension": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/html": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.39.0.tgz", + "integrity": "sha512-7VLWP5DpzBg3kKctpNK6PbhymKAtU6NAnKieopCfCIWlMW+EqpldteiIXGqSqrMRK0JWTmF1gKgr9nnQyOOsXw==", "license": "MIT", "dependencies": { - "@lexical/selection": "0.12.6", - "@lexical/utils": "0.12.6" - }, - "peerDependencies": { - "lexical": "0.12.6" + "@lexical/selection": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/link": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.39.0.tgz", + "integrity": "sha512-L1jSF2BVRHDqIQbKYFcQt3CqtVIphRA3QAW2VooYPNlKeaAb/yfFS+C60GX1cj96b0rMlHKrNC17ik2aEBZKLQ==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.12.6" - }, - "peerDependencies": { - "lexical": "0.12.6" + "@lexical/extension": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/list": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.39.0.tgz", + "integrity": "sha512-mxgSxUrakTCHtC+gF30BChQBJTsCMiMgfC2H5VvhcFwXMgsKE/aK9+a+C/sSvvzCmPXqzYsuAcGkJcrY3e5xlw==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.12.6" - }, - "peerDependencies": { - "lexical": "0.12.6" + "@lexical/extension": "0.39.0", + "@lexical/selection": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/mark": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.39.0.tgz", + "integrity": "sha512-wVs5498dWYOQ07FAHaFW6oYgNG3moBargf6es7+gHPzjlaoZ6Hd8sbvJtlT8F2RRlw+U+kUh4s8SjFuMSEJp0w==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.12.6" - }, - "peerDependencies": { - "lexical": "0.12.6" + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/markdown": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.39.0.tgz", + "integrity": "sha512-mPaKH2FSwRwU2bDbMiMtdOridaEvSLU3Q5l7bqYE+TW799C/1EEtiv4xSkI01SjV9YOxNf24VNOipAMymPueKA==", "license": "MIT", "dependencies": { - "@lexical/code": "0.12.6", - "@lexical/link": "0.12.6", - "@lexical/list": "0.12.6", - "@lexical/rich-text": "0.12.6", - "@lexical/text": "0.12.6", - "@lexical/utils": "0.12.6" - }, - "peerDependencies": { - "lexical": "0.12.6" + "@lexical/code": "0.39.0", + "@lexical/link": "0.39.0", + "@lexical/list": "0.39.0", + "@lexical/rich-text": "0.39.0", + "@lexical/text": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/offset": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.39.0.tgz", + "integrity": "sha512-8p+16AgFsG8ecZVQlFO6TQ+zHHHg7LKPNdm9BkklkJux41Y1+9rlPO12Mgbi4x2Hy2pRA8Gd/Su3hySGqEEVlA==", "license": "MIT", - "peerDependencies": { - "lexical": "0.12.6" + "dependencies": { + "lexical": "0.39.0" } }, "node_modules/@lexical/overflow": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.39.0.tgz", + "integrity": "sha512-BLtF4MNDrTNQFgryw6MPWh2Fj4GMjqC/6p9bbnZ9fdwMWKGSbsSNcK9PLlBwg3IzEK3XiibFDHUbsETwUd/bfw==", "license": "MIT", - "peerDependencies": { - "lexical": "0.12.6" + "dependencies": { + "lexical": "0.39.0" } }, "node_modules/@lexical/plain-text": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.39.0.tgz", + "integrity": "sha512-Ep0PGF7GlBNgiJJh/DBEPLt1WXyHUb7bCYZ4MUbD31AiJdG0p5a/g9dVTUr4QtNlCIXBCZjuatHyp6e2mzMacg==", "license": "MIT", - "peerDependencies": { - "@lexical/clipboard": "0.12.6", - "@lexical/selection": "0.12.6", - "@lexical/utils": "0.12.6", - "lexical": "0.12.6" + "dependencies": { + "@lexical/clipboard": "0.39.0", + "@lexical/dragon": "0.39.0", + "@lexical/selection": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/react": { - "version": "0.12.6", - "license": "MIT", - "dependencies": { - "@lexical/clipboard": "0.12.6", - "@lexical/code": "0.12.6", - "@lexical/dragon": "0.12.6", - "@lexical/hashtag": "0.12.6", - "@lexical/history": "0.12.6", - "@lexical/link": "0.12.6", - "@lexical/list": "0.12.6", - "@lexical/mark": "0.12.6", - "@lexical/markdown": "0.12.6", - "@lexical/overflow": "0.12.6", - "@lexical/plain-text": "0.12.6", - "@lexical/rich-text": "0.12.6", - "@lexical/selection": "0.12.6", - "@lexical/table": "0.12.6", - "@lexical/text": "0.12.6", - "@lexical/utils": "0.12.6", - "@lexical/yjs": "0.12.6", - "react-error-boundary": "^3.1.4" - }, - "peerDependencies": { - "lexical": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.39.0.tgz", + "integrity": "sha512-6ySVb5xv99GIkVzio4qqOBxkPgOSSeFAB4o9bVqtg72JbCoEKZPnWq5VVurGe1uiRJM8jvqTseM9mo2zTvUfXQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.16", + "@lexical/devtools-core": "0.39.0", + "@lexical/dragon": "0.39.0", + "@lexical/extension": "0.39.0", + "@lexical/hashtag": "0.39.0", + "@lexical/history": "0.39.0", + "@lexical/link": "0.39.0", + "@lexical/list": "0.39.0", + "@lexical/mark": "0.39.0", + "@lexical/markdown": "0.39.0", + "@lexical/overflow": "0.39.0", + "@lexical/plain-text": "0.39.0", + "@lexical/rich-text": "0.39.0", + "@lexical/table": "0.39.0", + "@lexical/text": "0.39.0", + "@lexical/utils": "0.39.0", + "@lexical/yjs": "0.39.0", + "lexical": "0.39.0", + "react-error-boundary": "^6.0.0" + }, + "peerDependencies": { "react": ">=17.x", "react-dom": ">=17.x" } }, "node_modules/@lexical/rich-text": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.39.0.tgz", + "integrity": "sha512-UoSgRi09nLP/mmD3ijdZycr9icnqlb761rzHC1gicuPDdTu0ruxAFbGanSE2h36ihSu0IUHwkpf4gBpgPPqWBw==", "license": "MIT", - "peerDependencies": { - "@lexical/clipboard": "0.12.6", - "@lexical/selection": "0.12.6", - "@lexical/utils": "0.12.6", - "lexical": "0.12.6" + "dependencies": { + "@lexical/clipboard": "0.39.0", + "@lexical/dragon": "0.39.0", + "@lexical/selection": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" } }, "node_modules/@lexical/selection": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.39.0.tgz", + "integrity": "sha512-j0cgNuTKDCdf/4MzRnAUwEqG6C/WQp18k2WKmX5KIVZJlhnGIJmlgSBrxjo8AuZ16DIHxTm2XNB4cUDCgZNuPA==", "license": "MIT", - "peer": true, - "peerDependencies": { - "lexical": "0.12.6" + "dependencies": { + "lexical": "0.39.0" } }, "node_modules/@lexical/table": { - "version": "0.12.6", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.39.0.tgz", + "integrity": "sha512-1eH11kV4bJ0fufCYl8DpE19kHwqUI8Ev5CZwivfAtC3ntwyNkeEpjCc0pqeYYIWN/4rTZ5jgB3IJV4FntyfCzw==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.39.0", + "@lexical/extension": "0.39.0", + "@lexical/utils": "0.39.0", + "lexical": "0.39.0" + } + }, + "node_modules/@lexical/text": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.39.0.tgz", + "integrity": "sha512-fcIgejtIgfMAkxio6BO1eLA2eb4oRIFoUVA2jAXdCaLVHrG/cizitbygPrgWnWd8nt1WlMuS4lxa0PJl7h7Lqg==", + "license": "MIT", + "dependencies": { + "lexical": "0.39.0" + } + }, + "node_modules/@lexical/utils": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.39.0.tgz", + "integrity": "sha512-8YChidpMJpwQc4nex29FKUeuZzC++QCS/Jt46lPuy1GS/BZQoPHFKQ5hyVvM9QVhc5CEs4WGNoaCZvZIVN8bQw==", + "license": "MIT", + "dependencies": { + "@lexical/list": "0.39.0", + "@lexical/selection": "0.39.0", + "@lexical/table": "0.39.0", + "lexical": "0.39.0" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.39.0.tgz", + "integrity": "sha512-peBrzIDoRWeyX9XTilKVdeJua6A+RZ24CG7lgGLEhmNSGCqpj9FqlC1Wtrul4wTSh85KlDeI1Nq30gnyeNKWYA==", "license": "MIT", "dependencies": { - "@lexical/utils": "0.12.6" + "@lexical/offset": "0.39.0", + "@lexical/selection": "0.39.0", + "lexical": "0.39.0" }, "peerDependencies": { - "lexical": "0.12.6" + "yjs": ">=13.5.22" } }, - "node_modules/@lexical/text": { - "version": "0.12.6", + "node_modules/@microsoft/applicationinsights-analytics-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-3.4.1.tgz", + "integrity": "sha512-zdxZzu50/gsE2JWrzeviHloFZu9r5/x2+OLD0TIYHhvrod321AKkStmKlDoep1JAsSephjxBfwTjciKiFXXyGA==", "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.4.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" + }, "peerDependencies": { - "lexical": "0.12.6" + "tslib": ">= 1.0.0" } }, - "node_modules/@lexical/utils": { - "version": "0.12.6", + "node_modules/@microsoft/applicationinsights-cfgsync-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-cfgsync-js/-/applicationinsights-cfgsync-js-3.4.1.tgz", + "integrity": "sha512-ifNgSIisKM/rZoLdpzS6sIQqBBRNXXAhiazZizwoP2MLdK93Z8JcPNi6eXkKPhW0Fi3SuoUivCaM7GgAMgVEFw==", "license": "MIT", - "peer": true, "dependencies": { - "@lexical/list": "0.12.6", - "@lexical/selection": "0.12.6", - "@lexical/table": "0.12.6" + "@microsoft/applicationinsights-core-js": "3.4.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" }, "peerDependencies": { - "lexical": "0.12.6" + "tslib": ">= 1.0.0" } }, - "node_modules/@lexical/yjs": { - "version": "0.12.6", + "node_modules/@microsoft/applicationinsights-channel-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.4.1.tgz", + "integrity": "sha512-QS1k6iwVwR1MznGAB1H0F9raqpevbFNbadLS5O1419pz9OEWBfF9wRQLnENCyo8QS9Q0IdiqnGAON/D8IywpWg==", "license": "MIT", "dependencies": { - "@lexical/offset": "0.12.6" + "@microsoft/applicationinsights-core-js": "3.4.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" }, "peerDependencies": { - "lexical": "0.12.6", - "yjs": ">=13.5.22" + "tslib": ">= 1.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.4.1.tgz", + "integrity": "sha512-eXIHZ1+nvBiJgVpufBiTP801Vtr5FEwjWZioUsb44NC/z/UcsZh2MDJ1mBpjaDO73LVYUw/ZZmDCCo6Pg/61kA==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "tslib": ">= 1.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, + "node_modules/@microsoft/applicationinsights-dependencies-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-dependencies-js/-/applicationinsights-dependencies-js-3.4.1.tgz", + "integrity": "sha512-cnjVVTxSeavmwCoOwXi/ZQuCcjo7SnYYRWyS+GsMCrTRTItVHZlVj6NHgmFEQZWNybrM+U1RgwZE2wXn1/Liyw==", "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.4.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, + "node_modules/@microsoft/applicationinsights-properties-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-properties-js/-/applicationinsights-properties-js-3.4.1.tgz", + "integrity": "sha512-s2cUuknjazaoCbh9i6ljymeZqeQqpyAE8v2ZUxCkAwRuxbonAvZWQtEr4QQmEHWJIdbWgn0Ge+OOlMtMkh+Ixg==", "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@microsoft/applicationinsights-core-js": "3.4.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web/-/applicationinsights-web-3.4.1.tgz", + "integrity": "sha512-gdYLIYkP11D+V71nNCupYsmWE8LAL9EpIR2Q7+B3n6dpck7tgaYMXFN3S5ZrOh3yxLAwt7GVXZoDcN2mGkagxQ==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-analytics-js": "3.4.1", + "@microsoft/applicationinsights-cfgsync-js": "3.4.1", + "@microsoft/applicationinsights-channel-js": "3.4.1", + "@microsoft/applicationinsights-core-js": "3.4.1", + "@microsoft/applicationinsights-dependencies-js": "3.4.1", + "@microsoft/applicationinsights-properties-js": "3.4.1", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.5 < 2.x", + "@nevware21/ts-utils": ">= 0.12.6 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" + } + }, + "node_modules/@nevware21/ts-async": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.5.tgz", + "integrity": "sha512-vwqaL05iJPjLeh5igPi8MeeAu10i+Aq7xko1fbo9F5Si6MnVN5505qaV7AhSdk5MCBJVT/UYMk3kgInNjDb4Ig==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.12.2 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.13.0.tgz", + "integrity": "sha512-F3mD+DsUn9OiZmZc5tg0oKqrJCtiCstwx+wE+DNzFYh2cCRUuzTYdK9zGGP/au2BWvbOQ6Tqlbjr2+dT1P3AlQ==", + "license": "MIT" + }, + "node_modules/@preact/signals-core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", + "integrity": "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.43", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -3611,9 +4188,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -3625,9 +4202,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -3639,9 +4216,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -3653,9 +4230,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -3667,9 +4244,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -3681,9 +4258,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -3695,9 +4272,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -3709,9 +4286,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -3723,9 +4300,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -3737,9 +4314,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -3751,9 +4342,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -3765,9 +4370,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -3779,9 +4384,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -3793,9 +4398,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -3807,13 +4412,12 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3821,9 +4425,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -3834,10 +4438,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "cpu": [ "arm64" ], @@ -3849,9 +4467,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -3863,9 +4481,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -3877,7 +4495,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -3889,7 +4509,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -3900,8 +4522,17 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { - "version": "0.5.17", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -3909,6 +4540,8 @@ }, "node_modules/@types/babel__core": { "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", "dependencies": { @@ -3921,6 +4554,8 @@ }, "node_modules/@types/babel__generator": { "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", "dependencies": { @@ -3929,6 +4564,8 @@ }, "node_modules/@types/babel__template": { "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", "dependencies": { @@ -3938,25 +4575,58 @@ }, "node_modules/@types/babel__traverse": { "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "license": "MIT", "dependencies": { "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "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": "*" @@ -3964,6 +4634,8 @@ }, "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": "*" @@ -3971,11 +4643,15 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "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": "*" @@ -3983,45 +4659,49 @@ }, "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.10.0", - "dev": true, + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, "node_modules/@types/prismjs": { - "version": "1.26.5", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.2", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.2", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } }, "node_modules/@types/react-syntax-highlighter": { "version": "15.5.13", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", "dev": true, "license": "MIT", "dependencies": { @@ -4030,22 +4710,40 @@ }, "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/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/type-utils": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4055,13 +4753,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.3", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -4069,16 +4769,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4088,18 +4789,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.3", - "@typescript-eslint/types": "^8.46.3", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4109,16 +4812,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3" + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4129,7 +4834,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", "dev": true, "license": "MIT", "engines": { @@ -4140,19 +4847,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4162,12 +4871,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", "dev": true, "license": "MIT", "engines": { @@ -4179,20 +4890,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.3", - "@typescript-eslint/tsconfig-utils": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4202,33 +4914,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4239,14 +4931,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4256,17 +4950,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4278,17 +4974,21 @@ }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.0", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.4", + "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.43", + "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -4296,129 +4996,304 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn": { - "version": "8.15.0", + "node_modules/@vitest/coverage-v8": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", + "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", "dev": true, "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.5", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.5", + "vitest": "4.1.5" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/ajv": { - "version": "6.12.6", + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/argparse": { - "version": "2.0.1", + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", "dev": true, - "license": "Python-2.0" - }, - "node_modules/bail": { - "version": "2.0.2", "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://opencollective.com/vitest" } }, - "node_modules/balanced-match": { - "version": "1.0.2", + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.20", + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/brace-expansion": { - "version": "1.1.12", + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/braces": { - "version": "3.0.3", + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=8" + "node": ">=0.4.0" } }, - "node_modules/browserslist": { - "version": "4.27.0", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "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", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -4427,16 +5302,10 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001751", + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", "dev": true, "funding": [ { @@ -4456,29 +5325,28 @@ }, "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": "4.1.2", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" } }, "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", @@ -4487,6 +5355,8 @@ }, "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", @@ -4495,6 +5365,8 @@ }, "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", @@ -4503,6 +5375,8 @@ }, "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", @@ -4511,47 +5385,34 @@ }, "node_modules/clsx": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "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/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/copy-to-clipboard": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", "license": "MIT", "dependencies": { "toggle-selection": "^1.0.6" @@ -4559,6 +5420,8 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -4570,12 +5433,44 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { - "version": "3.1.3", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/date-fns": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", "funding": { "type": "github", @@ -4584,6 +5479,8 @@ }, "node_modules/debug": { "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4597,8 +5494,17 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/decode-named-character-reference": { - "version": "1.2.0", + "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" @@ -4610,11 +5516,15 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "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" @@ -4622,6 +5532,8 @@ }, "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" @@ -4632,17 +5544,22 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.240", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "dev": true, "license": "ISC" }, "node_modules/embla-carousel": { "version": "8.6.0", - "license": "MIT", - "peer": true + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" }, "node_modules/embla-carousel-autoplay": { "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz", + "integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==", "license": "MIT", "peerDependencies": { "embla-carousel": "8.6.0" @@ -4650,13 +5567,37 @@ }, "node_modules/embla-carousel-fade": { "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-fade/-/embla-carousel-fade-8.6.0.tgz", + "integrity": "sha512-qaYsx5mwCz72ZrjlsXgs1nKejSrW+UhkbOMwLgfRT7w2LtdEB03nPRI06GHuHv5ac2USvbEiX2/nAHctcDwvpg==", "license": "MIT", "peerDependencies": { "embla-carousel": "8.6.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { - "version": "0.25.11", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4667,36 +5608,38 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -4705,6 +5648,8 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -4715,32 +5660,30 @@ } }, "node_modules/eslint": { - "version": "9.39.1", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", + "integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -4750,8 +5693,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4759,7 +5701,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -4774,7 +5716,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { @@ -4788,61 +5732,73 @@ "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.24", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=8.40" + "eslint": "^9 || ^10" } }, "node_modules/eslint-scope": { - "version": "8.4.0", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.6.0", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4854,6 +5810,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4865,6 +5823,8 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4873,75 +5833,75 @@ }, "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fault": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", "license": "MIT", "dependencies": { "format": "^0.2.0" @@ -4951,30 +5911,41 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, "engines": { - "node": ">=16.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/fill-range": { - "version": "7.1.1", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" } }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -4990,6 +5961,8 @@ }, "node_modules/flat-cache": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -5001,12 +5974,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/format": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", "engines": { "node": ">=0.4.x" } @@ -5028,6 +6005,8 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { @@ -5036,6 +6015,8 @@ }, "node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -5046,7 +6027,9 @@ } }, "node_modules/globals": { - "version": "16.5.0", + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", + "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", "dev": true, "license": "MIT", "engines": { @@ -5056,13 +6039,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "dev": true, - "license": "MIT" + "node_modules/happy-dom": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.9.0.tgz", + "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/happy-dom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "license": "MIT", + "engines": { + "node": ">=12" + } }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -5071,6 +6089,8 @@ }, "node_modules/hast-util-is-element": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -5082,6 +6102,8 @@ }, "node_modules/hast-util-parse-selector": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -5093,6 +6115,8 @@ }, "node_modules/hast-util-sanitize": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -5106,6 +6130,8 @@ }, "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", @@ -5131,6 +6157,8 @@ }, "node_modules/hast-util-to-text": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -5145,6 +6173,8 @@ }, "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" @@ -5156,6 +6186,8 @@ }, "node_modules/hastscript": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -5171,11 +6203,15 @@ }, "node_modules/hermes-estree": { "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", "dev": true, "license": "MIT" }, "node_modules/hermes-parser": { "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", "dev": true, "license": "MIT", "dependencies": { @@ -5183,49 +6219,105 @@ } }, "node_modules/highlight.js": { - "version": "11.11.1", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "license": "BSD-3-Clause", "engines": { - "node": ">=12.0.0" + "node": "*" } }, "node_modules/highlightjs-vue": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "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/ignore": { - "version": "5.3.2", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, "engines": { - "node": ">= 4" + "node": ">= 14" } }, - "node_modules/import-fresh": { - "version": "3.3.1", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=6" + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -5233,11 +6325,15 @@ } }, "node_modules/inline-style-parser": { - "version": "0.2.4", + "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/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", @@ -5246,6 +6342,8 @@ }, "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", @@ -5258,6 +6356,8 @@ }, "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", @@ -5266,6 +6366,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -5274,6 +6376,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -5285,22 +6389,18 @@ }, "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-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { "node": ">=12" @@ -5309,8 +6409,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, @@ -5324,23 +6433,96 @@ "url": "https://github.com/sponsors/dmonad" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, "node_modules/jsesc": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -5352,21 +6534,29 @@ }, "node_modules/json-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -5378,10 +6568,14 @@ }, "node_modules/keyborg": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.6.0.tgz", + "integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA==", "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -5390,6 +6584,8 @@ }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5401,14 +6597,15 @@ } }, "node_modules/lexical": { - "version": "0.12.6", - "license": "MIT", - "peer": true + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.39.0.tgz", + "integrity": "sha512-lpLv7MEJH5QDujEDlYqettL3ATVtNYjqyimzqgrm0RvCm3AO9WXSdsgTxuN7IAZRu88xkxCDeYubeUf4mNZVdg==", + "license": "MIT" }, "node_modules/lib0": { - "version": "0.2.114", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", - "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", + "version": "0.2.117", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", + "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", "license": "MIT", "dependencies": { "isomorphic.js": "^0.2.4" @@ -5428,6 +6625,8 @@ }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -5440,38 +6639,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "dev": true, - "license": "MIT" - }, "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/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/lowlight": { - "version": "3.3.0", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.0.0", - "highlight.js": "~11.11.0" + "fault": "^1.0.0", + "highlight.js": "~10.7.0" }, "funding": { "type": "github", @@ -5480,14 +6665,69 @@ }, "node_modules/lru-cache": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "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", @@ -5496,6 +6736,8 @@ }, "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", @@ -5510,6 +6752,8 @@ }, "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" @@ -5519,7 +6763,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", + "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", @@ -5542,6 +6788,8 @@ }, "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", @@ -5559,6 +6807,8 @@ }, "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", @@ -5574,6 +6824,8 @@ }, "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", @@ -5589,6 +6841,8 @@ }, "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", @@ -5602,6 +6856,8 @@ }, "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", @@ -5617,6 +6873,8 @@ }, "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", @@ -5631,6 +6889,8 @@ }, "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", @@ -5647,6 +6907,8 @@ }, "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", @@ -5669,6 +6931,8 @@ }, "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", @@ -5685,6 +6949,8 @@ }, "node_modules/mdast-util-newline-to-break": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz", + "integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -5697,6 +6963,8 @@ }, "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", @@ -5708,7 +6976,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", + "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", @@ -5728,6 +6998,8 @@ }, "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", @@ -5747,6 +7019,8 @@ }, "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" @@ -5756,16 +7030,10 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/merge2": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "engines": { - "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", @@ -5799,6 +7067,8 @@ }, "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", @@ -5831,6 +7101,8 @@ }, "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", @@ -5849,6 +7121,8 @@ }, "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", @@ -5863,6 +7137,8 @@ }, "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", @@ -5881,6 +7157,8 @@ }, "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", @@ -5897,6 +7175,8 @@ }, "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", @@ -5912,6 +7192,8 @@ }, "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" @@ -5923,6 +7205,8 @@ }, "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", @@ -5938,6 +7222,8 @@ }, "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", @@ -5957,6 +7243,8 @@ }, "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", @@ -5977,6 +7265,8 @@ }, "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", @@ -5995,6 +7285,8 @@ }, "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", @@ -6015,6 +7307,8 @@ }, "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", @@ -6035,6 +7329,8 @@ }, "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", @@ -6053,6 +7349,8 @@ }, "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", @@ -6070,6 +7368,8 @@ }, "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", @@ -6089,6 +7389,8 @@ }, "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", @@ -6107,6 +7409,8 @@ }, "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", @@ -6124,6 +7428,8 @@ }, "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", @@ -6144,6 +7450,8 @@ }, "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", @@ -6158,6 +7466,8 @@ }, "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", @@ -6172,6 +7482,8 @@ }, "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", @@ -6189,6 +7501,8 @@ }, "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", @@ -6206,6 +7520,8 @@ }, "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", @@ -6225,6 +7541,8 @@ }, "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", @@ -6245,6 +7563,8 @@ }, "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", @@ -6259,6 +7579,8 @@ }, "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", @@ -6271,35 +7593,32 @@ ], "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -6317,16 +7636,40 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.26", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT" }, "node_modules/optionator": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -6343,6 +7686,8 @@ }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6357,6 +7702,8 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -6369,19 +7716,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "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", @@ -6399,10 +7737,27 @@ }, "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/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -6411,23 +7766,36 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -6435,6 +7803,8 @@ }, "node_modules/postcss": { "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -6462,6 +7832,8 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -6470,6 +7842,8 @@ }, "node_modules/prismjs": { "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", "engines": { "node": ">=6" @@ -6477,6 +7851,8 @@ }, "node_modules/property-information": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", @@ -6485,70 +7861,48 @@ }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/react": { - "version": "19.2.0", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.0", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.5" } }, "node_modules/react-error-boundary": { - "version": "3.1.4", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.1.tgz", + "integrity": "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==", "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, "peerDependencies": { - "react": ">=16.13.1" + "react": "^18.0.0 || ^19.0.0" } }, - "node_modules/react-is": { - "version": "17.0.2", - "license": "MIT" - }, "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", @@ -6574,6 +7928,8 @@ }, "node_modules/react-refresh": { "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", "engines": { @@ -6581,7 +7937,9 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "16.1.0", + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha512-PjVawBGy80C6YbC5DDZJeUjBmC7skaoEUdvfFQediQHgCL7aKyVHe57SaJGfQsloGDac+gCpTfRdtxzWWKmCXA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -6598,27 +7956,10 @@ "react": ">= 0.14.0" } }, - "node_modules/react-syntax-highlighter/node_modules/highlight.js": { - "version": "10.7.3", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/react-syntax-highlighter/node_modules/lowlight": { - "version": "1.20.0", - "license": "MIT", - "dependencies": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/refractor": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -6633,6 +7974,8 @@ }, "node_modules/rehype-highlight": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -6646,8 +7989,34 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-highlight/node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/rehype-highlight/node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/rehype-sanitize": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -6660,6 +8029,8 @@ }, "node_modules/remark-breaks": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz", + "integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -6673,6 +8044,8 @@ }, "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", @@ -6689,6 +8062,8 @@ }, "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", @@ -6703,6 +8078,8 @@ }, "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", @@ -6718,6 +8095,8 @@ }, "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", @@ -6729,25 +8108,10 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rollup": { - "version": "4.52.5", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -6761,58 +8125,82 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/rtl-css-js": { "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", "dependencies": { - "queue-microtask": "^1.2.2" + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" } }, "node_modules/scheduler": { @@ -6823,6 +8211,8 @@ }, "node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -6831,6 +8221,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -6842,14 +8234,25 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6858,14 +8261,32 @@ }, "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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "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", @@ -6876,37 +8297,34 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/style-to-js": { - "version": "1.1.18", + "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.11" + "style-to-object": "1.0.14" } }, "node_modules/style-to-object": { - "version": "1.0.11", + "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.4" + "inline-style-parser": "0.2.7" } }, "node_modules/stylis": { - "version": "4.3.6", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", "license": "MIT" }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -6916,32 +8334,53 @@ "node": ">=8" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/tabster": { - "version": "8.5.6", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/tabster/-/tabster-8.7.0.tgz", + "integrity": "sha512-AKYquti8AdWzuqJdQo4LUMQDZrHoYQy6V+8yUq2PmgLZV10EaB+8BD0nWOfC/3TBp4mPNg4fbHkz6SFtkr0PpA==", "license": "MIT", "dependencies": { "keyborg": "2.6.0", "tslib": "^2.8.1" }, "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "4.40.0" + "@rollup/rollup-linux-x64-gnu": "4.53.3" } }, - "node_modules/tabster/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", - "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", - "cpu": [ - "x64" - ], + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=18" + } }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6955,51 +8394,72 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=14.0.0" } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" + "dependencies": { + "tldts-core": "^6.1.86" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "bin": { + "tldts": "bin/cli.js" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "is-number": "^7.0.0" + "tldts": "^6.1.32" }, "engines": { - "node": ">=8.0" + "node": ">=16" } }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "license": "MIT" + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } }, "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", @@ -7008,6 +8468,8 @@ }, "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", @@ -7015,7 +8477,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.1.0", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -7027,10 +8491,14 @@ }, "node_modules/tslib": { "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -7042,9 +8510,10 @@ }, "node_modules/typescript": { "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7054,14 +8523,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.3", + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.3", - "@typescript-eslint/parser": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3" + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7071,17 +8542,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici-types": { "version": "7.16.0", - "dev": true, + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "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", @@ -7099,6 +8573,8 @@ }, "node_modules/unist-util-find-after": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -7111,6 +8587,8 @@ }, "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" @@ -7122,6 +8600,8 @@ }, "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" @@ -7133,6 +8613,8 @@ }, "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" @@ -7143,7 +8625,9 @@ } }, "node_modules/unist-util-visit": { - "version": "5.0.0", + "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", @@ -7157,6 +8641,8 @@ }, "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", @@ -7168,7 +8654,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -7198,6 +8686,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7206,6 +8696,8 @@ }, "node_modules/use-sync-external-store": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -7213,6 +8705,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", @@ -7225,6 +8719,8 @@ }, "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", @@ -7236,12 +8732,13 @@ } }, "node_modules/vite": { - "version": "7.2.0", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -7309,36 +8806,161 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", + "node_modules/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", "dev": true, "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">=12.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "picomatch": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", - "peer": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -7351,25 +8973,83 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yjs": { - "version": "13.6.27", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", - "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", + "version": "13.6.30", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.30.tgz", + "integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==", "license": "MIT", - "peer": true, "dependencies": { "lib0": "^0.2.99" }, @@ -7384,6 +9064,8 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -7394,16 +9076,19 @@ } }, "node_modules/zod": { - "version": "4.1.12", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-validation-error": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", "dev": true, "license": "MIT", "engines": { @@ -7415,6 +9100,8 @@ }, "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", diff --git a/frontend/package.json b/frontend/package.json index 96a5df4..5e6d184 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,40 +8,48 @@ "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", - "clean": "rm -rf dist node_modules" + "clean": "rm -rf dist node_modules", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage" }, "dependencies": { - "@azure/msal-browser": "^4.26.0", - "@azure/msal-react": "^3.0.21", - "@fluentui-copilot/react-copilot": "0.30.0", - "@fluentui-copilot/react-copilot-chat": "0.13.0", - "@fluentui/react-components": "^9.72.4", - "@fluentui/react-icons": "^2.0.313", + "@azure/msal-browser": "^4.27.0", + "@azure/msal-react": "^3.0.23", + "@fluentui-copilot/react-copilot": "0.30.5", + "@fluentui-copilot/react-copilot-chat": "0.13.2", + "@fluentui/react-components": "^9.73.7", + "@fluentui/react-icons": "^2.0.324", + "@microsoft/applicationinsights-web": "^3.4.1", "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "date-fns": "^4.1.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", + "react": "^19.2.5", + "react-dom": "^19.2.5", "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^16.1.0", + "react-syntax-highlighter": "^16.1.1", "rehype-highlight": "^7.0.2", "rehype-sanitize": "^6.0.0", "remark-breaks": "^4.0.0", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "yjs": "^13.6.30" }, "devDependencies": { - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", + "@eslint/js": "^10.0.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.22", - "globals": "^16.5.0", + "@vitejs/plugin-react": "^5.2.0", + "@vitest/coverage-v8": "^4.1.5", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "jsdom": "^26.1.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.46.3", - "vite": "^7.2.0" + "typescript-eslint": "^8.59.0", + "vite": "^7.2.6", + "vitest": "^4.1.5" } } diff --git a/frontend/plugins/envcheck.ts b/frontend/plugins/envcheck.ts new file mode 100644 index 0000000..65e4e5b --- /dev/null +++ b/frontend/plugins/envcheck.ts @@ -0,0 +1,112 @@ +import type { Plugin, ViteDevServer } from "vite"; + +const REQUIRED_VARS = [ + "VITE_ENTRA_SPA_CLIENT_ID", + "VITE_ENTRA_TENANT_ID", +] as const; + +const ERROR_HTML = ` + + + + Setup Required + + + +
+

⚠️ Setup Required

+

Missing environment variables needed for Entra ID authentication.

+ +

The following variables are not set:

+
MISSING_VARS_PLACEHOLDER
+ +

How to fix

+
    +
  1. Run azd up from the repo root — this creates the Entra app registration and generates the required .env files automatically.
  2. +
  3. Restart the dev server after azd up completes.
  4. +
+ +

Coming from the AI Foundry portal?

+

The portal's "View sample app code" gives you AI resource variables, but this app also needs an Entra ID app registration for authentication. Running azd up creates it for you — even if your AI Foundry resources already exist.

+ +
azd up
+ +
+ 📖 See the README for full setup instructions. +
+
+ +`; + +export function envCheckPlugin(): Plugin { + let missing: string[] = []; + + return { + name: "env-check", + configResolved(config) { + if (config.command !== "serve") return; + + missing = REQUIRED_VARS.filter( + (v) => !process.env[v] || process.env[v] === "undefined" + ); + + if (missing.length > 0) { + const border = "━".repeat(60); + console.warn(`\n\x1b[31m${border}\x1b[0m`); + console.warn(`\x1b[31m ⚠️ SETUP REQUIRED\x1b[0m`); + console.warn(`\x1b[31m${border}\x1b[0m\n`); + console.warn( + ` Missing environment variables:\n${missing.map((v) => ` • ${v}`).join("\n")}\n` + ); + console.warn(` Run \x1b[36mazd up\x1b[0m from the repo root to create the`); + console.warn(` Entra app registration and generate .env files.\n`); + console.warn( + ` Coming from the AI Foundry portal? You still need to` + ); + console.warn( + ` run \x1b[36mazd up\x1b[0m — the portal gives AI resource vars, but` + ); + console.warn( + ` this app also requires an Entra ID app for authentication.\n` + ); + console.warn(`\x1b[31m${border}\x1b[0m\n`); + } + }, + configureServer(server: ViteDevServer) { + if (missing.length === 0) return; + + server.middlewares.use((_req, res, next) => { + if ( + _req.url?.startsWith("/@") || + _req.url?.startsWith("/__") || + _req.url?.startsWith("/api") + ) { + next(); + return; + } + + const html = ERROR_HTML.replace( + "MISSING_VARS_PLACEHOLDER", + missing.join("\n") + ); + + res.statusCode = 503; + res.setHeader("Content-Type", "text/html"); + res.end(html); + }); + }, + }; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5f5261e..8b7838b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,18 +3,13 @@ import { Spinner } from '@fluentui/react-components'; import { useAppState } from './hooks/useAppState'; import { InteractionType } from "@azure/msal-browser"; import { ErrorBoundary } from "./components/core/ErrorBoundary"; -import { AgentPreview } from "./components/AgentPreview"; +import { AgentChat } from "./components/AgentChat"; import { loginRequest } from "./config/authConfig"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { useAuth } from "./hooks/useAuth"; import type { IAgentMetadata } from "./types/chat"; import "./App.css"; -export interface ChatInterfaceRef { - clearChat: () => void; - loadConversation: (conversationId: string) => Promise; -} - function App() { // This hook handles authentication automatically - redirects if not authenticated useMsalAuthentication(InteractionType.Redirect, loginRequest); @@ -23,50 +18,51 @@ function App() { const [agentMetadata, setAgentMetadata] = useState(null); const [isLoadingAgent, setIsLoadingAgent] = useState(true); - useEffect(() => { - const fetchAgentMetadata = async () => { - if (auth.status !== 'authenticated') return; - - try { - const token = await getAccessToken(); - const apiUrl = import.meta.env.VITE_API_URL || '/api'; - - const response = await fetch(`${apiUrl}/agent`, { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - }); + // Wrap fetchAgentMetadata in useCallback to make it stable for the effect + const fetchAgentMetadata = useCallback(async () => { + if (auth.status !== 'authenticated') return; - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); + try { + const token = await getAccessToken(); + const apiUrl = import.meta.env.VITE_API_URL || '/api'; + + const response = await fetch(`${apiUrl}/agent`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' } + }); - const data = await response.json(); - setAgentMetadata(data); - - // Update document title with agent name - document.title = data.name ? `${data.name} - Azure AI Agent` : 'Azure AI Agent'; - } catch (error) { - console.error('Error fetching agent metadata:', error); - // Fallback data keeps UI functional on error - setAgentMetadata({ - id: 'fallback-agent', - object: 'agent', - createdAt: Date.now() / 1000, - name: 'Azure AI Agent', - description: 'Your intelligent conversational partner powered by Azure AI', - model: 'gpt-4o-mini', - metadata: { logo: 'Avatar_Default.svg' } - }); - document.title = 'Azure AI Agent'; - } finally { - setIsLoadingAgent(false); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - }; + const data = await response.json(); + setAgentMetadata(data); + + // Update document title with agent name + document.title = data.name ? `${data.name} - Azure AI Agent` : 'Azure AI Agent'; + } catch (error) { + console.error('Error fetching agent metadata:', error); + // Fallback data keeps UI functional on error + setAgentMetadata({ + id: 'fallback-agent', + object: 'agent', + createdAt: Date.now() / 1000, + name: 'Azure AI Agent', + description: 'Your intelligent conversational partner powered by Azure AI', + model: 'gpt-4o-mini', + metadata: { logo: 'Avatar_Default.svg' } + }); + document.title = 'Azure AI Agent'; + } finally { + setIsLoadingAgent(false); + } + }, [auth.status, getAccessToken]); + + useEffect(() => { fetchAgentMetadata(); - }, [auth.status]); // eslint-disable-line react-hooks/exhaustive-deps + }, [fetchAgentMetadata]); return ( @@ -89,11 +85,12 @@ function App() { {agentMetadata && (
-
)} diff --git a/frontend/src/components/AgentPreview.module.css b/frontend/src/components/AgentChat.module.css similarity index 57% rename from frontend/src/components/AgentPreview.module.css rename to frontend/src/components/AgentChat.module.css index 6f5b6fc..bd5fe5d 100644 --- a/frontend/src/components/AgentPreview.module.css +++ b/frontend/src/components/AgentChat.module.css @@ -14,15 +14,3 @@ position: relative; overflow: hidden; } - -.builtWithBadge { - position: absolute; - bottom: 24px; - left: 24px; - z-index: 10; - - /* Hide on mobile screens */ - @media (width <= 768px) { - display: none; - } -} diff --git a/frontend/src/components/AgentChat.tsx b/frontend/src/components/AgentChat.tsx new file mode 100644 index 0000000..e63c46a --- /dev/null +++ b/frontend/src/components/AgentChat.tsx @@ -0,0 +1,266 @@ +import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import { ChatInterface } from './ChatInterface'; +import { ConversationSidebar } from './ConversationSidebar'; +import { SettingsPanel } from './core/SettingsPanel'; +import { useAppState } from '../hooks/useAppState'; +import { useAuth } from '../hooks/useAuth'; +import { ChatService } from '../services/chatService'; +import { useAppContext } from '../contexts/AppContext'; +import { exportAsMarkdown, downloadMarkdown } from '../utils/exportConversation'; +import { trackFeedback } from '../services/telemetry'; +import type { IChatItem } from '../types/chat'; +import styles from './AgentChat.module.css'; + +interface AgentChatProps { + agentId: string; + agentName: string; + agentDescription?: string; + agentLogo?: string; + starterPrompts?: string[]; +} + +export const AgentChat: React.FC = ({ agentName, agentDescription, agentLogo, starterPrompts }) => { + const { chat, state } = useAppState(); + const { dispatch } = useAppContext(); + const { getAccessToken } = useAuth(); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + + // Create service instances + const apiUrl = import.meta.env.VITE_API_URL || '/api'; + + const chatService = useMemo(() => { + return new ChatService(apiUrl, getAccessToken, dispatch); + }, [apiUrl, getAccessToken, dispatch]); + + const handleSendMessage = async (text: string, files?: File[]) => { + if (chat.status === 'streaming' || chat.status === 'sending') { + dispatch({ type: 'CHAT_QUEUE_MESSAGE', text, files }); + return; + } + await chatService.sendMessage(text, chat.currentConversationId, files); + }; + + // Drain the queue when the stream completes + const pendingRef = useRef(chat.pendingMessages); + pendingRef.current = chat.pendingMessages; + + useEffect(() => { + if (chat.status === 'idle' && pendingRef.current.length > 0) { + const combinedText = pendingRef.current.map(m => m.text).join('\n\n'); + const combinedFiles = pendingRef.current.flatMap(m => m.files || []); + dispatch({ type: 'CHAT_CLEAR_QUEUE' }); + chatService.sendMessage( + combinedText, + chat.currentConversationId, + combinedFiles.length > 0 ? combinedFiles : undefined + ); + } + }, [chat.status, chat.currentConversationId, chatService, dispatch]); + + const handleDequeueMessage = (index: number) => { + dispatch({ type: 'CHAT_DEQUEUE_MESSAGE', index }); + }; + + const handleClearError = () => { + chatService.clearError(); + }; + + const handleNewChat = () => { + chatService.cancelStream(); + chatService.clearChat(); + }; + + const handleCancelStream = () => { + chatService.cancelStream(); + }; + + const handleRecoveredInputConsumed = () => { + dispatch({ type: 'CHAT_CONSUMED_RECOVERED_INPUT' }); + }; + + const handleRegenerate = useCallback(() => { + chatService.cancelStream(); + dispatch({ type: 'CHAT_REGENERATE' }); + }, [chatService, dispatch]); + + const handleEditMessage = useCallback((messageId: string, newText: string) => { + dispatch({ type: 'CHAT_EDIT_MESSAGE', messageId, newText }); + }, [dispatch]); + + const handleFeedback = useCallback((messageId: string, rating: 'positive' | 'negative') => { + trackFeedback(messageId, chat.currentConversationId, rating); + }, [chat.currentConversationId]); + + const handleCancelEdit = useCallback(() => { + dispatch({ type: 'CHAT_CANCEL_EDIT' }); + }, [dispatch]); + + const handleDownloadFile = useCallback(async (fileId: string, fileName: string, containerId?: string) => { + try { + await chatService.downloadFile(fileId, fileName, containerId); + } catch (err) { + dispatch({ + type: 'CHAT_ERROR', + error: { code: 'NETWORK', message: `Failed to download ${fileName}: ${err instanceof Error ? err.message : 'Unknown error'}`, recoverable: true }, + }); + } + }, [chatService, dispatch]); + + // Auto-send when regenerateText is set (from regenerate or edit actions) + useEffect(() => { + if (chat.regenerateText?.trim() && chat.status === 'idle') { + const text = chat.regenerateText; + dispatch({ type: 'CHAT_CONSUMED_REGENERATE' }); + chatService.sendMessage(text, chat.currentConversationId); + } + }, [chat.regenerateText, chat.status, chat.currentConversationId, chatService, dispatch]); + + const handleMcpApproval = async ( + approvalRequestId: string, + approved: boolean, + previousResponseId: string, + conversationId: string + ) => { + dispatch({ type: 'CHAT_MCP_APPROVAL_RESOLVED', approvalRequestId, resolved: approved ? 'approved' : 'rejected' }); + try { + await chatService.sendMcpApproval(approvalRequestId, approved, previousResponseId, conversationId); + } catch { + // Rollback so user can retry — clears resolved state, restoring buttons + dispatch({ type: 'CHAT_MCP_APPROVAL_RESOLVED', approvalRequestId, resolved: undefined }); + } + }; + + const handleExportConversation = useCallback(() => { + const md = exportAsMarkdown(chat.messages, agentName); + downloadMarkdown(md); + }, [chat.messages, agentName]); + + const handleToggleSidebar = useCallback(async () => { + const willOpen = !state.conversations.sidebarOpen; + dispatch({ type: 'CONVERSATIONS_TOGGLE_SIDEBAR' }); + if (willOpen) { + dispatch({ type: 'CONVERSATIONS_LOADING' }); + try { + const result = await chatService.listConversations(); + dispatch({ type: 'CONVERSATIONS_SET_LIST', conversations: result.conversations, hasMore: result.hasMore }); + } catch (error) { + console.error('Failed to load conversations:', error); + dispatch({ type: 'CONVERSATIONS_SET_LIST', conversations: [], hasMore: false }); + } + } + }, [state.conversations.sidebarOpen, dispatch, chatService]); + + const handleSidebarOpenChange = useCallback((open: boolean) => { + if (!open && state.conversations.sidebarOpen) { + dispatch({ type: 'CONVERSATIONS_TOGGLE_SIDEBAR' }); + } + }, [state.conversations.sidebarOpen, dispatch]); + + const handleLoadMoreConversations = useCallback(async () => { + dispatch({ type: 'CONVERSATIONS_LOADING' }); + try { + const currentCount = state.conversations.list.length; + const result = await chatService.listConversations(currentCount + 20); + // Slice off items we already have and append only new ones + const newItems = result.conversations.slice(currentCount); + // If no new items returned (e.g., backend limit cap), stop pagination + const hasMore = newItems.length > 0 && result.hasMore; + dispatch({ type: 'CONVERSATIONS_SET_LIST', conversations: newItems, hasMore, append: true }); + } catch (error) { + console.error('Failed to load more conversations:', error); + dispatch({ type: 'CONVERSATIONS_LOADING_DONE' }); + } + }, [state.conversations.list.length, dispatch, chatService]); + + const handleSelectConversation = useCallback(async (conversationId: string) => { + try { + chatService.cancelStream(); + const messages = await chatService.getConversationMessages(conversationId); + const chatItems: IChatItem[] = messages + .filter(msg => msg.role === 'user' || msg.role === 'assistant') + .map((msg, index) => ({ + id: `${conversationId}-${index}`, + role: msg.role as 'user' | 'assistant', + content: msg.content, + more: { time: new Date().toISOString() }, + })); + + dispatch({ type: 'CHAT_LOAD_CONVERSATION', conversationId, messages: chatItems }); + } catch (error) { + console.error('Failed to load conversation:', error); + } + }, [chatService, dispatch]); + + const handleDeleteConversation = useCallback(async (conversationId: string) => { + // Remove from UI immediately (optimistic) + dispatch({ type: 'CONVERSATIONS_REMOVE', conversationId }); + if (chat.currentConversationId === conversationId) { + chatService.clearChat(); + } + // Attempt server-side delete (may not be supported yet) + try { + await chatService.deleteConversation(conversationId); + } catch (error) { + // 501 = SDK doesn't support delete yet — item is hidden locally only + console.warn('Server-side conversation delete not available:', error); + } + }, [chatService, dispatch, chat.currentConversationId]); + + return ( +
+
+ setIsSettingsOpen(true)} + onNewChat={handleNewChat} + onCancelStream={handleCancelStream} + onMcpApproval={handleMcpApproval} + onToggleSidebar={handleToggleSidebar} + onExportConversation={handleExportConversation} + onRegenerate={handleRegenerate} + onEditMessage={handleEditMessage} + onCancelEdit={handleCancelEdit} + isEditing={!!chat.editSnapshot} + onFeedback={handleFeedback} + onDownloadFile={handleDownloadFile} + conversationId={chat.currentConversationId} + pendingMessages={chat.pendingMessages} + onDequeueMessage={handleDequeueMessage} + hasMessages={chat.messages.length > 0} + disabled={false} + agentName={agentName} + agentDescription={agentDescription} + agentLogo={agentLogo} + starterPrompts={starterPrompts} + /> +
+ + + + +
+ ); +}; diff --git a/frontend/src/components/AgentPreview.tsx b/frontend/src/components/AgentPreview.tsx deleted file mode 100644 index ec0a6da..0000000 --- a/frontend/src/components/AgentPreview.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { ChatInterface } from './ChatInterface'; -import { SettingsPanel } from './core/SettingsPanel'; -import { BuiltWithBadge } from './core/BuiltWithBadge'; -import { useAppState } from '../hooks/useAppState'; -import { useAuth } from '../hooks/useAuth'; -import { ChatService } from '../services/chatService'; -import { useAppContext } from '../contexts/AppContext'; -import styles from './AgentPreview.module.css'; - -interface AgentPreviewProps { - agentId: string; - agentName: string; - agentDescription?: string; - agentLogo?: string; -} - -export const AgentPreview: React.FC = ({ agentName, agentDescription, agentLogo }) => { - const { chat } = useAppState(); - const { dispatch } = useAppContext(); - const { getAccessToken } = useAuth(); - const [isSettingsOpen, setIsSettingsOpen] = useState(false); - - // Create service instances - const apiUrl = import.meta.env.VITE_API_URL || '/api'; - - const chatService = useMemo(() => { - return new ChatService(apiUrl, getAccessToken, dispatch); - }, [apiUrl, getAccessToken, dispatch]); - - const handleSendMessage = async (text: string, files?: File[]) => { - await chatService.sendMessage(text, chat.currentConversationId, files); - }; - - const handleClearError = () => { - chatService.clearError(); - }; - - const handleNewChat = () => { - chatService.clearChat(); - }; - - const handleCancelStream = () => { - chatService.cancelStream(); - }; - - return ( -
-
- setIsSettingsOpen(true)} - onNewChat={handleNewChat} - onCancelStream={handleCancelStream} - hasMessages={chat.messages.length > 0} - disabled={false} - agentName={agentName} - agentDescription={agentDescription} - agentLogo={agentLogo} - /> - - -
- - -
- ); -}; diff --git a/frontend/src/components/ChatInterface.module.css b/frontend/src/components/ChatInterface.module.css index 2dc7677..311d4cc 100644 --- a/frontend/src/components/ChatInterface.module.css +++ b/frontend/src/components/ChatInterface.module.css @@ -7,6 +7,7 @@ flex: 1; min-height: 0; overflow: hidden; + position: relative; } /* Messages container - scrolling area */ @@ -103,3 +104,55 @@ padding: 0 1rem; box-sizing: border-box; } + +/* New messages pill - anchored at bottom of messages area */ +.newMessagesPill { + position: absolute; + bottom: 12px; + left: 50%; + transform: translateX(-50%); + padding: 6px 16px; + border-radius: 999px; + border: 1px solid var(--colorNeutralStroke1); + background-color: var(--colorNeutralBackground1); + color: var(--colorBrandForeground1); + font-size: var(--fontSizeBase200); + font-weight: var(--fontWeightSemibold); + cursor: pointer; + z-index: 10; + box-shadow: var(--shadow8); + transition: background-color 0.15s ease, box-shadow 0.15s ease; +} + +.newMessagesPill:hover { + background-color: var(--colorNeutralBackground1Hover); + box-shadow: var(--shadow16); +} + +/* Built with badge - centered under input */ +.builtWithBadge { + padding-bottom: 0.5rem; +} + +.editBanner { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 16px; + font-size: 13px; + color: var(--colorNeutralForeground3); +} + +.undoButton { + background: none; + border: none; + color: var(--colorBrandForeground1); + cursor: pointer; + font-size: 13px; + padding: 0; + text-decoration: underline; +} + +.undoButton:hover { + color: var(--colorBrandForeground2); +} diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index 50c7c5e..d587a9e 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -1,14 +1,17 @@ -import { useRef, useEffect, useState, forwardRef, useImperativeHandle } from "react"; +import { useRef, useEffect, useState, useDeferredValue, useCallback } from "react"; import { AssistantMessage } from "./chat/AssistantMessage"; import { UserMessage } from "./chat/UserMessage"; +import { McpApprovalCard } from "./chat/McpApprovalCard"; import { StarterMessages } from "./chat/StarterMessages"; import { ChatInput } from "./chat/ChatInput"; +import { DropZone } from "./chat/DropZone"; import { Waves } from "./animations/Waves"; import { ErrorMessage } from "./core/ErrorMessage"; +import { KeyboardShortcuts } from "./core/KeyboardShortcuts"; +import { BuiltWithBadge } from "./core/BuiltWithBadge"; import type { IChatItem } from "../types/chat"; import type { AppState } from "../types/appState"; import type { AppError } from "../types/errors"; -import type { ChatInterfaceRef } from "../App"; import styles from './ChatInterface.module.css'; interface ChatInterfaceProps { @@ -16,52 +19,94 @@ interface ChatInterfaceProps { status: AppState['chat']['status']; error: AppError | null; streamingMessageId?: string; + recoveredInput?: string; + recoveredAttachments?: import('../types/chat').IFileAttachment[]; + pendingMessages?: Array<{ text: string; files?: File[] }>; onSendMessage: (text: string, files?: File[]) => void; + onMcpApproval?: (approvalRequestId: string, approved: boolean, previousResponseId: string, conversationId: string) => void; onClearError?: () => void; + onRecoveredInputConsumed?: () => void; + onDequeueMessage?: (index: number) => void; onOpenSettings?: () => void; onNewChat?: () => void; onCancelStream?: () => void; + onToggleSidebar?: () => void; + onExportConversation?: () => void; + onRegenerate?: () => void; + onEditMessage?: (messageId: string, newText: string) => void; + onCancelEdit?: () => void; + isEditing?: boolean; + onFeedback?: (messageId: string, rating: 'positive' | 'negative') => void; + onDownloadFile?: (fileId: string, fileName: string, containerId?: string) => void; hasMessages?: boolean; disabled: boolean; agentName?: string; agentDescription?: string; agentLogo?: string; + starterPrompts?: string[]; + conversationId?: string | null; } -export const ChatInterface = forwardRef((props, ref) => { - const { messages, status, error, streamingMessageId, onSendMessage, onClearError, onOpenSettings, onNewChat, onCancelStream, hasMessages, disabled, agentName, agentDescription, agentLogo } = props; +export const ChatInterface: React.FC = (props) => { + const { messages, status, error, streamingMessageId, recoveredInput, recoveredAttachments, pendingMessages, onSendMessage, onMcpApproval, onClearError, onRecoveredInputConsumed, onDequeueMessage, onOpenSettings, onNewChat, onCancelStream, onToggleSidebar, onExportConversation, onRegenerate, onEditMessage, onCancelEdit, isEditing, onFeedback, onDownloadFile, hasMessages, disabled, agentName, agentDescription, agentLogo, starterPrompts, conversationId } = props; + const deferredMessages = useDeferredValue(messages); const messagesEndRef = useRef(null); const [liveRegionMessage, setLiveRegionMessage] = useState(''); + const [isNearBottom, setIsNearBottom] = useState(true); + const [hasNewMessages, setHasNewMessages] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [isShortcutsOpen, setIsShortcutsOpen] = useState(false); + const [droppedFiles, setDroppedFiles] = useState(); + const dragCounterRef = useRef(0); + const observerRef = useRef(null); const isStreaming = status === 'streaming'; - const isBusy = disabled || ['sending', 'streaming'].includes(status); + const isBusy = disabled || status === 'sending'; - const scrollToBottom = () => { + const scrollToBottom = useCallback(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); - }; + }, []); + + const handleShowShortcuts = useCallback(() => setIsShortcutsOpen(true), []); + const handleDroppedFilesConsumed = useCallback(() => setDroppedFiles(undefined), []); + // Track whether user is near the bottom via IntersectionObserver useEffect(() => { - // Scroll immediately on every message change for real-time streaming feel - scrollToBottom(); - }, [messages]); + const el = messagesEndRef.current; + if (!el) return; + + observerRef.current = new IntersectionObserver( + ([entry]) => setIsNearBottom(entry.isIntersecting), + { threshold: 0.1 } + ); + observerRef.current.observe(el); + + return () => observerRef.current?.disconnect(); + }, []); + + useEffect(() => { + if (isNearBottom) { + scrollToBottom(); + setHasNewMessages(false); + } else if (messages.length > 0) { + setHasNewMessages(true); + } + }, [messages, isNearBottom, scrollToBottom]); - // Announce streaming status changes to screen readers useEffect(() => { if (isStreaming) { - setLiveRegionMessage('Assistant is responding'); + const streamingMessage = messages.find(m => m.id === streamingMessageId); + if (streamingMessage?.retryAttempt) { + setLiveRegionMessage(`Retrying, attempt ${streamingMessage.retryAttempt} of ${streamingMessage.maxRetries}`); + } else { + setLiveRegionMessage('Assistant is responding'); + } } else if (status === 'idle' && messages.length > 0 && messages[messages.length - 1].role === 'assistant') { setLiveRegionMessage('Response complete'); - // Clear the message after announcement const timer = setTimeout(() => setLiveRegionMessage(''), 1000); return () => clearTimeout(timer); } - }, [isStreaming, status, messages]); - - // Expose ref methods (no-op, controlled by parent now) - useImperativeHandle(ref, () => ({ - clearChat: () => {}, // Parent controls via AppContext - loadConversation: async () => {}, // Parent controls via AppContext - })); + }, [isStreaming, status, messages, streamingMessageId]); const handleSendMessage = (messageText: string, files?: File[]) => { if (!messageText.trim() || disabled) return; @@ -72,8 +117,66 @@ export const ChatInterface = forwardRef((p handleSendMessage(prompt); }; + // Drag-drop handlers + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounterRef.current++; + if (e.dataTransfer.types.includes('Files')) { + setIsDragging(true); + } + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounterRef.current--; + if (dragCounterRef.current === 0) { + setIsDragging(false); + } + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + dragCounterRef.current = 0; + setIsDragging(false); + + const files = Array.from(e.dataTransfer.files); + if (files.length > 0) { + setDroppedFiles(files); + } + }, []); + + // Global keyboard shortcuts + useEffect(() => { + const handler = (e: KeyboardEvent) => { + // Ctrl/Cmd+N → new chat + if (e.key === 'n' && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + onNewChat?.(); + } + }; + + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [onNewChat]); + return ( -
+
+ + {/* Live region for announcing streaming status to screen readers */}
((p agentName={agentName} agentDescription={agentDescription} agentLogo={agentLogo} + starterPrompts={starterPrompts} onPromptClick={handleStarterPromptClick} /> ) : ( @@ -106,9 +210,43 @@ export const ChatInterface = forwardRef((p `Assistant: ${messages[messages.length - 1].content.substring(0, 100)}` }
- {messages.map((message) => - message.role === "user" ? ( - + {(() => { + let lastUserIdx = -1; + for (let i = deferredMessages.length - 1; i >= 0; i--) { + if (deferredMessages[i].role === 'user') { lastUserIdx = i; break; } + } + return deferredMessages.map((message, index) => { + const isLastUserMessage = message.role === 'user' && index === lastUserIdx && !isStreaming; + return message.role === "approval" ? ( + onMcpApproval?.( + message.mcpApproval!.id, + true, + message.mcpApproval!.previousResponseId || '', + conversationId || '' + )} + onReject={() => onMcpApproval?.( + message.mcpApproval!.id, + false, + message.mcpApproval!.previousResponseId || '', + conversationId || '' + )} + disabled={isBusy} + agentName={agentName} + agentLogo={agentLogo} + /> + ) : message.role === "user" ? ( + ) : ( ((p isStreaming={isStreaming && message.id === streamingMessageId} agentName={agentName} agentLogo={agentLogo} + onRegenerate={onRegenerate} + onFeedback={onFeedback} + onDownloadFile={onDownloadFile} /> - ) - )} -
+ ); + }) + })()} +
)}
+ {hasNewMessages && !isNearBottom && ( + + )}
@@ -150,12 +301,25 @@ export const ChatInterface = forwardRef((p disabled={isBusy} onOpenSettings={onOpenSettings} onNewChat={onNewChat} + onToggleSidebar={onToggleSidebar} hasMessages={hasMessages} placeholder="Type your message here..." isStreaming={isStreaming} onCancelStream={isStreaming && onCancelStream ? onCancelStream : undefined} + isEditing={isEditing} + onCancelEdit={onCancelEdit} + onExportConversation={onExportConversation} + onShowShortcuts={handleShowShortcuts} + recoveredInput={recoveredInput} + recoveredAttachments={recoveredAttachments} + onRecoveredInputConsumed={onRecoveredInputConsumed} + pendingMessages={pendingMessages} + onDequeueMessage={onDequeueMessage} + droppedFiles={droppedFiles} + onDroppedFilesConsumed={handleDroppedFilesConsumed} /> +
); -}); +}; diff --git a/frontend/src/components/ConversationSidebar.tsx b/frontend/src/components/ConversationSidebar.tsx new file mode 100644 index 0000000..c388364 --- /dev/null +++ b/frontend/src/components/ConversationSidebar.tsx @@ -0,0 +1,322 @@ +import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react'; +import { + Drawer, + DrawerHeader, + DrawerHeaderTitle, + DrawerBody, + Button, + Spinner, + Text, + Input, + makeStyles, + tokens, +} from '@fluentui/react-components'; +import { + Dismiss24Regular, + ChatAdd24Regular, + Delete24Regular, + Search24Regular, + DismissCircle24Regular, +} from '@fluentui/react-icons'; +import type { ConversationSummary } from '../types/appState'; + +interface ConversationSidebarProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + conversations: ConversationSummary[]; + isLoading: boolean; + hasMore: boolean; + currentConversationId: string | null; + onSelectConversation: (conversationId: string) => void; + onNewChat: () => void; + onDeleteConversation: (conversationId: string) => void; + onLoadMore: () => void; +} + +const useStyles = makeStyles({ + drawer: { + width: '320px', + }, + newChatButton: { + width: '100%', + marginBottom: tokens.spacingVerticalM, + }, + conversationList: { + display: 'flex', + flexDirection: 'column', + gap: tokens.spacingVerticalXS, + }, + conversationItem: { + display: 'flex', + alignItems: 'center', + padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalM}`, + borderRadius: tokens.borderRadiusMedium, + cursor: 'pointer', + border: 'none', + backgroundColor: 'transparent', + width: '100%', + textAlign: 'left', + gap: tokens.spacingHorizontalS, + '&:hover': { + backgroundColor: tokens.colorNeutralBackground1Hover, + }, + }, + conversationItemActive: { + backgroundColor: tokens.colorNeutralBackground1Selected, + }, + conversationContent: { + flex: 1, + minWidth: 0, + display: 'flex', + flexDirection: 'column', + gap: '2px', + }, + conversationTitle: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, + conversationDate: { + fontSize: tokens.fontSizeBase100, + color: tokens.colorNeutralForeground3, + }, + deleteButton: { + flexShrink: 0, + opacity: 0, + '.conversation-item:hover &, .conversation-item:focus-within &': { + opacity: 1, + }, + ':focus': { + opacity: 1, + }, + }, + emptyState: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: tokens.spacingVerticalXXL, + color: tokens.colorNeutralForeground3, + textAlign: 'center', + }, + spinnerContainer: { + display: 'flex', + justifyContent: 'center', + padding: tokens.spacingVerticalXXL, + }, + loadMoreButton: { + width: '100%', + marginTop: tokens.spacingVerticalS, + }, + searchBox: { + marginBottom: tokens.spacingVerticalS, + }, + noResults: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + padding: tokens.spacingVerticalL, + color: tokens.colorNeutralForeground3, + textAlign: 'center', + }, +}); + +function formatDate(timestamp: number): string { + const date = new Date(timestamp * 1000); // Backend sends Unix seconds + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) return 'Today'; + if (diffDays === 1) return 'Yesterday'; + if (diffDays < 7) return `${diffDays} days ago`; + return date.toLocaleDateString(); +} + +export const ConversationSidebar: React.FC = ({ + isOpen, + onOpenChange, + conversations, + isLoading, + hasMore, + currentConversationId, + onSelectConversation, + onNewChat, + onDeleteConversation, + onLoadMore, +}) => { + const styles = useStyles(); + const [searchQuery, setSearchQuery] = useState(''); + const debounceRef = useRef | null>(null); + const [debouncedQuery, setDebouncedQuery] = useState(''); + + useEffect(() => { + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + }, []); + + const handleSearchChange = useCallback((_: React.ChangeEvent, data: { value: string }) => { + setSearchQuery(data.value); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + setDebouncedQuery(data.value); + }, 300); + }, []); + + const handleClearSearch = useCallback(() => { + setSearchQuery(''); + setDebouncedQuery(''); + if (debounceRef.current) clearTimeout(debounceRef.current); + }, []); + + const filteredConversations = useMemo(() => { + if (!debouncedQuery.trim()) return conversations; + const query = debouncedQuery.toLowerCase(); + return conversations.filter(c => c.title?.toLowerCase().includes(query)); + }, [conversations, debouncedQuery]); + + const handleDelete = useCallback( + (e: React.MouseEvent, conversationId: string) => { + e.stopPropagation(); + onDeleteConversation(conversationId); + }, + [onDeleteConversation] + ); + + return ( + onOpenChange(open)} + position="start" + className={styles.drawer} + > + + } + onClick={() => onOpenChange(false)} + /> + } + > + Conversations + + + + + + + {conversations.length > 0 && ( + } + contentAfter={ + searchQuery ? ( +
+ ))} + + {hasMore && ( + + )} + + )} + + + ); +}; diff --git a/frontend/src/components/chat/AssistantMessage.module.css b/frontend/src/components/chat/AssistantMessage.module.css index 7bf6b5b..8429144 100644 --- a/frontend/src/components/chat/AssistantMessage.module.css +++ b/frontend/src/components/chat/AssistantMessage.module.css @@ -3,6 +3,7 @@ margin: 0 16px; padding: 4px 0; animation: fadeIn 0.3s ease-in; + min-width: 0; } @keyframes fadeIn { @@ -51,9 +52,173 @@ } } +/* Retrying indicator */ +.retryingState { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 0; + color: var(--colorNeutralForeground3); +} + +/* Tool-use step indicator */ +.toolUseIndicator { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + color: var(--colorNeutralForeground3); +} + +.retryingIcon { + animation: spin 1.5s linear infinite; + font-size: 16px; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + /* Timestamp styling */ .timestamp { font-size: 12px; color: var(--colorNeutralForeground3); margin-right: 8px; } + +/* Citation list container */ +.citationList { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +/* Citation button - matches Foundry style */ +.citation { + display: inline-flex; + align-items: center; + gap: 0; + padding: 0; + border-radius: 6px; + background-color: var(--colorNeutralBackground1); + border: 1px solid var(--colorNeutralStroke1); + cursor: default; + font-size: 13px; + color: var(--colorNeutralForeground1); + overflow: hidden; + transition: box-shadow 0.3s ease, border-color 0.15s ease; +} + +/* Clickable citation (URI with URL) */ +.citationClickable { + cursor: pointer; +} + +.citationClickable:hover { + border-color: var(--colorBrandStroke1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.citationClickable:focus-visible { + outline: 2px solid var(--colorBrandStroke1); + outline-offset: 1px; +} + +/* Highlight animation when scrolled to from inline citation */ +.citationHighlight { + animation: citationPulse 2s ease-out; +} + +@keyframes citationPulse { + 0% { + box-shadow: 0 0 0 4px var(--colorBrandBackground2); + border-color: var(--colorBrandStroke1); + } + 100% { + box-shadow: 0 0 0 0 transparent; + border-color: var(--colorNeutralStroke1); + } +} + +/* Numbered badge box on left */ +.citationNumber { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 100%; + padding: 6px 8px; + background-color: var(--colorNeutralBackground3); + border-right: 1px solid var(--colorNeutralStroke1); + color: var(--colorNeutralForeground2); + font-size: 12px; + font-weight: 500; +} + +/* Icon and label container */ +.citationContent { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; +} + +.citationIcon { + width: 14px; + height: 14px; + flex-shrink: 0; + color: var(--colorNeutralForeground3); +} + +.citationLabel { + color: var(--colorNeutralForeground1); + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* External link indicator for URI citations */ +.citationExternalIcon { + width: 12px; + height: 12px; + flex-shrink: 0; + color: var(--colorNeutralForeground3); + margin-left: 4px; +} + +/* Reference count badge for deduplicated citations */ +.citationCount { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 1px 6px; + border-radius: 10px; + background-color: var(--colorBrandBackground2); + color: var(--colorBrandForeground1); + font-size: 11px; + font-weight: 500; + margin-left: 4px; +} + +/* Footnote layout */ +.footnoteContainer { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.metadataRow { + display: flex; + align-items: center; + justify-content: space-between; +} + +.metadataLeft { + display: flex; + align-items: center; + gap: 8px; +} diff --git a/frontend/src/components/chat/AssistantMessage.tsx b/frontend/src/components/chat/AssistantMessage.tsx index 9503fae..9f9c0af 100644 --- a/frontend/src/components/chat/AssistantMessage.tsx +++ b/frontend/src/components/chat/AssistantMessage.tsx @@ -1,18 +1,37 @@ -import { Suspense, memo } from 'react'; -import { Spinner } from '@fluentui/react-components'; +import { Suspense, memo, useMemo, useCallback } from 'react'; +import { Spinner, Tooltip, Text } from '@fluentui/react-components'; import { CopilotMessage } from '@fluentui-copilot/react-copilot-chat'; +import { DocumentRegular, GlobeRegular, FolderRegular, OpenRegular, ArrowSyncRegular } from '@fluentui/react-icons'; import { Markdown } from '../core/Markdown'; import { AgentIcon } from '../core/AgentIcon'; import { UsageInfo } from './UsageInfo'; +import { MessageActions } from './MessageActions'; import { useFormatTimestamp } from '../../hooks/useFormatTimestamp'; -import type { IChatItem } from '../../types/chat'; +import { parseContentWithCitations } from '../../utils/citationParser'; +import type { IChatItem, IAnnotation } from '../../types/chat'; import styles from './AssistantMessage.module.css'; +function getToolUseLabel(toolName: string): string { + switch (toolName) { + case 'file_search': + return 'Searching files\u2026'; + case 'code_interpreter': + return 'Running code\u2026'; + case 'function_call': + return 'Calling tool\u2026'; + default: + return 'Working\u2026'; + } +} + interface AssistantMessageProps { message: IChatItem; agentName?: string; agentLogo?: string; isStreaming?: boolean; + onRegenerate?: () => void; + onFeedback?: (messageId: string, rating: 'positive' | 'negative') => void; + onDownloadFile?: (fileId: string, fileName: string, containerId?: string) => void; } function AssistantMessageComponent({ @@ -20,12 +39,137 @@ function AssistantMessageComponent({ agentName = 'AI Assistant', agentLogo, isStreaming = false, + onRegenerate, + onFeedback, + onDownloadFile, }: AssistantMessageProps) { const formatTimestamp = useFormatTimestamp(); const timestamp = message.more?.time ? formatTimestamp(new Date(message.more.time)) : ''; // Show custom loading indicator when streaming with no content - const showLoadingDots = isStreaming && !message.content; + const showLoadingDots = isStreaming && !message.content && !message.retryAttempt; + const isRetrying = isStreaming && !!message.retryAttempt; + const hasAnnotations = message.annotations && message.annotations.length > 0; + + // Parse content with citations for consistent numbering between inline and footnotes + const parsedContent = useMemo(() => { + if (!hasAnnotations) return null; + return parseContentWithCitations(message.content, message.annotations); + }, [message.content, message.annotations, hasAnnotations]); + + // Get unique annotations with consistent indices + // If the parser found citations (inline placeholders), use those + // Otherwise, fall back to displaying all annotations as footnotes + const indexedCitations = useMemo(() => { + if (parsedContent?.citations && parsedContent.citations.length > 0) { + return parsedContent.citations; + } + // No inline placeholders found - display all annotations as numbered footnotes + // Deduplicate by label+type for fallback case + if (message.annotations && message.annotations.length > 0) { + const seen = new Map(); + message.annotations.forEach((annotation) => { + const key = `${annotation.type}:${annotation.label}:${annotation.url || annotation.fileId || ''}`; + if (seen.has(key)) { + seen.get(key)!.count++; + } else { + seen.set(key, { index: seen.size + 1, annotation, count: 1 }); + } + }); + return Array.from(seen.values()); + } + return []; + }, [parsedContent, message.annotations]); + + const handleFeedback = useCallback((rating: 'positive' | 'negative') => { + onFeedback?.(message.id, rating); + }, [message.id, onFeedback]); + + // Handle citation click - scroll to footnote or open URL + const handleCitationClick = useCallback((index: number, annotation?: IAnnotation) => { + if (annotation?.type === 'uri_citation' && annotation.url) { + window.open(annotation.url, '_blank', 'noopener,noreferrer'); + } else { + // Scroll to citation in footnotes + const citationElement = document.getElementById(`citation-${message.id}-${index}`); + if (citationElement) { + citationElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + citationElement.classList.add(styles.citationHighlight); + setTimeout(() => { + citationElement.classList.remove(styles.citationHighlight); + }, 2000); + } + } + }, [message.id]); + + // Build citation elements matching Foundry style + const renderCitation = (annotation: IAnnotation, index: number, count: number = 1) => { + const getIcon = () => { + switch (annotation.type) { + case 'uri_citation': + return ; + case 'file_path': + return ; + default: + return ; + } + }; + + const citationNumber = index; + const tooltipContent = annotation.quote + ? `${annotation.label}${count > 1 ? ` (referenced ${count} times)` : ''}\n\n"${annotation.quote.slice(0, 200)}${annotation.quote.length > 200 ? '...' : ''}"` + : `${annotation.label}${count > 1 ? ` (referenced ${count} times)` : ''}`; + + const hasFileDownload = (annotation.type === 'file_path' || annotation.type === 'container_file_citation') && annotation.fileId; + const isClickable = (annotation.type === 'uri_citation' && annotation.url) || hasFileDownload; + + const handleClick = () => { + if (annotation.type === 'uri_citation' && annotation.url) { + window.open(annotation.url, '_blank', 'noopener,noreferrer'); + } else if (hasFileDownload && annotation.fileId) { + onDownloadFile?.(annotation.fileId, annotation.label, annotation.containerId); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (isClickable && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + handleClick(); + } + }; + + // Render citation button matching Foundry style + return ( + + + {citationNumber} + + {getIcon()} + {annotation.label} + {count > 1 && ×{count}} + {isClickable && } + + + + ); + }; + + const citations = indexedCitations.map(({ index, annotation, count }) => + renderCitation(annotation, index, count) + ); return ( AI-generated content may be incorrect} footnote={ - <> - {timestamp && {timestamp}} - {message.more?.usage && ( - +
+ {hasAnnotations && !isStreaming && ( +
+ {citations} +
)} - +
+
+ {timestamp && {timestamp}} + {message.more?.usage && ( + + )} +
+ {!isStreaming && message.content && onRegenerate && ( + + )} +
+
} > {showLoadingDots ? ( -
- - - + isStreaming && message.activeToolUse ? ( +
+ + {getToolUseLabel(message.activeToolUse)} +
+ ) : ( +
+ + + +
+ ) + ) : isRetrying ? ( +
+ + + Retrying ({message.retryAttempt}/{message.maxRetries})... +
) : ( - }> - - + <> + }> + + + {isStreaming && message.activeToolUse && ( +
+ + {getToolUseLabel(message.activeToolUse)} +
+ )} + )} ); } export const AssistantMessage = memo(AssistantMessageComponent, (prev, next) => { - // Re-render only if streaming state or content/usage changes return ( prev.message.id === next.message.id && prev.message.content === next.message.content && prev.isStreaming === next.isStreaming && prev.agentLogo === next.agentLogo && - prev.message.more?.usage === next.message.more?.usage + prev.message.more?.usage === next.message.more?.usage && + prev.message.annotations?.length === next.message.annotations?.length && + prev.message.retryAttempt === next.message.retryAttempt && + prev.message.activeToolUse === next.message.activeToolUse ); }); diff --git a/frontend/src/components/chat/ChatInput.tsx b/frontend/src/components/chat/ChatInput.tsx index 27bc85a..df851ab 100644 --- a/frontend/src/components/chat/ChatInput.tsx +++ b/frontend/src/components/chat/ChatInput.tsx @@ -4,10 +4,12 @@ import { ImperativeControlPlugin, type ImperativeControlPluginRef, } from '@fluentui-copilot/react-copilot'; -import { Button, Toast, ToastTitle, Toaster, useId, useToastController, Text, makeStyles, tokens } from '@fluentui/react-components'; -import { Attach24Regular, Settings24Regular, ChatAdd24Regular, Stop24Regular } from '@fluentui/react-icons'; +import { Button, Toast, ToastTitle, Toaster, useId, useToastController, Text, makeStyles, tokens, Menu, MenuTrigger, MenuPopover, MenuList, MenuItem } from '@fluentui/react-components'; +import { Attach24Regular, Stop24Regular, MoreHorizontal24Regular, History24Regular, Settings24Regular, ChatAdd24Regular, ArrowDownload24Regular, Keyboard24Regular } from '@fluentui/react-icons'; import { FilePreview } from './FilePreview'; -import { validateImageFile, validateFileCount } from '../../utils/fileAttachments'; +import { VoiceInput } from './VoiceInput'; +import { MessageQueue } from './MessageQueue'; +import { validateFile, validateFileCount } from '../../utils/fileAttachments'; import styles from './ChatInput.module.css'; const CHAR_WARNING_THRESHOLD = 3000; @@ -41,9 +43,21 @@ interface ChatInputProps { placeholder?: string; onOpenSettings?: () => void; onNewChat?: () => void; + onToggleSidebar?: () => void; + onExportConversation?: () => void; + onShowShortcuts?: () => void; hasMessages?: boolean; isStreaming?: boolean; onCancelStream?: () => void; + isEditing?: boolean; + onCancelEdit?: () => void; + recoveredInput?: string; + recoveredAttachments?: import('../../types/chat').IFileAttachment[]; + onRecoveredInputConsumed?: () => void; + pendingMessages?: Array<{ text: string; files?: File[] }>; + onDequeueMessage?: (index: number) => void; + droppedFiles?: File[]; + onDroppedFilesConsumed?: () => void; } const focusInput = (containerRef: React.RefObject) => { @@ -59,9 +73,21 @@ export const ChatInput: React.FC = ({ placeholder = "Type your message...", onOpenSettings, onNewChat, + onToggleSidebar, + onExportConversation, + onShowShortcuts, hasMessages = false, isStreaming = false, onCancelStream, + isEditing = false, + onCancelEdit, + recoveredInput, + recoveredAttachments, + onRecoveredInputConsumed, + pendingMessages = [], + onDequeueMessage, + droppedFiles, + onDroppedFilesConsumed, }) => { const [inputText, setInputText] = useState(""); const [selectedFiles, setSelectedFiles] = useState([]); @@ -90,6 +116,7 @@ export const ChatInput: React.FC = ({ const timer = setTimeout(() => focusInput(inputContainerRef), 100); return () => clearTimeout(timer); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only on mount // Restore focus after message is sent (when status changes from disabled back to enabled) @@ -110,6 +137,75 @@ export const ChatInput: React.FC = ({ } }, [hasMessages, disabled]); + useEffect(() => { + if (recoveredInput) { + setInputText(recoveredInput); + controlRef.current?.setInputText(recoveredInput); + // Restore attachments by converting dataURIs back to Files + if (recoveredAttachments?.length) { + for (const att of recoveredAttachments) { + if (att.dataUri) { + try { + const res = fetch(att.dataUri); + res.then(r => r.blob()).then(blob => { + const file = new File([blob], att.fileName, { type: blob.type }); + setSelectedFiles(prev => [...prev, file]); + }); + } catch { /* skip unrecoverable attachments */ } + } + } + } + onRecoveredInputConsumed?.(); + const timer = setTimeout(() => focusInput(inputContainerRef), 50); + return () => clearTimeout(timer); + } + }, [recoveredInput, recoveredAttachments, onRecoveredInputConsumed]); + + // Clear input when edit is cancelled + const prevEditingRef = useRef(isEditing); + useEffect(() => { + if (prevEditingRef.current && !isEditing) { + setInputText(""); + controlRef.current?.setInputText(""); + setSelectedFiles([]); + } + prevEditingRef.current = isEditing; + }, [isEditing]); + + // Accept files from drag-drop via parent + useEffect(() => { + if (droppedFiles && droppedFiles.length > 0) { + const countValidation = validateFileCount(droppedFiles, selectedFiles.length); + if (!countValidation.valid) { + dispatchToast( + + {countValidation.error} + , + { intent: 'warning' }, + ); + } else { + const validFiles: File[] = []; + for (const file of droppedFiles) { + const validation = validateFile(file); + if (validation.valid) { + validFiles.push(file); + } else { + dispatchToast( + + {validation.error} + , + { intent: 'error' }, + ); + } + } + if (validFiles.length > 0) { + setSelectedFiles(prev => [...prev, ...validFiles]); + } + } + onDroppedFilesConsumed?.(); + } + }, [droppedFiles, onDroppedFilesConsumed, selectedFiles.length, dispatchToast]); + const handleSubmit = () => { if (inputText && inputText.trim() !== "") { onSubmit(inputText.trim(), selectedFiles.length > 0 ? selectedFiles : undefined); @@ -144,7 +240,7 @@ export const ChatInput: React.FC = ({ // Validate each file const validFiles: File[] = []; for (const file of files) { - const validation = validateImageFile(file); + const validation = validateFile(file); if (!validation.valid) { dispatchToast( @@ -208,7 +304,7 @@ export const ChatInput: React.FC = ({ // Validate each file const validFiles: File[] = []; for (const file of files) { - const validation = validateImageFile(file); + const validation = validateFile(file); if (!validation.valid) { dispatchToast( @@ -235,6 +331,13 @@ export const ChatInput: React.FC = ({ } }; + const handleVoiceTranscript = (transcript: string) => { + const newText = inputText ? `${inputText} ${transcript}` : transcript; + setInputText(newText); + controlRef.current?.setInputText(newText); + focusInput(inputContainerRef); + }; + return ( <> @@ -249,7 +352,7 @@ export const ChatInput: React.FC = ({ aria-label="Chat Input" aria-describedby={showCounter ? charCounterId : undefined} charactersRemainingMessage={() => ``} - disabled={disabled || isStreaming} + disabled={disabled} history={true} onChange={(_, data) => setInputText(data.value)} onSubmit={handleSubmit} @@ -264,26 +367,11 @@ export const ChatInput: React.FC = ({
)} + {pendingMessages.length > 0 && onDequeueMessage && ( + + )}
- {onOpenSettings && ( -
@@ -308,7 +443,8 @@ export const ChatInput: React.FC = ({ multiple style={{ display: 'none' }} onChange={handleFileSelect} - accept="image/*" + accept="image/*,.pdf,.txt,.md,.csv,.json,.html,.xml" + aria-label="Upload files" /> diff --git a/frontend/src/components/chat/CitationMarker.module.css b/frontend/src/components/chat/CitationMarker.module.css new file mode 100644 index 0000000..ee43900 --- /dev/null +++ b/frontend/src/components/chat/CitationMarker.module.css @@ -0,0 +1,48 @@ +/* Inline citation marker - superscript badge */ +.citationMarker { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.7em; + font-weight: 600; + min-width: 1.2em; + height: 1.2em; + padding: 0 0.3em; + margin: 0 0.1em; + border-radius: 4px; + background-color: var(--colorBrandBackground2); + color: var(--colorBrandForeground1); + cursor: pointer; + vertical-align: super; + line-height: 1; + transition: background-color 0.15s ease, transform 0.1s ease; + user-select: none; +} + +.citationMarker:hover { + background-color: var(--colorBrandBackground2Hover); + transform: scale(1.1); +} + +.citationMarker:focus-visible { + outline: 2px solid var(--colorBrandStroke1); + outline-offset: 1px; +} + +.citationMarker:active { + transform: scale(0.95); +} + +/* Highlight animation when scrolled to */ +.highlight { + animation: highlightPulse 2s ease-out; +} + +@keyframes highlightPulse { + 0% { + box-shadow: 0 0 0 4px var(--colorBrandBackground2); + } + 100% { + box-shadow: 0 0 0 0 transparent; + } +} diff --git a/frontend/src/components/chat/CitationMarker.tsx b/frontend/src/components/chat/CitationMarker.tsx new file mode 100644 index 0000000..77f7b5b --- /dev/null +++ b/frontend/src/components/chat/CitationMarker.tsx @@ -0,0 +1,54 @@ +import { memo } from 'react'; +import { Tooltip } from '@fluentui/react-components'; +import type { IAnnotation } from '../../types/chat'; +import styles from './CitationMarker.module.css'; + +interface CitationMarkerProps { + /** 1-based citation index */ + index: number; + /** The annotation data */ + annotation?: IAnnotation; + /** Callback when marker is clicked */ + onClick: (index: number, annotation?: IAnnotation) => void; +} + +/** + * Inline citation marker rendered as a superscript badge. + * Clicking invokes the onClick handler to scroll/navigate to the citation. + */ +function CitationMarkerComponent({ + index, + annotation, + onClick +}: CitationMarkerProps) { + const sourcePrefix = annotation?.type === 'uri_citation' ? '🔗 ' : annotation?.type === 'file_citation' ? '📄 ' : ''; + const tooltipContent = `${sourcePrefix}${annotation?.label || `Citation ${index}`}`; + + const handleClick = () => { + onClick(index, annotation); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleClick(); + } + }; + + return ( + + + {index} + + + ); +} + +export const CitationMarker = memo(CitationMarkerComponent); diff --git a/frontend/src/components/chat/DropZone.module.css b/frontend/src/components/chat/DropZone.module.css new file mode 100644 index 0000000..71b016b --- /dev/null +++ b/frontend/src/components/chat/DropZone.module.css @@ -0,0 +1,45 @@ +.overlay { + position: absolute; + inset: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--colorNeutralBackgroundAlpha2); + backdrop-filter: blur(4px); + pointer-events: none; + animation: fadeIn 0.15s ease-out; +} + +.dropArea { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + padding: 3rem 4rem; + border: 2px dashed var(--colorBrandStroke1); + border-radius: 12px; + background-color: var(--colorNeutralBackground1); + box-shadow: var(--shadow16); +} + +.icon { + font-size: 48px; + color: var(--colorBrandForeground1); +} + +.label { + font-size: var(--fontSizeBase400); + font-weight: var(--fontWeightSemibold); + color: var(--colorNeutralForeground1); +} + +.hint { + font-size: var(--fontSizeBase200); + color: var(--colorNeutralForeground3); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} diff --git a/frontend/src/components/chat/DropZone.tsx b/frontend/src/components/chat/DropZone.tsx new file mode 100644 index 0000000..40738a9 --- /dev/null +++ b/frontend/src/components/chat/DropZone.tsx @@ -0,0 +1,20 @@ +import { ArrowUploadRegular } from '@fluentui/react-icons'; +import styles from './DropZone.module.css'; + +interface DropZoneProps { + visible: boolean; +} + +export const DropZone: React.FC = ({ visible }) => { + if (!visible) return null; + + return ( +
+
+
+
+ ); +}; diff --git a/frontend/src/components/chat/FilePreview.tsx b/frontend/src/components/chat/FilePreview.tsx index a810952..2b1d136 100644 --- a/frontend/src/components/chat/FilePreview.tsx +++ b/frontend/src/components/chat/FilePreview.tsx @@ -1,6 +1,25 @@ -import { makeStyles, tokens, Button, Badge } from '@fluentui/react-components'; -import { Dismiss24Regular, ImageRegular } from '@fluentui/react-icons'; -import { useState, useEffect } from 'react'; +import { makeStyles, tokens, Button, Badge, Text } from '@fluentui/react-components'; +import { + Dismiss24Regular, + ImageRegular, + DocumentPdfRegular, + DocumentRegular, + DocumentTextRegular, + CodeRegular +} from '@fluentui/react-icons'; +import { useState, useEffect, useRef } from 'react'; +import { getEffectiveMimeType } from '../../utils/fileAttachments'; + +// MIME types that can be previewed as text content +const TEXT_PREVIEW_TYPES = new Set([ + 'text/plain', + 'text/markdown', + 'text/csv', + 'application/json', + 'text/html', + 'application/xml', + 'text/xml', +]); const useStyles = makeStyles({ container: { @@ -26,13 +45,37 @@ const useStyles = makeStyles({ height: '100%', objectFit: 'cover', }, + textPreview: { + width: '100%', + height: '100%', + padding: '4px', + fontSize: '7px', + fontFamily: 'monospace', + lineHeight: '1.2', + whiteSpace: 'pre-wrap', + wordBreak: 'break-all', + overflow: 'hidden', + color: tokens.colorNeutralForeground2, + backgroundColor: tokens.colorNeutralBackground1, + }, placeholderIcon: { width: '100%', height: '100%', display: 'flex', + flexDirection: 'column', alignItems: 'center', justifyContent: 'center', + gap: tokens.spacingVerticalXXS, color: tokens.colorNeutralForeground3, + padding: tokens.spacingHorizontalXS, + }, + fileName: { + fontSize: '9px', + textAlign: 'center', + wordBreak: 'break-word', + lineHeight: '1.1', + maxHeight: '22px', + overflow: 'hidden', }, removeButton: { position: 'absolute', @@ -65,74 +108,186 @@ interface FilePreviewProps { export const FilePreview: React.FC = ({ files, onRemove, disabled }) => { const styles = useStyles(); - const [thumbnails, setThumbnails] = useState>(new Map()); + // Key thumbnails by unique file identifier to prevent stale mappings on reorder + const [thumbnails, setThumbnails] = useState>(new Map()); + // Store text content previews for text-based files + const [textPreviews, setTextPreviews] = useState>(new Map()); + // Track files that are currently being read to prevent duplicate reads + const pendingReadsRef = useRef>(new Set()); + + // Generate a stable unique key for each file + const getFileKey = (file: File): string => `${file.name}-${file.size}-${file.lastModified}`; + + // Check if a file can be previewed as text using the shared MIME type utility + const isTextPreviewable = (file: File): boolean => { + const mimeType = getEffectiveMimeType(file); + return TEXT_PREVIEW_TYPES.has(mimeType); + }; useEffect(() => { - files.forEach((file, index) => { - if (file.type.startsWith('image/')) { - const reader = new FileReader(); - reader.onload = (e) => { - if (e.target?.result) { - setThumbnails(prev => new Map(prev).set(index, e.target!.result as string)); - } - }; - reader.readAsDataURL(file); + const currentFileKeys = new Set(files.map(getFileKey)); + + // Clean up previews for removed files + setThumbnails(prev => { + const updated = new Map(); + for (const [key, value] of prev) { + if (currentFileKeys.has(key)) { + updated.set(key, value); + } } + return updated; }); - - // Cleanup: revoke old object URLs that are no longer needed - return () => { - thumbnails.forEach(url => { - if (url.startsWith('blob:')) { - URL.revokeObjectURL(url); + + setTextPreviews(prev => { + const updated = new Map(); + for (const [key, value] of prev) { + if (currentFileKeys.has(key)) { + updated.set(key, value); } - }); - }; + } + return updated; + }); + + // Clear pending reads for removed files + for (const key of pendingReadsRef.current) { + if (!currentFileKeys.has(key)) { + pendingReadsRef.current.delete(key); + } + } + + // Generate previews for new files + for (const file of files) { + const fileKey = getFileKey(file); + const mimeType = getEffectiveMimeType(file); + + // Skip if already loaded or currently loading + if (pendingReadsRef.current.has(fileKey)) continue; + + if (mimeType.startsWith('image/')) { + // Check if already have this thumbnail + setThumbnails(prev => { + if (prev.has(fileKey)) return prev; + + pendingReadsRef.current.add(fileKey); + const reader = new FileReader(); + reader.onload = (e) => { + pendingReadsRef.current.delete(fileKey); + if (e.target?.result) { + setThumbnails(p => new Map(p).set(fileKey, e.target!.result as string)); + } + }; + reader.onerror = () => { + pendingReadsRef.current.delete(fileKey); + }; + reader.readAsDataURL(file); + return prev; + }); + } else if (isTextPreviewable(file)) { + // Check if already have this text preview + setTextPreviews(prev => { + if (prev.has(fileKey)) return prev; + + pendingReadsRef.current.add(fileKey); + const reader = new FileReader(); + reader.onload = (e) => { + pendingReadsRef.current.delete(fileKey); + if (e.target?.result) { + const text = e.target.result as string; + // Limit preview to first 200 characters for display + setTextPreviews(p => new Map(p).set(fileKey, text.slice(0, 200))); + } + }; + reader.onerror = () => { + pendingReadsRef.current.delete(fileKey); + }; + reader.readAsText(file); + return prev; + }); + } + } }, [files]); const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; - const sizes = ['B', 'KB', 'MB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round(bytes / Math.pow(k, i) * 10) / 10 + sizes[i]; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); + return `${Math.round(bytes / Math.pow(k, i) * 10) / 10} ${sizes[i]}`; + }; + + const getFileIcon = (file: File) => { + const mimeType = getEffectiveMimeType(file); + + if (mimeType.startsWith('image/')) { + return