From a5ea38d35b7d6f686942eec303dd039853d6dd15 Mon Sep 17 00:00:00 2001 From: adilei Date: Wed, 21 Oct 2026 16:40:36 +0300 Subject: [PATCH 1/7] Revamp SharePoint sidepane with discovery and native Web Chat Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 940e4d9a-9134-45e2-888a-1d9c5ef96c58 --- ui/embed/README.md | 2 +- ui/embed/sharepoint-customizer/.gitignore | 5 + ui/embed/sharepoint-customizer/README.md | 364 +++++++++--------- .../sharepoint-customizer/config/serve.json | 24 +- ui/embed/sharepoint-customizer/gulpfile.js | 13 + ui/embed/sharepoint-customizer/package.json | 13 +- .../scripts/sideload.cjs | 58 +++ .../sharepoint/assets/ClientSideInstance.xml | 13 +- .../sharepoint/assets/elements.xml | 9 +- .../SidebarAgentApplicationCustomizer.tsx | 321 +++++---------- .../sidebarAgent/assets/copilot-studio.svg | 75 ++++ .../components/Chat/AttachmentValidation.tsx | 27 ++ .../components/Chat/Chat.module.scss | 43 +++ .../sidebarAgent/components/Chat/Chat.tsx | 284 +++++++++----- .../components/Chat/acquireToken.ts | 122 +++--- .../components/Chat/activityDetails.ts | 127 ++++++ .../components/Chat/attachments.ts | 49 +++ .../components/Chat/createConnection.ts | 145 +++++++ .../components/Chat/nativeActivities.ts | 116 ++++++ .../SidePanel/SidePanel.module.scss | 101 +---- .../components/SidePanel/SidePanel.tsx | 116 +++--- .../extensions/sidebarAgent/configuration.ts | 72 ++++ .../models/ISidebarAgentProperties.ts | 13 + .../tests/activityDetails.test.cjs | 119 ++++++ .../tests/attachments.test.cjs | 36 ++ .../sharepoint-customizer/tests/auth.test.cjs | 143 +++++++ .../tests/configuration.test.cjs | 63 +++ .../tests/connection.test.cjs | 271 +++++++++++++ .../tests/fixtures/sdkActivities.cjs | 36 ++ .../tests/lazyStart.test.cjs | 61 +++ .../tests/nativeActivities.test.cjs | 132 +++++++ .../tests/sdkActivityMapping.test.cjs | 75 ++++ ui/embed/sharepoint-customizer/tsconfig.json | 4 +- .../sharepoint-customizer/tsconfig.test.json | 20 + 34 files changed, 2315 insertions(+), 757 deletions(-) create mode 100644 ui/embed/sharepoint-customizer/scripts/sideload.cjs create mode 100644 ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/assets/copilot-studio.svg create mode 100644 ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/AttachmentValidation.tsx create mode 100644 ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.module.scss create mode 100644 ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/activityDetails.ts create mode 100644 ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/attachments.ts create mode 100644 ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/createConnection.ts create mode 100644 ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/nativeActivities.ts create mode 100644 ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/configuration.ts create mode 100644 ui/embed/sharepoint-customizer/tests/activityDetails.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/attachments.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/auth.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/configuration.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/connection.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/fixtures/sdkActivities.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/lazyStart.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/nativeActivities.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/sdkActivityMapping.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tsconfig.test.json diff --git a/ui/embed/README.md b/ui/embed/README.md index 3a336443..84ead4f8 100644 --- a/ui/embed/README.md +++ b/ui/embed/README.md @@ -18,7 +18,7 @@ Plug a Copilot Studio chat agent into an existing platform. | [minimizable-widget/](./minimizable-widget/) | Minimizable/expandable WebChat widget for any website | | [pcf-canvas-app/](./pcf-canvas-app/) | PCF control to embed a chat agent in Power Apps canvas apps | | [servicenow-widget/](./servicenow-widget/) | Floating chat widget for ServiceNow Service Portal | -| [sharepoint-customizer/](./sharepoint-customizer/) | SharePoint app customizer with SSO | +| [sharepoint-customizer/](./sharepoint-customizer/) | Fluent SharePoint side panel with SSO, endpoint discovery, local files, and optional agent activity | | [typeahead-suggestions/](./typeahead-suggestions/) | Typeahead suggestions for WebChat | | [WebChat React](../custom-ui/webchat-react/) | WebChat React client with auth (Node) — *M365 Agents SDK repo* | | [Web Client](../custom-ui/webclient/) | Web client with auth (Node) — *M365 Agents SDK repo* | diff --git a/ui/embed/sharepoint-customizer/.gitignore b/ui/embed/sharepoint-customizer/.gitignore index 1eac77fa..7868cf47 100644 --- a/ui/embed/sharepoint-customizer/.gitignore +++ b/ui/embed/sharepoint-customizer/.gitignore @@ -12,6 +12,7 @@ lib release solution/* !solution/*.sppkg +sharepoint/solution/ temp .heft @@ -32,3 +33,7 @@ obj # Styles Generated Code *.scss.ts + +# Local review configuration and browser artifacts +*.local.json +.playwright-cli/ diff --git a/ui/embed/sharepoint-customizer/README.md b/ui/embed/sharepoint-customizer/README.md index ef3c2346..d6e5303b 100644 --- a/ui/embed/sharepoint-customizer/README.md +++ b/ui/embed/sharepoint-customizer/README.md @@ -4,250 +4,252 @@ parent: Embed grand_parent: UI nav_order: 6 --- -# Copilot Studio Agent Side Panel for SharePoint -![Copilot Studio Agent in SharePoint](./images/agent%20sidepanel.png) +# Copilot Studio side panel for SharePoint -## Summary +Embed a Microsoft-authenticated Copilot Studio agent in a SharePoint Framework (SPFx) Application Customizer. The panel uses the Microsoft 365 Agents SDK to discover the agent's conversation endpoints and WebChat's Fluent composer for messages, local files, cards, and suggested replies. -This sample demonstrates how to integrate a Microsoft Copilot Studio agent into SharePoint using a SharePoint Framework (SPFx) Application Customizer. It leverages the ReactWebChat component with the [Fluent UI theme pack](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack) and the [Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js) to establish a secure connection with a Copilot Studio agent configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft). +{: .important } +> This is an open-source reference sample, not a production-supported SharePoint channel. For production deployments, consider the [built-in SharePoint channel](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel). Its capabilities differ from this custom SDK integration; this sample does not enable file uploads in the built-in channel. -The key advantage of using the Microsoft 365 Agents SDK is its ability to connect to agents that support Microsoft authentication, which enables [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) capabilities. +{: .warning } +> Endpoint discovery currently requires the pinned public SDK preview, `1.10.0-beta.7.g65f69dfa27`. Stable SDK `1.9.1` does not contain discovery. WebChat and its Fluent theme are pinned together at `4.19.2`. Review preview dependency suitability before deploying. -{: .important } -> ⚠️ **This is an open-source reference sample only.** -> For production deployments, we strongly recommend using the [built-in SharePoint channel in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel). +## Features -## Features of this sample +- **Discovery-first connection:** configure an environment ID and agent schema name; the SDK resolves the correct published conversation endpoints. +- **Microsoft sign-in:** try silent authentication with the current SharePoint user's account, then offer a **Sign in** button when interaction is required or the silent iframe times out. +- **Clean Fluent panel:** responsive sizing, site theme integration, accessible controls, Escape/light-dismiss, and focus restoration. +- **Choose the opening experience:** default to native Fluent starter cards without starting a conversation, or opt into the agent's conversation-start flow instead. +- **Local attachments:** WebChat's native picker, drag-and-drop, clipboard files/images, multiple attachments, previews, and removal. +- **Optional agent activity:** independently enable client-visible reasoning and tool activity. Both are hidden by default. +- **Existing WebChat features:** Markdown, cards, suggested replies, typing, and streamed responses remain available. +- **Launcher:** an icon-only Copilot Studio button opens the agent from SharePoint's supported Top placeholder, below the Microsoft 365 suite header. The sample does not inject elements into the suite header or override SharePoint's built-in Copilot button. +- **Conversation lifecycle:** the single collapse chevron hides the panel while preserving the conversation; **New conversation** (the circular refresh arrow) clears its messages, pending files, and activity details, then follows the configured opening experience. -- **Single Sign-On (SSO)**: Leverages the current user's SharePoint authentication for seamless agent access -- **Sliding Panel UI**: Modern, responsive chat interface that slides in from the right side of the page -- **Customizable Appearance**: Configure header color and agent title to match your branding -- **New Conversation Support**: Users can start fresh conversations without leaving the page -- **Fluent UI Styling**: Uses the Fluent UI theme pack for a native Microsoft 365 look and feel -- **Tenant Graph Grounding**: Enables the agent to access and search Microsoft 365 content through [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) +There is no SharePoint/OneDrive picker, voice integration, or cross-page conversation persistence in this sample. ## Prerequisites -Before you begin, ensure you have the following: +- A published Copilot Studio agent accessible to the signed-in user, configured to [authenticate with Microsoft](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft). +- Access to create or use an Entra single-page application registration, and consent for delegated `CopilotStudio.Copilots.Invoke`. +- A modern SharePoint site for local sideloading. An app catalog and deployment permissions are only needed for packaged installation. +- Node.js **22.14 or later within v22**, npm, and the SPFx 1.21.1 toolchain. +- For file analysis, an agent/runtime that accepts uploaded files. For standard-harness agents, enable **Settings > Generative AI > File processing capabilities > File uploads**. +- For activity details, a model/runtime that emits the supported client-visible activities described below. Enabling a UI flag does not enable server-side telemetry. -1. **Published Copilot Studio Agent** - - Agent must be configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) authentication - - Agent should be published and accessible - - Consider enabling [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) for access to Microsoft 365 content +## Architecture -2. **Azure Portal Access** - - Required for creating and configuring app registrations - - Permissions to create new app registrations in your Azure AD tenant +The Application Customizer hosts a Fluent panel. It creates the chat only after the user opens it. MSAL acquires a Power Platform API token. By default, opening the panel does not start an agent conversation: the SDK discovers endpoints on the first submitted message through: -3. **SharePoint Administrator Access** - - Required to deploy the SPFx extension to the app catalog +```text +GET https:///copilotstudio/agents//conversation-endpoints +``` -4. **Development Environment** - - Node.js v22 (as per SPFx 1.21.1 requirements) - - npm or yarn package manager - - Git for cloning the repository +On the first submitted message, the adapter uses `startConversationStreaming(false)` to establish a conversation without firing the welcome event, then sends the message to its execute-turn endpoint. The create-conversation endpoint must not be treated as an execute-turn endpoint: it can return only a session ID and typing activity without processing a supplied message. Subsequent messages reuse the conversation. -## Technical Architecture +Setting `startConversation: true` instead performs discovery and starts the conversation on opening, without showing the sample's starter cards. The sample does not construct agentic-runtime routes or start a separate subscription connection. The SDK retains conversation IDs, including IDs returned only in response headers. -This solution uses: -- **[Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js)**: Provides secure connection and authentication handling for Copilot Studio agents -- **[BotFramework-WebChat with Fluent UI Theme](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack)**: Renders the chat interface with Microsoft 365 styling -- **SharePoint Framework**: Enables deployment as a site-wide extension -- **[Tenant Graph Grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding)**: Allows the agent to search and access Microsoft 365 content +A small adapter around `CopilotStudioWebChat.createConnection` validates local attachments before sending and translates client-visible `thought` and `toolCall` entities, plus supported legacy tool events, into WebChat's native Schema.org `Message`/`HowTo` activities. WebChat's Fluent pre-chat and collapsible part-group components render the welcome screen, starter cards, and activity groups; there are no custom reasoning/tool React components. A shared incoming subscription replays activities so WebChat's startup and transcript subscriptions do not lose greetings. Raw tool metadata, arguments, results, and embedded planner thoughts are never replayed into WebChat. Send acknowledgements do not end response monitoring: later response errors are still surfaced, and the composer stays disabled during file preparation and the response to prevent overlapping turns. -## Setup Instructions +The Fluent **copilot** variant and SharePoint theme colors provide the presentation. WebChat's style options are memoized across turn-state updates so response completion does not reset transcript scrolling; theme or upload-setting changes still refresh them. The sample retains released WebChat `4.19.2` and React `17`, compatible with this SPFx version. -### Step 1: Configure App Registration +The unmodified Copilot Studio SVG is from [Microsoft's official icon collection](https://learn.microsoft.com/en-us/power-platform/guidance/icons) and is included for this sample/training experience. Microsoft's separate icon usage terms apply; review them before reusing the logo in a production application. -This step requires permissions to create application identities in your Azure tenant. For this sample, create a Native Client Application Identity (no secrets required): +`gulpfile.js` maps the SDK beta's Activity browser crypto shim to its **shipped JavaScript** file, because that beta's package mapping otherwise points SPFx at uncompiled TypeScript. No Node crypto polyfill or vendored SDK is required. -1. **Open Azure Portal** - - Navigate to [portal.azure.com](https://portal.azure.com) - - Go to **Azure Active Directory** (Entra ID) +## Setup -2. **Create a new App Registration** - - Click **App registrations** → **New registration** - - Provide a **Name** (e.g., "SharePoint Copilot Studio Agent") - - Choose **"Accounts in this organization directory only"** - - Under **Redirect URI**: - - Select **"Single-page application"** from the platform dropdown - - Enter your first SharePoint site URL without the trailing slash (e.g., `https://contoso.sharepoint.com/sites/mysite`) - - Click **Register** +### 1. Configure Microsoft authentication -3. **Configure Authentication - Add SharePoint URLs** - - Go to **Authentication** in the Manage section - - Under **Single-page application**, add redirect URIs: - - `https://localhost:4321` (only if you plan to test locally - this is the default SPFx port) - - For each SharePoint site where you'll deploy the extension, add: - - The site URL **without** trailing slash (e.g., `https://contoso.sharepoint.com/sites/mysite`) - - Under **Implicit grant and hybrid flows**, ensure both checkboxes are **unchecked** (SPAs use PKCE flow) - - Click **Save** - - **Example for multiple sites:** - ``` - https://localhost:4321 (for local testing - default SPFx port) - https://contoso.sharepoint.com/sites/hr - https://contoso.sharepoint.com/sites/finance - https://contoso.sharepoint.com/sites/it - ``` - -4. **Configure API Permissions** - - In your new application, go to **API Permissions** in the Manage section - - Click **Add Permission** - - In the side panel, click the **APIs my organization uses** tab - - Search for **"Power Platform API"** or use the GUID `8578e004-a5c6-46e7-913e-12f58912df43` - - Under **Delegated permissions**, expand **CopilotStudio** and check: - - `CopilotStudio.Copilots.Invoke` - - Click **Add Permissions** - - (Optional) Click **Grant admin consent** for your organization - -5. **Note Required Values** - - On the **Overview** page, copy and save: - - **Application (client) ID** (e.g., `12345678-1234-1234-1234-123456789012`) - - **Directory (tenant) ID** (e.g., `87654321-4321-4321-4321-210987654321`) +1. In [Entra app registrations](https://portal.azure.com/), create or select an application for this sample. For a new registration, choose accounts in your organization. +2. Add a **Single-page application** redirect URI matching the SharePoint **site URL without a trailing slash**, for example `https://contoso.sharepoint.com/sites/my-site`. +3. Add a redirect URI for each site where the extension will run. Do not enable implicit grant; MSAL uses authorization code with PKCE. +4. Under **API permissions > Add a permission > APIs my organization uses**, find **Power Platform API** (`8578e004-a5c6-46e7-913e-12f58912df43`). +5. Add delegated **CopilotStudio.Copilots.Invoke** and obtain consent as required by your tenant. +6. Record the application/client ID and directory/tenant ID. **No client secret belongs in this application.** {: .note } -> If you don't see "Power Platform API" in the list, you need to add it to your tenant first. See [Power Platform API Authentication](https://learn.microsoft.com/en-us/power-platform/admin/programmability-authentication) and follow Step 2 to add the API. +> The redirect URI is the SharePoint site even during local sideloading. Serving scripts from `https://localhost:4321` does not make localhost the sign-in redirect. MSAL caches authentication in session storage. If silent sign-in cannot complete, select **Sign in** to open a popup and complete any required MFA. The popup is opened from your click, without repeating the failed silent iframe request. -### Step 2: Get Copilot Studio Configuration +If Power Platform API is missing from your tenant, see [Power Platform API authentication](https://learn.microsoft.com/en-us/power-platform/admin/programmability-authentication). -1. Open your Copilot Studio environment -2. Navigate to your published agent -3. Ensure the agent is configured with ['Authenticate with Microsoft'](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) -4. Optionally enable [tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) for Microsoft 365 content access -5. Collect the required configuration values using one of these methods: +### 2. Collect the agent settings - **Option A: Get Direct Connect URL (Recommended)** - - Go to **Channels** → **Web app** - - Under **Microsoft 365 Agents SDK**, copy the **Connection string** - - This will look like: `https://xxxxxxx.07.environment.api.powerplatform.com/...` +In Copilot Studio, open the published agent's metadata and record its **environment ID** and **schema name**. Use the schema name, not the display name or agent GUID. - **Option B: Get Environment ID and Agent Identifier** - - Go to **Settings** → **Advanced** → **Metadata** - - Copy the following values: - - **Environment ID** - - **Schema name** (this is your Agent Identifier, e.g., `cr770_myAgent`) +The recommended configuration omits `directConnectUrl`. Supplying one explicitly bypasses SDK discovery, even when environment/schema values are also present. This override remains available for existing deployments. -### Step 3: Clone the Repository +### 3. Install dependencies ```bash git clone https://github.com/microsoft/CopilotStudioSamples.git cd CopilotStudioSamples/ui/embed/sharepoint-customizer +npm install ``` -### Step 4: Configure the Extension - -1. Open `sharepoint/assets/elements.xml` in your editor -2. Update the `ClientSideComponentProperties` with your values: - -```xml -ClientSideComponentProperties="{ - "appClientId": "YOUR_APP_CLIENT_ID", - "tenantId": "YOUR_TENANT_ID", - "environmentId": "YOUR_ENVIRONMENT_ID", - "agentIdentifier": "YOUR_AGENT_IDENTIFIER", - "directConnectUrl": "YOUR_DIRECT_CONNECT_URL", - "showTyping": true, - "headerBackgroundColor": "white", - "agentTitle": "Copilot Studio Agent" -}" +### 4. Create a local review configuration + +Create `properties.local.json` in the sample directory. Files named `*.local.json` are gitignored. + +```json +{ + "appClientId": "YOUR_APP_CLIENT_ID", + "tenantId": "YOUR_TENANT_ID", + "environmentId": "YOUR_ENVIRONMENT_ID", + "schemaName": "YOUR_AGENT_SCHEMA_NAME", + "agentTitle": "Team assistant", + "startConversation": false, + "showTyping": true, + "enableFileUpload": true, + "showReasoning": false, + "showToolCalls": false, + "starterPrompts": [ + { "title": "Get started", "text": "What can you help me with?" } + ] +} ``` -**Configuration Options:** +| Property | Required | Description / default | +|----------|----------|-----------------------| +| `appClientId` | Yes | Entra SPA application ID | +| `tenantId` | Yes | Directory/tenant ID | +| `environmentId` | For discovery | Power Platform environment ID | +| `schemaName` | For discovery | Agent schema name | +| `agentIdentifier` | No | Legacy alias for `schemaName`; canonical `schemaName` takes precedence | +| `directConnectUrl` | No | Explicit HTTPS connection URL; bypasses discovery | +| `agentTitle` | No | `Copilot Studio Agent` | +| `headerBackgroundColor` | No | Optional launcher-bar CSS color override; otherwise follows the site theme | +| `startConversation` | No | `false`: show the local welcome/starter cards and wait for the first submitted message. `true`: start the agent's conversation immediately and hide the local welcome/starter cards | +| `showTyping` | No | `true`; ordinary typing is independent of reasoning/tool visibility | +| `enableFileUpload` | No | `true` | +| `showReasoning` | No | `false` | +| `showToolCalls` | No | `false` | +| `starterPrompts` | No | Array of `{ "title": "...", "text": "..." }`, used only when `startConversation` is `false`; defaults to the **Get started** card above. Use `[]` for a welcome screen without starter cards | -| Property | Required | Description | Default | -|----------|----------|-------------|---------| -| `appClientId` | Yes | Your Azure AD app registration client ID | - | -| `tenantId` | Yes | Your Azure AD tenant ID | - | -| `environmentId` | Conditional | Copilot Studio environment ID (required if not using directConnectUrl) | - | -| `agentIdentifier` | Conditional | Your agent's identifier (required if not using directConnectUrl) | - | -| `directConnectUrl` | Conditional | Direct connection URL (use this OR environmentId + agentIdentifier) | - | -| `showTyping` | No | Show typing indicators | `true` | -| `headerBackgroundColor` | No | Header bar color (accepts any CSS color value) | `white` | -| `agentTitle` | No | Display title for the agent | `"Copilot Studio Agent"` | +Use JSON booleans, not strings such as `"false"`. Keep tokens, passwords, client secrets, and browser state out of the configuration. -{: .note } -> You must provide either `directConnectUrl` OR both `environmentId` and `agentIdentifier`. +Starter prompts are public extension configuration, not system instructions. They are not automatically retrieved from the agent's authoring settings. Native cards put the configured text into the composer and focus it; they do not send a message until the user submits. The two opening modes are mutually exclusive: with `startConversation: true`, the agent's own welcome flow runs instead, and its messages and suggested replies render normally. -### Step 5: Test Locally (Optional) +## Sideload on a real SharePoint page -Before building and deploying, you can test the extension locally: +Application Customizers must be tested on a **live modern SharePoint page**, not the local workbench. Debug sideloading does not require an app-catalog installation and does not create a persistent site customization. -```bash -# Install dependencies -npm install +1. Trust the SPFx development certificate if it is not already trusted: -# Check if gulp is installed, if not, install it -which gulp || npm install -g gulp-cli -# On Windows, use: where gulp || npm install -g gulp-cli + ```bash + npx gulp trust-dev-cert + ``` -# Serve the solution locally -gulp serve --nobrowser -``` +2. Start the local HTTPS server and leave it running: -After running `gulp serve`, navigate to the following URL (replace the values with your configuration): + ```bash + npm run serve + ``` -{% raw %} -``` -https://YOUR-TENANT.sharepoint.com/sites/YOUR-SITE/SitePages/Home.aspx?debugManifestsFile=https://localhost:4321/temp/build/manifests.js&loadSPFX=true&customActions={%224c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2%22:{%22location%22:%22ClientSideExtension.ApplicationCustomizer%22,%22properties%22:{%22appClientId%22:%22YOUR_APP_CLIENT_ID%22,%22tenantId%22:%22YOUR_TENANT_ID%22,%22directConnectUrl%22:%22YOUR_DIRECT_CONNECT_URL%22,%22showTyping%22:true,%22headerBackgroundColor%22:%22white%22,%22agentTitle%22:%22Your%20Agent%20Title%22}}} -``` -{% endraw %} +3. Confirm `https://localhost:4321/temp/build/manifests.js` loads in the review browser. Resolve certificate/local-network permission prompts normally; do not disable browser security. +4. Generate a debug URL using an actual modern page URL from your site: -- Replace `YOUR-TENANT`, `YOUR-SITE`, `YOUR_APP_CLIENT_ID`, `YOUR_TENANT_ID`, and `YOUR_DIRECT_CONNECT_URL` with your actual values -- Ensure you've added `https://localhost:4321` to your app registration's redirect URIs if testing locally + ```bash + npm run sideload -- \ + "https://contoso.sharepoint.com/sites/my-site/" \ + properties.local.json + ``` -### Step 6: Build and Package +5. Open the generated URL in your signed-in browser and select **Load debug scripts**. Open the agent from the launcher. -Once you've tested locally (or if you're ready to deploy directly): +The helper preserves existing query parameters and encodes `customActions` safely. It uses this extension's manifest ID and omits any settings you did not supply. To use a different local port, configure the server and pass its manifest URL as the third argument. + +To end the review, leave the debug URL or close that tab and stop the development server. No app uninstall or persistent custom-action removal is necessary. + +### Review with Playwright + +Use a named, headed browser session and the generated debug URL: ```bash -# Build the solution -gulp build +playwright-cli -s=sharepoint-review open "" --browser msedge --headed --persistent +playwright-cli -s=sharepoint-review snapshot +``` -# Bundle the solution -gulp bundle --ship +Complete sign-in/MFA yourself when required. To use an already signed-in Edge browser that supports remote debugging, enable **Allow remote debugging for this browser instance** at `edge://inspect/#remote-debugging`, then attach: -# Package the solution -gulp package-solution --ship +```bash +playwright-cli -s=sharepoint-review attach --cdp=msedge +playwright-cli -s=sharepoint-review tab-new "" ``` -This will create a `.sppkg` file in the `sharepoint/solution` folder. +Keep actions confined to the review tab. On completion, use `detach` for an attached browser; do not close someone else's browser. -### Step 7: Deploy to SharePoint +Review the default configuration first, then generate URLs with `showReasoning` and `showToolCalls` enabled independently. Check: -Upload the `.sppkg` file from `sharepoint/solution` to your SharePoint App Catalog. For detailed instructions on creating an app catalog, deploying apps, and adding them to sites, see [Add custom apps to SharePoint](https://learn.microsoft.com/en-us/sharepoint/use-app-catalog#add-custom-apps). +- Discovery and a follow-up prompt that retains context. +- With the default `startConversation: false`, no conversation request until the first submission, editable starter text, and a follow-up that keeps the conversation ID. +- With `startConversation: true`, an agent-supplied welcome message and no local starter cards. +- A small, synthetic local document whose contents the agent can identify. +- Picker, paste/drop, attachment removal, rejection of oversized/unsupported files, and attachment-only messages. +- Supported reasoning/tool events, collapsed details, and no detail text when flags are disabled. +- Long streamed replies: follow the latest text through completion, but preserve the reading position when the user deliberately scrolls up. +- Close/reopen, **New conversation**, keyboard focus/Escape, narrow viewports, and error/reconnect behavior. -## Troubleshooting +An upload appearing in the transcript is **not** proof that the agent processed it. Likewise, a working Copilot Studio test pane does not prove the sideloaded SDK connection works. Keep screenshots and diagnostic metadata free of tokens, user identities, and private SharePoint content. + +## File input behavior + +The sample accepts DOCX, CSV, PDF, TXT, JPG/JPEG, PNG, WebP, and GIF filenames, with a **15 MiB (15 × 1024 × 1024 bytes)** individual-file cap. Empty files are rejected. The checks apply to picker, paste/drop, and outgoing attachments; invalid selections are removed with an explanation before sending. -### "Unable to acquire token" error +Files are sent directly in the authenticated conversation as data-URL attachments with their name and MIME type. They are not uploaded into a SharePoint library or a new storage service. Failure to read a local blob aborts the send instead of transmitting an unusable `blob:` URL. -If you encounter this error: -1. Open your browser's Developer Tools (F12) -2. Go to the **Network** tab -3. Look for failed authentication requests (usually to `login.microsoftonline.com`) -4. Click on the failed requests to see detailed error messages -5. Follow the authentication URLs that MSAL is attempting - the actual error details are often revealed in the response or redirect parameters +{: .note } +> This is a conservative client policy, not a guarantee of service acceptance. Standard-harness documentation describes a 15 MB file limit, nonanimated GIFs only, PDF page/processed-size restrictions, and text-character limits. The service must still validate actual content, animation, protection, and model limits. Agentic-runtime support can differ. See [Allow file input from users](https://learn.microsoft.com/en-us/microsoft-copilot-studio/image-input-analysis). + +Do not upload sensitive files merely to test the sample. Earlier uploaded content may continue to count against the agent's conversation limits; start a new conversation when appropriate. + +## Reasoning and tool visibility + +**Agent activity** uses WebChat's native collapsible part groups, closed by default. Steps update in place while the response runs and remain in the transcript after it finishes. Each turn has its own group; WebChat owns rendering, focus, status icons, and expansion. -Common causes: -- Missing or incorrect redirect URIs in your app registration (check for trailing slashes!) -- Incorrect permissions in the app registration (ensure `CopilotStudio.Copilots.Invoke` is granted) -- Incorrect tenant ID or client ID -- MSAL authentication flow being blocked by browser settings +- `showReasoning` displays client-visible `entities[].type: "thought"` content from the SDK. Text comes from the entity's `text`, not the activity's answer text. Explicit `chunkType: "delta"` chunks append; replacement/cumulative snapshots replace. Thought chain/step identifiers keep separate thoughts apart, with the stream ID (or the initial activity ID) used when the chain ID is empty. Ordinary answer streaming is untouched. +- Native Schema.org messages tagged `AnalysisMessage` also respect `showReasoning`. The adapter uses WebChat's [documented reasoning activity format](https://github.com/microsoft/BotFramework-WebChat/blob/v4.19.2/docs/CHAIN_OF_THOUGHTS.md); it does not request hidden model chain-of-thought. +- `showToolCalls` displays `entities[].type: "toolCall"` as a named tool step. `toolCallId` correlates invocations, `toolDisplayName` provides the label, and `status` supplies started/completed/failed state. Multiple calls sharing one stream remain separate. +- Legacy `DynamicPlanReceived`, `DynamicPlanStepTriggered`, and `DynamicPlanStepFinished` event `valueType`s remain supported. Definitions provide display names; step identifiers correlate lifecycle updates. +- `streamType: "informative"` alone is **not** evidence of reasoning. Tool notifications can use it too. Unclassified informative updates are omitted rather than guessed into a reasoning/tool entry. +- Tool arguments, filled/hidden parameters, descriptions, results, and embedded planner `thought` fields are not displayed. Only explicitly client-visible `thought` entities are used for reasoning; the sample does not retrieve authoring-only traces. +- Unknown events are not guessed into a tool call. A flag being on cannot make an agent emit missing events, and these event families are not guaranteed across all models/harnesses. +- **Started** means a start event was received; **Finished** means a finish event was received, not necessarily that the operation succeeded. +- The group completes when the turn finishes. Missing tool-finish events are explicitly labeled rather than invented; failed responses mark the group as interrupted. -### "The redirect URI specified in the request does not match the redirect URIs configured for the application" error +Choose safe, read-only agent actions for review. Do not trigger a business-changing tool just to demonstrate the disclosure. -Check that the current SharePoint URL has been added as an allowed redirect URI for the provided app registration. +## Build and package -## Additional Resources +```bash +npm test +npm run build +npx gulp bundle --ship +npx gulp package-solution --ship +``` -- [Built-in SharePoint Channel for Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel) -- [Authenticate with Microsoft in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/configuration-end-user-authentication#authenticate-with-microsoft) -- [Tenant Graph Grounding in Copilot Studio](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) -- [Microsoft 365 Agents SDK for NodeJS/TypeScript](https://github.com/microsoft/Agents-for-js) -- [BotFramework-WebChat with Fluent UI Theme](https://github.com/microsoft/BotFramework-WebChat#experimental-fluent-ui-theme-pack) -- [Copilot Studio Documentation](https://learn.microsoft.com/en-us/microsoft-copilot-studio/) -- [SharePoint Framework Documentation](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/sharepoint-framework-overview) +The package is generated at `sharepoint/solution/sidebar-agent.sppkg`. Before packaged deployment, replace placeholders in `sharepoint/assets/elements.xml` (site custom action) and `sharepoint/assets/ClientSideInstance.xml` (tenant-wide extension instance) for the deployment method you use. Avoid registering both paths on the same site and producing duplicate launchers. -## License +Upload the package to your app catalog only when ready for installation; local review does not require this step. See [Use the SharePoint app catalog](https://learn.microsoft.com/en-us/sharepoint/use-app-catalog). + +## Troubleshooting -This project is licensed under the MIT License - see the LICENSE file for details. +| Symptom | Check | +|---------|-------| +| Debug scripts do not load | Local server, trusted HTTPS certificate, actual manifest URL, debug-script/local-network prompts, and tenant restrictions | +| No discovery request | Remove the explicit `directConnectUrl`; check environment ID and schema name | +| Discovery fails | Agent publication/sharing, tenant, delegated invocation permission, service availability; the sample does not silently switch transports | +| Sign-in/redirect failure | Register the exact SharePoint site redirect without a trailing slash, verify consent, and permit the interactive sign-in popup | +| `AADSTS50011` | Add the exact **Redirect URI** shown in the panel to the indicated **Application ID** under **Authentication > Single-page application** in Entra. An unregistered URI can also leave an error inside the silent iframe and appear only as an MSAL timeout | +| `monitor_window_timeout` during silent sign-in | Select **Sign in** to view Microsoft's interactive sign-in page and any detailed error. A timeout alone does not identify the cause. The panel preserves the full error text returned by MSAL and shows the client ID and redirect URI, but cannot read a cross-origin Microsoft error page that never redirects back. Do not relax iframe sandbox restrictions; see [MSAL timeout guidance](https://learn.microsoft.com/en-us/entra/msal/javascript/browser/errors#monitor_window_timeout) | +| Passkey crashes in an automated browser | Use another tenant-approved sign-in method, or attach to a normally signed-in Edge instance; do not bypass MFA | +| File appears but is not analyzed | Verify agent file-input settings, supported content/model/channel, and service-side limits | +| No reasoning/tool details | Enable the correct flag and verify that the published runtime actually sends a supported event family | +| Response fails after sending | A request may already have reached the agent. **Reconnect** starts a new conversation; avoid blindly repeating operations with side effects | + +## Resources + +- [Microsoft 365 Agents SDK for JavaScript](https://github.com/microsoft/Agents-for-js) +- [Bot Framework WebChat](https://github.com/microsoft/BotFramework-WebChat) +- [SPFx Application Customizer debugging](https://learn.microsoft.com/en-us/sharepoint/dev/spfx/extensions/get-started/build-a-hello-world-extension#debug-your-application-customizer) +- [Tenant graph grounding](https://learn.microsoft.com/en-us/microsoft-copilot-studio/knowledge-copilot-studio#tenant-graph-grounding) diff --git a/ui/embed/sharepoint-customizer/config/serve.json b/ui/embed/sharepoint-customizer/config/serve.json index 0337190d..e73547aa 100644 --- a/ui/embed/sharepoint-customizer/config/serve.json +++ b/ui/embed/sharepoint-customizer/config/serve.json @@ -4,7 +4,7 @@ "https": true, "serveConfigurations": { "default": { - "pageUrl": "https://m365x66364744.sharepoint.com/sites/GlobalSales/SitePages/Home.aspx", + "pageUrl": "https://YOUR-TENANT.sharepoint.com/sites/YOUR-SITE/", "customActions": { "4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2": { "location": "ClientSideExtension.ApplicationCustomizer", @@ -12,15 +12,19 @@ "appClientId": "APP-REGISTRATION-ID", "tenantId": "TENANT-ID", "environmentId": "ENV-ID", - "agentIdentifier": "AGENT-ID", - "directConnectUrl": "YOUR-DIRECT-CONNECT-URL", - "showTyping": true + "schemaName": "AGENT-SCHEMA-NAME", + "startConversation": false, + "showTyping": true, + "enableFileUpload": true, + "showReasoning": false, + "showToolCalls": false, + "starterPrompts": [{ "title": "Get started", "text": "What can you help me with?" }] } } } }, "sidebarAgent": { - "pageUrl": "https://m365x66364744.sharepoint.com/sites/GlobalSales/SitePages/Home.aspx", + "pageUrl": "https://YOUR-TENANT.sharepoint.com/sites/YOUR-SITE/", "customActions": { "4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2": { "location": "ClientSideExtension.ApplicationCustomizer", @@ -28,9 +32,13 @@ "appClientId": "APP-REGISTRATION-ID", "tenantId": "TENANT-ID", "environmentId": "ENV-ID", - "agentIdentifier": "AGENT-ID", - "directConnectUrl": "YOUR-DIRECT-CONNECT-URL", - "showTyping": true + "schemaName": "AGENT-SCHEMA-NAME", + "startConversation": false, + "showTyping": true, + "enableFileUpload": true, + "showReasoning": false, + "showToolCalls": false, + "starterPrompts": [{ "title": "Get started", "text": "What can you help me with?" }] } } } diff --git a/ui/embed/sharepoint-customizer/gulpfile.js b/ui/embed/sharepoint-customizer/gulpfile.js index be291870..9b113a54 100644 --- a/ui/embed/sharepoint-customizer/gulpfile.js +++ b/ui/embed/sharepoint-customizer/gulpfile.js @@ -1,6 +1,19 @@ 'use strict'; const build = require('@microsoft/sp-build-web'); +const path = require('node:path'); + +// The SDK beta's Activity browser mapping points at TypeScript; use its shipped JavaScript shim. +const activityRoot = path.dirname(require.resolve('@microsoft/agents-activity/package.json')); +build.configureWebpack.mergeConfig({ + additionalConfiguration: config => { + config.resolve.alias = { + ...config.resolve.alias, + [path.join(activityRoot, 'src/browser/crypto.ts')]: path.join(activityRoot, 'dist/src/browser/crypto.js') + }; + return config; + } +}); build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); diff --git a/ui/embed/sharepoint-customizer/package.json b/ui/embed/sharepoint-customizer/package.json index 8fe1cf5c..2528a8de 100644 --- a/ui/embed/sharepoint-customizer/package.json +++ b/ui/embed/sharepoint-customizer/package.json @@ -9,19 +9,24 @@ "scripts": { "build": "gulp bundle", "clean": "gulp clean", - "test": "gulp test" + "test": "tsc -p tsconfig.test.json && node --test tests/*.test.cjs", + "serve": "gulp serve --nobrowser", + "sideload": "node scripts/sideload.cjs" }, "dependencies": { "@azure/msal-browser": "^4.13.1", "@fluentui/react": "^8.121.4", - "@microsoft/agents-copilotstudio-client": "^1.0.0", + "@microsoft/agents-activity": "1.10.0-beta.7.g65f69dfa27", + "@microsoft/agents-copilotstudio-client": "1.10.0-beta.7.g65f69dfa27", "@microsoft/decorators": "1.21.1", "@microsoft/sp-application-base": "1.21.1", "@microsoft/sp-core-library": "1.21.1", - "botframework-webchat": "4.18.1-main.20250804.93043a9", - "botframework-webchat-fluent-theme": "4.18.1-main.20250804.93043a9", + "@microsoft/sp-component-base": "1.21.1", + "botframework-webchat": "4.19.2", + "botframework-webchat-fluent-theme": "4.19.2", "react": "17.0.1", "react-dom": "17.0.1", + "rxjs": "7.8.2", "tslib": "2.3.1" }, "devDependencies": { diff --git a/ui/embed/sharepoint-customizer/scripts/sideload.cjs b/ui/embed/sharepoint-customizer/scripts/sideload.cjs new file mode 100644 index 00000000..f77c23d7 --- /dev/null +++ b/ui/embed/sharepoint-customizer/scripts/sideload.cjs @@ -0,0 +1,58 @@ +const { readFileSync } = require('node:fs'); + +const COMPONENT_ID = '4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2'; + +function createSideloadUrl(pageUrl, properties, manifestUrl = 'https://localhost:4321/temp/build/manifests.js') { + const page = new URL(pageUrl); + const manifest = new URL(manifestUrl); + if (page.protocol !== 'https:' || page.username || page.password) { + throw new Error('The SharePoint page must be an HTTPS URL without credentials.'); + } + if (manifest.protocol !== 'https:' || manifest.username || manifest.password) { + throw new Error('The development manifest must be an HTTPS URL without credentials.'); + } + if (!properties || typeof properties !== 'object' || Array.isArray(properties)) { + throw new Error('Properties must be a JSON object.'); + } + const allowed = [ + 'appClientId', 'tenantId', 'environmentId', 'schemaName', 'agentIdentifier', 'directConnectUrl', + 'startConversation', 'showTyping', 'enableFileUpload', 'showReasoning', 'showToolCalls', + 'agentTitle', 'headerBackgroundColor', 'starterPrompts' + ]; + for (const key of Object.keys(properties)) { + if (!allowed.includes(key)) throw new Error(`Unknown property "${key}". Never put credentials in a debug URL.`); + } + if (!properties.appClientId || !properties.tenantId || + !(properties.directConnectUrl || (properties.environmentId && (properties.schemaName || properties.agentIdentifier)))) { + throw new Error('Provide appClientId, tenantId, and environmentId/schemaName (or an explicit directConnectUrl).'); + } + for (const flag of ['startConversation', 'showTyping', 'enableFileUpload', 'showReasoning', 'showToolCalls']) { + if (properties[flag] !== undefined && typeof properties[flag] !== 'boolean') throw new Error(`${flag} must be a boolean.`); + } + if (properties.starterPrompts !== undefined) { + if (!Array.isArray(properties.starterPrompts) || properties.starterPrompts.some(prompt => + !prompt || typeof prompt !== 'object' || Array.isArray(prompt) || + Object.keys(prompt).some(key => key !== 'title' && key !== 'text') || + typeof prompt.title !== 'string' || !prompt.title.trim() || typeof prompt.text !== 'string' || !prompt.text.trim() + )) throw new Error('starterPrompts must be an array of objects containing only a non-empty title and text.'); + } + page.searchParams.set('loadSPFX', 'true'); + page.searchParams.set('debugManifestsFile', manifest.href); + page.searchParams.set('customActions', JSON.stringify({ + [COMPONENT_ID]: { location: 'ClientSideExtension.ApplicationCustomizer', properties } + })); + return page.href; +} + +if (require.main === module) { + try { + const [page, file, manifest] = process.argv.slice(2); + if (!page || !file) throw new Error('Usage: npm run sideload -- [HTTPS-manifest-URL]'); + console.log(createSideloadUrl(page, JSON.parse(readFileSync(file, 'utf8')), manifest)); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} + +module.exports = { createSideloadUrl, COMPONENT_ID }; diff --git a/ui/embed/sharepoint-customizer/sharepoint/assets/ClientSideInstance.xml b/ui/embed/sharepoint-customizer/sharepoint/assets/ClientSideInstance.xml index 04b50040..f1f82156 100644 --- a/ui/embed/sharepoint-customizer/sharepoint/assets/ClientSideInstance.xml +++ b/ui/embed/sharepoint-customizer/sharepoint/assets/ClientSideInstance.xml @@ -4,6 +4,17 @@ Title="SidebarAgent" Location="ClientSideExtension.ApplicationCustomizer" ComponentId="4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2" - Properties=""> + Properties="{ + "appClientId": "YOUR_APP_CLIENT_ID", + "tenantId": "YOUR_TENANT_ID", + "environmentId": "YOUR_ENVIRONMENT_ID", + "schemaName": "YOUR_AGENT_SCHEMA_NAME", + "startConversation": false, + "showTyping": true, + "enableFileUpload": true, + "showReasoning": false, + "showToolCalls": false, + "starterPrompts": [{ "title": "Get started", "text": "What can you help me with?" }] + }"> \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml b/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml index 70f90775..5c19c586 100644 --- a/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml +++ b/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml @@ -8,10 +8,13 @@ "appClientId": "YOUR_APP_CLIENT_ID", "tenantId": "YOUR_TENANT_ID", "environmentId": "YOUR_ENVIRONMENT_ID", - "agentIdentifier": "YOUR_AGENT_IDENTIFIER", - "directConnectUrl": "YOUR_DIRECT_CONNECT_URL", + "schemaName": "YOUR_AGENT_SCHEMA_NAME", + "startConversation": false, "showTyping": true, - "headerBackgroundColor": "white", + "enableFileUpload": true, + "showReasoning": false, + "showToolCalls": false, + "starterPrompts": [{ "title": "Get started", "text": "What can you help me with?" }], "agentTitle": "Copilot Studio Agent" }"> diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.tsx b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.tsx index 20a33873..fc79f793 100644 --- a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.tsx +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.tsx @@ -1,277 +1,128 @@ import { Log } from '@microsoft/sp-core-library'; -import { - BaseApplicationCustomizer, - PlaceholderContent, - PlaceholderName, - ApplicationCustomizerContext -} from '@microsoft/sp-application-base'; +import { BaseApplicationCustomizer, PlaceholderContent, PlaceholderName, ApplicationCustomizerContext } from '@microsoft/sp-application-base'; +import { IconButton, initializeIcons, MessageBar, MessageBarType, ThemeProvider as FluentThemeProvider } from '@fluentui/react'; +import { IReadonlyTheme, ThemeChangedEventArgs, ThemeProvider } from '@microsoft/sp-component-base'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as strings from 'SidebarAgentApplicationCustomizerStrings'; import SidePanel from './components/SidePanel/SidePanel'; -import { - ISidebarAgentApplicationCustomizerProperties, - ISidebarAgentState -} from './models/ISidebarAgentProperties'; +import styles from './components/SidePanel/SidePanel.module.scss'; +import { ISidebarAgentApplicationCustomizerProperties, ISidebarAgentState } from './models/ISidebarAgentProperties'; +import { normalizeConfiguration } from './configuration'; -const LOG_SOURCE: string = 'SidebarAgentApplicationCustomizer'; +const LOG_SOURCE = 'SidebarAgentApplicationCustomizer'; +const COPILOT_STUDIO_ICON: string = require('./assets/copilot-studio.svg'); -class SidebarAgentComponent extends React.Component< - { properties: ISidebarAgentApplicationCustomizerProperties; context: ApplicationCustomizerContext }, +export class SidebarAgentComponent extends React.Component< + { properties: ISidebarAgentApplicationCustomizerProperties; context: ApplicationCustomizerContext }, ISidebarAgentState > { - constructor(props: { properties: ISidebarAgentApplicationCustomizerProperties; context: ApplicationCustomizerContext }) { + public constructor(props: { properties: ISidebarAgentApplicationCustomizerProperties; context: ApplicationCustomizerContext }) { super(props); - const user = props.context?.pageContext?.user; + const user = props.context.pageContext.user; this.state = { - isPanelOpen: false, - currentUserLogin: user ? (user.loginName || user.email) : undefined, - chatKey: 0 + isPanelOpen: false, hasOpened: false, chatKey: 0, + currentUserLogin: user.email || user.loginName.split('|').pop() }; } - - private _togglePanel = (): void => { - this.setState({ isPanelOpen: !this.state.isPanelOpen }); - }; - - private _onPanelDismiss = (): void => { - this.setState({ isPanelOpen: false }); - }; - - private _startNewConversation = (): void => { - this.setState(prevState => ({ - chatKey: prevState.chatKey + 1 - })); - }; - - private _getBaseUrl = (): string => { - // Use SPFx context to get the base site URL - const context = this.props.context; - if (context?.pageContext?.web?.absoluteUrl) { - return context.pageContext.web.absoluteUrl; - } - // Fallback to origin if context is not available - return window.location.origin; - }; - public render(): React.ReactElement { - const { isPanelOpen } = this.state; - const { properties } = this.props; - - const headerColor = properties.headerBackgroundColor || 'white'; + const { properties, context } = this.props; + const { isPanelOpen, hasOpened, chatKey, currentUserLogin } = this.state; const agentTitle = properties.agentTitle || 'Copilot Studio Agent'; - - const hasRequiredProps = properties.appClientId && - properties.tenantId && - (properties.directConnectUrl || - (properties.agentIdentifier && properties.environmentId)); - - if (!hasRequiredProps) { - return ( -
- Configuration Error: Missing required properties. Please configure appClientId, tenantId, and either directConnectUrl OR both agentIdentifier and environmentId. -
- ); + try { + normalizeConfiguration(properties); + } catch (error) { + return + {error instanceof Error ? error.message : 'Check the agent configuration.'} + ; } - return ( -
-
- + <> +
+ } + ariaLabel={`Open ${agentTitle}`} + aria-expanded={isPanelOpen} + aria-haspopup="dialog" + title={`Open ${agentTitle} (Microsoft Copilot Studio)`} + onClick={() => this.setState({ isPanelOpen: true, hasOpened: true })} + />
- - -
+ {hasOpened && ( + this.setState({ isPanelOpen: false })} + onNewConversation={() => this.setState(previous => ({ chatKey: previous.chatKey + 1 }))} + chatKey={chatKey} + /> + )} + ); } } -function CopilotIcon(): JSX.Element { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -export default class SidebarAgentApplicationCustomizer - extends BaseApplicationCustomizer { - +export default class SidebarAgentApplicationCustomizer extends BaseApplicationCustomizer { private _topPlaceholder?: PlaceholderContent; private _reactContainer?: HTMLDivElement; + private _themeProvider?: ThemeProvider; + private _theme?: IReadonlyTheme; public onInit(): Promise { + initializeIcons(); + this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey); + this._theme = this._themeProvider.tryGetTheme(); + this._themeProvider.themeChangedEvent.add(this, this._onThemeChanged); Log.info(LOG_SOURCE, `Initialized ${strings.Title}`); - - if (!this.properties.appClientId || !this.properties.tenantId) { - Log.error(LOG_SOURCE, new Error('appClientId and tenantId are required properties.')); - return Promise.reject('Missing required properties: appClientId and tenantId'); - } - - if (!this.properties.directConnectUrl && (!this.properties.agentIdentifier || !this.properties.environmentId)) { - Log.error(LOG_SOURCE, new Error('Either directConnectUrl OR both agentIdentifier and environmentId must be provided.')); - return Promise.reject('Missing required properties: Either provide directConnectUrl OR both agentIdentifier and environmentId'); - } - this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceholders); this._renderPlaceholders(); return Promise.resolve(); } - private _renderPlaceholders(): void { + private _renderPlaceholders = (): void => { + if (this._topPlaceholder) return; + this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._unmount }); if (!this._topPlaceholder) { - this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { onDispose: this._onDispose }); - - if (!this._topPlaceholder) { - Log.warn(LOG_SOURCE, 'Top placeholder not available.'); - return; - } - - if (this._topPlaceholder.domElement) { - this._topPlaceholder.domElement.innerHTML = ''; - this._reactContainer = document.createElement('div'); - this._topPlaceholder.domElement.appendChild(this._reactContainer); - - const componentElement: React.ReactElement = React.createElement( - SidebarAgentComponent, - { properties: this.properties, context: this.context } - ); + Log.warn(LOG_SOURCE, 'Top placeholder is not available on this page.'); + return; + } + this._reactContainer = document.createElement('div'); + this._topPlaceholder.domElement.appendChild(this._reactContainer); + this._renderComponent(); + }; - ReactDOM.render(componentElement, this._reactContainer); - } + private _renderComponent = (): void => { + if (this._reactContainer) { + ReactDOM.render( + + + , + this._reactContainer + ); } - } + }; - private _onDispose = (): void => { + private _onThemeChanged = (event: ThemeChangedEventArgs): void => { + this._theme = event.theme; + this._renderComponent(); + }; + + private _unmount = (): void => { if (this._reactContainer) { - try { - ReactDOM.unmountComponentAtNode(this._reactContainer); - } catch { /* noop */ } + ReactDOM.unmountComponentAtNode(this._reactContainer); + this._reactContainer.remove(); this._reactContainer = undefined; } - Log.info(LOG_SOURCE, 'Disposed Top placeholder content.'); + this._topPlaceholder = undefined; }; + + public onDispose(): void { + this.context.placeholderProvider.changedEvent.remove(this, this._renderPlaceholders); + this._themeProvider?.themeChangedEvent.remove(this, this._onThemeChanged); + this._topPlaceholder?.dispose(); + this._unmount(); + } } diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/assets/copilot-studio.svg b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/assets/copilot-studio.svg new file mode 100644 index 00000000..01d6824b --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/assets/copilot-studio.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/AttachmentValidation.tsx b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/AttachmentValidation.tsx new file mode 100644 index 00000000..feb134b0 --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/AttachmentValidation.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { hooks } from 'botframework-webchat'; +import { validateFile } from './attachments'; + +export function AttachmentValidation({ + enabled, onError +}: { enabled: boolean; onError: (message: string) => void }): React.ReactElement { + const [attachments, setAttachments] = hooks.useSendBoxAttachments(); + React.useEffect(() => { + const errors: string[] = []; + const accepted = attachments.filter(({ blob }: { blob: Blob }) => { + try { + if (!enabled) throw new Error('File uploads are disabled for this agent.'); + validateFile(blob instanceof File ? blob.name : '', blob.size); + return true; + } catch (error) { + errors.push(error instanceof Error ? error.message : 'Could not read the selected file.'); + return false; + } + }); + if (errors.length) { + setAttachments(accepted); + onError(errors.join(' ')); + } + }, [attachments, enabled, onError, setAttachments]); + return <>; +} diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.module.scss b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.module.scss new file mode 100644 index 00000000..fb4da1c4 --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.module.scss @@ -0,0 +1,43 @@ +.chat { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.webChat { + display: flex; + flex: 1; + min-height: 0; + + > * { flex: 1; min-width: 0; } +} + +.loading { + display: grid; + place-items: center; + flex: 1; + padding: 24px; + + p { max-width: 320px; text-align: center; } +} + +.errorMessage { + white-space: pre-wrap; + overflow-wrap: anywhere; + max-height: 160px; + overflow-y: auto; +} + +.authDetails { + margin-top: 12px; + overflow-wrap: anywhere; +} + +.reconnect { + display: flex; + align-items: center; + gap: 12px; + margin-top: 12px; +} diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.tsx b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.tsx index e8f829d8..364c3ba0 100644 --- a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.tsx +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/Chat.tsx @@ -1,130 +1,202 @@ import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { Components } from 'botframework-webchat'; +import { Components, createStore } from 'botframework-webchat'; import { FluentThemeProvider } from 'botframework-webchat-fluent-theme'; -import { ConnectionSettings, CopilotStudioClient, CopilotStudioWebChat, CopilotStudioWebChatConnection } from '@microsoft/agents-copilotstudio-client'; -import { acquireToken } from './acquireToken'; +import { ConnectionSettings, CopilotStudioClient, CopilotStudioWebChatConnection, ScopeHelper } from '@microsoft/agents-copilotstudio-client'; +import { DefaultButton, MessageBar, MessageBarType, Spinner, SpinnerSize, useTheme } from '@fluentui/react'; +import { ChatConfiguration } from '../../configuration'; +import { acquireToken, SignInRequiredError } from './acquireToken'; +import { createConnection } from './createConnection'; +import { AttachmentValidation } from './AttachmentValidation'; +import { UPLOAD_ACCEPT } from './attachments'; +import styles from './Chat.module.scss'; const { BasicWebChat, Composer } = Components; export interface IChatProps { - appClientId: string; - tenantId: string; - environmentId?: string; - agentIdentifier?: string; - directConnectUrl?: string; - showTyping?: boolean; + configuration: ChatConfiguration; currentUserLogin?: string; - baseUrl?: string; // Add this line + baseUrl: string; } -const Chat: React.FC = ({ - appClientId, - tenantId, - environmentId, - agentIdentifier, - directConnectUrl, - showTyping = true, - currentUserLogin, - baseUrl // Add this parameter -}) => { - const [connection, setConnection] = useState(null); - const [error, setError] = useState(); - - const isConfigured = appClientId && tenantId && (directConnectUrl || (environmentId && agentIdentifier)); - - useEffect(() => { - if (!isConfigured) return; - +const Chat: React.FC = ({ configuration, currentUserLogin, baseUrl }) => { + const theme = useTheme(); + // Recreating these options when busy changes resets Web Chat's transcript scroll position. + const styleOptions = React.useMemo['styleOptions']>(() => ({ + rootHeight: '100%', + rootWidth: '100%', + backgroundColor: theme.semanticColors.bodyBackground, + bubbleBackground: theme.semanticColors.bodyBackground, + bubbleTextColor: theme.semanticColors.bodyText, + bubbleFromUserBackground: theme.palette.neutralLighter, + bubbleFromUserTextColor: theme.semanticColors.bodyText, + bubbleBorderRadius: 8, + bubbleFromUserBorderRadius: 8, + primaryFont: theme.fonts.medium.fontFamily, + accent: theme.palette.themePrimary, + disableFileUpload: !configuration.enableFileUpload, + uploadAccept: UPLOAD_ACCEPT, + uploadMultiple: true, + hideTelephoneKeypadButton: true, + partGroupDefaultOpen: false + }), [theme, configuration.enableFileUpload]); + const themeVariables: React.CSSProperties & Record<`--${string}`, string> = { + '--colorNeutralForeground1': theme.semanticColors.bodyText, + '--colorNeutralForeground2': theme.semanticColors.bodyText, + '--colorNeutralForeground3': theme.semanticColors.bodySubtext, + '--colorNeutralForegroundDisabled': theme.semanticColors.disabledText, + '--colorNeutralBackground1': theme.semanticColors.bodyBackground, + '--colorNeutralBackground1Hover': theme.palette.neutralLighter, + '--colorNeutralBackground1Pressed': theme.palette.neutralLight, + '--colorNeutralBackground3': theme.palette.neutralLighter, + '--colorNeutralBackground4': theme.palette.neutralLighterAlt, + '--colorNeutralBackground5': theme.palette.neutralLight, + '--colorNeutralBackgroundDisabled': theme.semanticColors.disabledBackground, + '--colorNeutralStroke1': theme.palette.neutralTertiaryAlt, + '--colorNeutralStroke2': theme.palette.neutralLight, + '--colorStrokeFocus2': theme.palette.themePrimary, + '--colorBrandForeground1': theme.palette.themePrimary, + '--colorBrandForegroundLink': theme.palette.themePrimary, + '--colorBrandForegroundLinkHover': theme.palette.themeDark, + '--colorBrandBackground': theme.palette.themePrimary, + '--colorBrandBackgroundHover': theme.palette.themeDarkAlt, + '--colorBrandBackgroundPressed': theme.palette.themeDark, + '--colorBrandBackground2': theme.palette.themeLighterAlt, + '--colorStatusDangerForeground1': theme.semanticColors.errorText, + '--fontFamilyBase': theme.fonts.medium.fontFamily || '"Segoe UI", sans-serif' + }; + const [connection, setConnection] = React.useState(); + const [error, setError] = React.useState(); + const [authenticationFailed, setAuthenticationFailed] = React.useState(false); + const [signInRequired, setSignInRequired] = React.useState(false); + const signIn = React.useRef<() => void>(); + const [uploadError, setUploadError] = React.useState(); + const [attempt, setAttempt] = React.useState(0); + const [busy, setBusy] = React.useState(false); + const store = React.useMemo(() => createStore(), [attempt]); + + React.useEffect(() => { let cancelled = false; - - const initializeConnection = async (): Promise => { + let activeConnection: CopilotStudioWebChatConnection | undefined; + setError(undefined); + setAuthenticationFailed(false); + setSignInRequired(false); + setUploadError(undefined); + setConnection(undefined); + setBusy(false); + + const initialize = async (interactive = false): Promise => { + setError(undefined); + let acquiringToken = false; try { - const token = await acquireToken({ - appClientId, - tenantId, - currentUserLogin, - redirectUri: baseUrl // Pass baseUrl as redirectUri - }); - - if (!token) { - setError('Unable to acquire token.'); - return; - } - const settings = new ConnectionSettings({ - appClientId, - tenantId, - environmentId: environmentId || '', - agentIdentifier: agentIdentifier || '', - directConnectUrl: directConnectUrl || '' + environmentId: configuration.environmentId, + schemaName: configuration.schemaName, + directConnectUrl: configuration.directConnectUrl }); - - const client = new CopilotStudioClient(settings, token); - const webchatSettings = { showTyping }; - - if (!cancelled) { - setConnection(CopilotStudioWebChat.createConnection(client, webchatSettings)); - } - } catch (e) { + acquiringToken = true; + const token = await acquireToken({ + appClientId: configuration.appClientId, tenantId: configuration.tenantId, + currentUserLogin, redirectUri: baseUrl, + scopes: [ScopeHelper.getScopeFromSettings(settings)] + }, interactive); + acquiringToken = false; + if (cancelled) return; + setAuthenticationFailed(false); + setSignInRequired(false); + activeConnection = createConnection(new CopilotStudioClient(settings, token), configuration, { + onTurn: () => { + setError(undefined); + setBusy(true); + }, + onTurnEnd: () => { if (!cancelled) setBusy(false); }, + onError: failure => { + if (!cancelled) { + console.error('Copilot connection failed:', failure.name); + setError(failure.message); + } + } + }); + setConnection(activeConnection); + } catch (failure) { if (!cancelled) { - console.error('Chat initialization error:', e); - setError(e instanceof Error ? e.message : 'Unknown error initializing chat'); + setAuthenticationFailed(acquiringToken); + if (failure instanceof SignInRequiredError) setSignInRequired(true); + console.error('Copilot initialization failed:', failure instanceof Error ? failure.name : 'Unknown error'); + setError(failure instanceof Error ? failure.message : 'Could not connect to the agent.'); } } }; - - initializeConnection(); - - return () => { - cancelled = true; + const connect = (interactive = false): void => { + initialize(interactive).catch(failure => { + if (!cancelled) setError(failure instanceof Error ? failure.message : 'Could not connect to the agent.'); + }); }; - }, [isConfigured, appClientId, tenantId, environmentId, agentIdentifier, directConnectUrl, showTyping, currentUserLogin, baseUrl]); // Add baseUrl to dependencies - - if (!isConfigured) { - return ( - - Configure appClientId, tenantId, and either directConnectUrl or (environmentId and agentIdentifier) in the manifest properties. - - ); - } - - if (error) { - return Error: {error}; - } - - if (!connection) { - return Connecting to Copilot Studio...; - } + signIn.current = () => connect(true); + connect(); + return () => { + cancelled = true; + signIn.current = undefined; + activeConnection?.end(); + }; + }, [configuration, currentUserLogin, baseUrl, attempt]); return ( - - - - - +
+ {uploadError && setUploadError(undefined)}> + {uploadError} + } + {error && ( + +
{error}
+ {authenticationFailed && ( +
+ {signInRequired &&

+ Select Sign in to open Microsoft sign-in. If Microsoft shows an error without redirecting back, + its detailed message stays in that window and cannot be read by this page. +

} +
Application ID: {configuration.appClientId}
+
Redirect URI (Single-page application): {baseUrl}
+
+ )} +
+ {signInRequired ? ( + signIn.current?.()}>Sign in + ) : ( + <> + setAttempt(previous => previous + 1)}>Reconnect + Reconnecting starts a new conversation. A failed request may already have reached the agent. + + )} +
+
+ )} + {!connection && !error && ( +
+
+ + {signInRequired &&

+ Complete sign-in in the pop-up. If Microsoft displays an error, that window contains the detailed sign-in message. +

} +
+
+ )} + {connection && ( + + + +
+ +
+
+
+ )} +
); }; -interface ChatMessageProps { - children: React.ReactNode; - type?: 'info' | 'warning' | 'error'; -} - -const ChatMessage: React.FC = ({ children, type = 'info' }) => { - const styles = { - info: { padding: 16 }, - warning: { padding: 16, color: '#8a6d3b', backgroundColor: '#fcf8e3' }, - error: { padding: 16, color: '#a94442', backgroundColor: '#f2dede' } - }; - - return
{children}
; -}; - export default Chat; diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/acquireToken.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/acquireToken.ts index 2fd488b2..9305c147 100644 --- a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/acquireToken.ts +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/acquireToken.ts @@ -1,89 +1,63 @@ -import { PublicClientApplication, InteractionRequiredAuthError } from '@azure/msal-browser'; +import { PublicClientApplication, InteractionRequiredAuthError, BrowserAuthError, BrowserAuthErrorCodes } from '@azure/msal-browser'; export interface IAuthSettings { appClientId: string; tenantId: string; currentUserLogin?: string; - redirectUri?: string; + redirectUri: string; + scopes: string[]; } -export async function acquireToken(settings: IAuthSettings): Promise { - if (!settings?.appClientId || !settings?.tenantId) { - return undefined; +export class SignInRequiredError extends Error { + constructor(public readonly cause?: InteractionRequiredAuthError | BrowserAuthError) { + super(cause?.message || 'Sign in to connect to your agent.'); + this.name = 'SignInRequiredError'; } +} - console.log('using this as a redirect URI: ', settings.redirectUri || window.location.origin); - - const msalInstance = new PublicClientApplication({ - auth: { - clientId: settings.appClientId, - authority: `https://login.microsoftonline.com/${settings.tenantId}`, - redirectUri: settings.redirectUri || window.location.origin - }, - cache: { - cacheLocation: "localStorage" - } - }); - - await msalInstance.initialize(); - const scopes = ['https://api.powerplatform.com/.default']; +const applications = new Map>(); + +function getApplication(settings: IAuthSettings): Promise { + const key = `${settings.appClientId}|${settings.tenantId}|${settings.redirectUri}`; + let application = applications.get(key); + if (!application) { + const instance = new PublicClientApplication({ + auth: { + clientId: settings.appClientId, + authority: `https://login.microsoftonline.com/${settings.tenantId}`, + redirectUri: settings.redirectUri + }, + cache: { cacheLocation: 'sessionStorage' } + }); + application = instance.initialize().then(() => instance).catch(error => { + applications.delete(key); + throw error; + }); + applications.set(key, application); + } + return application; +} +export async function acquireToken(settings: IAuthSettings, interactive = false): Promise { + const instance = await getApplication(settings); + const accounts = instance.getAllAccounts(); + const account = settings.currentUserLogin + ? accounts.find(candidate => candidate.username.toLowerCase() === settings.currentUserLogin?.toLowerCase()) + : accounts.length === 1 ? accounts[0] : undefined; + const request = { scopes: settings.scopes, loginHint: settings.currentUserLogin }; + if (interactive) return (await instance.acquireTokenPopup(request)).accessToken; + if (!account && !settings.currentUserLogin) throw new SignInRequiredError(); try { - const accounts = await msalInstance.getAllAccounts(); - let userAccount = null; - - if (!accounts || accounts.length === 0) { - console.log("No users are signed in"); - } else if (accounts.length > 1 && settings.currentUserLogin) { - userAccount = accounts.find(account => - account.username.toLowerCase() === settings.currentUserLogin?.toLowerCase() - ) || null; - } else { - userAccount = accounts[0]; - } - - if (userAccount) { - const accessTokenRequest = { - scopes: scopes, - account: userAccount - }; - - try { - const response = await msalInstance.acquireTokenSilent(accessTokenRequest); - console.log('Token acquired via acquireTokenSilent'); - return response.accessToken; - } catch (errorInternal) { - console.log('acquireTokenSilent failed:', errorInternal); - // Fall through to try SSO - } + if (account) { + return (await instance.acquireTokenSilent({ ...request, account })).accessToken; } - - // Only reach here if no account or acquireTokenSilent failed - const ssoRequest = { - scopes: scopes, - loginHint: settings.currentUserLogin - }; - - try { - const response = await msalInstance.ssoSilent(ssoRequest); - console.log('Token acquired via ssoSilent'); - return response.accessToken; - } catch (silentError) { - console.log('ssoSilent failed:', silentError); - if (silentError instanceof InteractionRequiredAuthError) { - try { - const response = await msalInstance.loginPopup(ssoRequest); - console.log('Token acquired via loginPopup'); - return response.accessToken; - } catch (popupError) { - console.log('loginPopup failed:', popupError); - return undefined; - } - } - return undefined; + return (await instance.ssoSilent(request)).accessToken; + } catch (error) { + if (error instanceof InteractionRequiredAuthError || + (error instanceof BrowserAuthError && error.errorCode === BrowserAuthErrorCodes.monitorWindowTimeout)) { + console.warn('Copilot silent sign-in requires interaction:', error.errorCode); + throw new SignInRequiredError(error); } - } catch (e) { - console.error('Token acquisition error', e); - return undefined; + throw error; } } diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/activityDetails.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/activityDetails.ts new file mode 100644 index 00000000..1b5ce1bc --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/activityDetails.ts @@ -0,0 +1,127 @@ +export interface ActivityVisibility { + showReasoning: boolean; + showToolCalls: boolean; +} + +export interface ActivityDetail { + id: string; + kind: 'reasoning' | 'tool'; + text: string; + status?: 'running' | 'finished' | 'failed'; + sequence?: number; +} + +export interface ActivityDetailsState { + entries: ActivityDetail[]; + toolNames: Record; +} + +export function emptyActivityDetails(): ActivityDetailsState { + return { entries: [], toolNames: {} }; +} + +export function record(value: unknown): Record { + return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; +} + +function text(value: unknown): string { + return typeof value === 'string' ? value : ''; +} + +function identifier(value: unknown): string { + return typeof value === 'number' && Number.isFinite(value) ? String(value) : text(value); +} + +export function isInformative(activity: unknown): boolean { + const item = record(activity); + return item.type === 'typing' && streamMetadata(item).streamType === 'informative'; +} + +function entities(activity: Record): Record[] { + return Array.isArray(activity.entities) ? activity.entities.map(record) : []; +} + +function streamMetadata(activity: Record): Record { + return { ...entities(activity).find(entity => entity.type === 'streaminfo'), ...record(activity.channelData) }; +} + +export function hasDetailEntities(activity: unknown): boolean { + return entities(record(activity)).some(entity => entity.type === 'thought' || entity.type === 'toolCall'); +} + +export function isToolEvent(activity: unknown): boolean { + const item = record(activity); + return item.type === 'event' && [ + 'DynamicPlanReceived', 'DynamicPlanStepTriggered', 'DynamicPlanStepBindUpdate', 'DynamicPlanStepFinished' + ].indexOf(text(item.valueType)) !== -1; +} + +export function updateActivityDetails( + state: ActivityDetailsState, activity: unknown, visibility: ActivityVisibility +): ActivityDetailsState { + const item = record(activity); + const stream = streamMetadata(item); + const sequence = typeof stream.streamSequence === 'number' ? stream.streamSequence : undefined; + if (hasDetailEntities(item)) { + let next = state; + for (const entity of entities(item)) { + if (entity.type === 'thought' && visibility.showReasoning) { + const content = text(entity.text); + if (!content) continue; + const chain = identifier(entity.chainOfThoughtId) || text(stream.streamId) || text(item.id) || 'current'; + const id = `reasoning:${chain}:${identifier(entity.sequenceNumber) || '0'}`; + const previous = next.entries.find(entry => entry.id === id); + if (previous?.sequence !== undefined && sequence !== undefined && sequence <= previous.sequence) continue; + // The SDK accumulates activity.text, not thought.text; thought deltas still need assembly. + const nextText = stream.chunkType === 'delta' ? (previous?.text || '') + content : content; + const entry: ActivityDetail = { id, kind: 'reasoning', text: nextText, sequence }; + next = { ...next, entries: upsert(next.entries, entry) }; + } else if (entity.type === 'toolCall' && visibility.showToolCalls) { + const callId = identifier(entity.toolCallId); + const status = entity.status === 'completed' ? 'finished' + : entity.status === 'started' ? 'running' : entity.status === 'failed' ? 'failed' : undefined; + if (!callId || !status) continue; + const id = `tool:${callId}`; + const previous = next.entries.find(entry => entry.id === id); + if (previous?.sequence !== undefined && sequence !== undefined && sequence <= previous.sequence) continue; + if (previous?.status && previous.status !== 'running' && status === 'running') continue; + const entry: ActivityDetail = { + id, kind: 'tool', status, sequence, + text: text(entity.toolDisplayName) || text(entity.toolName) || text(entity.operationId) || previous?.text || 'Agent action' + }; + next = { ...next, entries: upsert(next.entries, entry) }; + } + } + return next; + } + if (!visibility.showToolCalls || !isToolEvent(item)) return state; + const value = record(item.value); + if (item.valueType === 'DynamicPlanReceived' && Array.isArray(value.toolDefinitions)) { + const toolNames = { ...state.toolNames }; + value.toolDefinitions.forEach(definition => { + const tool = record(definition); + const schema = text(tool.schemaName); + if (schema && text(tool.displayName)) toolNames[schema] = text(tool.displayName); + }); + return { ...state, toolNames }; + } + if (item.valueType !== 'DynamicPlanStepTriggered' && item.valueType !== 'DynamicPlanStepFinished') return state; + const schema = text(value.taskDialogId); + const step = identifier(value.stepId) || identifier(value.id) || schema; + if (!step) return state; + const id = `tool:${step}`; + const previous = state.entries.find(entry => entry.id === id); + if (previous?.status === 'finished' && item.valueType === 'DynamicPlanStepTriggered') return state; + const entry: ActivityDetail = { + id, kind: 'tool', + text: text(state.toolNames[schema]) || schema || 'Agent action', + status: item.valueType === 'DynamicPlanStepFinished' ? 'finished' : 'running' + }; + return { ...state, entries: upsert(state.entries, entry) }; +} + +function upsert(entries: ActivityDetail[], entry: ActivityDetail): ActivityDetail[] { + return entries.some(existing => existing.id === entry.id) + ? entries.map(existing => existing.id === entry.id ? entry : existing) + : [...entries, entry]; +} diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/attachments.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/attachments.ts new file mode 100644 index 00000000..55e9e027 --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/attachments.ts @@ -0,0 +1,49 @@ +export const MAX_FILE_BYTES = 15 * 1024 * 1024; +const contentTypes: Record = { + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + csv: 'text/csv', + pdf: 'application/pdf', + txt: 'text/plain', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + webp: 'image/webp', + gif: 'image/gif' +}; +export const UPLOAD_ACCEPT = Object.keys(contentTypes).map(extension => `.${extension}`).join(','); + +export function validateFile(name: string, size: number): string { + const extension = name.split('.').pop()?.toLowerCase() || ''; + const contentType = Object.prototype.hasOwnProperty.call(contentTypes, extension) ? contentTypes[extension] : undefined; + if (!contentType) throw new Error(`${name || 'This file'} is not supported. Use DOCX, CSV, PDF, TXT, JPG, PNG, WebP, or a nonanimated GIF.`); + if (!Number.isFinite(size) || size <= 0) throw new Error(`${name} is empty or could not be read.`); + if (size > MAX_FILE_BYTES) throw new Error(`${name} exceeds the 15 MiB file limit. Choose a smaller file.`); + return contentType; +} + +export interface LocalAttachment { + name?: string; + contentType: string; + contentUrl?: string; +} + +export async function prepareAttachments( + attachments: T[], enabled: boolean, read: typeof fetch = fetch +): Promise { + if (attachments.length && !enabled) throw new Error('File uploads are disabled for this agent.'); + return Promise.all(attachments.map(async attachment => { + if (!attachment.contentUrl?.startsWith('blob:')) { + throw new Error('Choose a local file to attach. Remote attachment URLs are not supported.'); + } + const response = await read(attachment.contentUrl); + if (!response.ok) throw new Error(`Could not read ${attachment.name || 'the attachment'}. Remove it and attach it again.`); + const blob = await response.blob(); + const contentType = validateFile(attachment.name || '', blob.size); + const bytes = new Uint8Array(await blob.arrayBuffer()); + let binary = ''; + for (let offset = 0; offset < bytes.length; offset += 8192) { + binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192)); + } + return { ...attachment, contentType, contentUrl: `data:${contentType};base64,${btoa(binary)}` }; + })); +} diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/createConnection.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/createConnection.ts new file mode 100644 index 00000000..f64e9f25 --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/createConnection.ts @@ -0,0 +1,145 @@ +import { CopilotStudioClient, CopilotStudioWebChat, CopilotStudioWebChatConnection } from '@microsoft/agents-copilotstudio-client'; +import { Observable, ReplaySubject, share, Subscription } from 'rxjs'; +import { Activity } from '@microsoft/agents-activity'; +import { ChatConfiguration } from '../../configuration'; +import { createPreChatActivity, NativeActivityAdapter } from './nativeActivities'; +import { prepareAttachments } from './attachments'; + +export interface ConnectionCallbacks { + onTurn: () => void; + onTurnEnd: () => void; + onError: (error: Error) => void; +} + +export function createConnection( + client: CopilotStudioClient, configuration: ChatConfiguration, callbacks: ConnectionCallbacks +): CopilotStudioWebChatConnection { + const connection = CopilotStudioWebChat.createConnection(client, { + showTyping: configuration.showTyping, startConversation: configuration.startConversation + }); + let initialization: Promise | undefined; + const initializeConversation = (): Promise => { + if (!initialization) { + initialization = (async () => { + const activities = client.startConversationStreaming(false); + while (!(await activities.next()).done) { + // Drain startup without a greeting; the SDK retains the ID from response headers. + } + })().catch(error => { + initialization = undefined; + throw error; + }); + } + return initialization; + }; + return wrapConnection( + connection, configuration, callbacks, configuration.startConversation ? undefined : initializeConversation + ); +} + +export function wrapConnection( + connection: CopilotStudioWebChatConnection, configuration: ChatConfiguration, callbacks: ConnectionCallbacks, + initializeConversation?: () => Promise +): CopilotStudioWebChatConnection { + let ended = false; + let sequence = 0; + const native = new NativeActivityAdapter(configuration); + let emitUpdates: ((activities: Partial[]) => void) | undefined; + const requests = new Set(); + const report = (error: unknown): Error => { + const failure = error instanceof Error ? error : new Error('The agent connection failed. Try reconnecting.'); + if (!ended) { + emitUpdates?.(native.finishTurn(true)); + callbacks.onError(failure); + } + return failure; + }; + // WebChat first subscribes to connect, then subscribes again to render the transcript. + // Share the SDK's single subscriber and replay activities that arrived during startup. + const activity$ = new Observable>(subscriber => { + const emit = (activities: Partial[]): void => { + activities.forEach(activity => { + const incoming = { ...activity, id: activity.id || `sidepane-activity-${++sequence}` }; + if (incoming.type === 'typing' && !incoming.text && !configuration.showTyping) return; + subscriber.next(incoming); + }); + }; + emitUpdates = emit; + if (!configuration.startConversation) emit([createPreChatActivity(configuration)]); + const subscription = connection.activity$.subscribe({ + next: activity => { + if (ended) return; + emit(native.transform(activity)); + }, + error: error => { + report(error); + connection.connectionStatus$.next(4); + subscriber.complete(); + }, + complete: () => subscriber.complete() + }); + return () => { + if (emitUpdates === emit) emitUpdates = undefined; + subscription.unsubscribe(); + }; + }).pipe(share({ + connector: () => new ReplaySubject>(), + resetOnComplete: true, + resetOnError: true, + resetOnRefCountZero: true + })); + return { + connectionStatus$: connection.connectionStatus$, + get conversationId() { return connection.conversationId; }, + activity$, + postActivity: activity => new Observable(subscriber => { + let cancelled = false; + let acknowledged = false; + const subscription = new Subscription(); + const finish = (interrupted = false): void => { + if (!requests.delete(subscription)) return; + if (!ended && activity.type === 'message') { + emitUpdates?.(native.finishTurn(interrupted)); + callbacks.onTurnEnd(); + } + }; + const send = async (): Promise => { + try { + if (ended || cancelled) return; + requests.add(subscription); + if (activity.type === 'message') { + native.startTurn(); + callbacks.onTurn(); + } + const attachments = await prepareAttachments(activity.attachments || [], configuration.enableFileUpload); + if (ended || cancelled) return; + if (activity.type === 'message') await initializeConversation?.(); + if (ended || cancelled) return; + subscription.add(connection.postActivity(Activity.fromObject({ ...activity, attachments })).subscribe({ + next: id => { acknowledged = true; subscriber.next(id); }, + error: error => { finish(true); subscriber.error(report(error)); }, + complete: () => { finish(); subscriber.complete(); } + })); + } catch (error) { + finish(true); + if (!ended && !cancelled) subscriber.error(report(error)); + } + }; + send().catch(error => subscriber.error(report(error))); + return () => { + cancelled = true; + // WebChat unsubscribes after the send acknowledgement, before the response stream finishes. + if (!acknowledged) { + subscription.unsubscribe(); + finish(true); + } + }; + }), + end: () => { + ended = true; + requests.forEach(subscription => subscription.unsubscribe()); + requests.clear(); + connection.end(); + } + }; +} diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/nativeActivities.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/nativeActivities.ts new file mode 100644 index 00000000..6d612956 --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/nativeActivities.ts @@ -0,0 +1,116 @@ +import { Activity } from '@microsoft/agents-activity'; +import { ChatConfiguration } from '../../configuration'; +import { ActivityDetail, emptyActivityDetails, hasDetailEntities, isInformative, isToolEvent, record, updateActivityDetails } from './activityDetails'; + +export function createPreChatActivity(configuration: ChatConfiguration): Partial { + return { + type: 'message', + id: 'sidepane:welcome', + from: { id: 'agent', role: 'bot' }, + timestamp: new Date().toISOString(), + channelData: { 'webchat:sequence-id': -1 }, + text: '', + entities: [{ + type: 'https://schema.org/Message', '@context': 'https://schema.org', '@id': '', '@type': 'Message', + keywords: ['PreChatMessage'], + author: { + '@type': 'Person', + name: 'How can I help?', + // Fluent 4.19 renders an avatar even when no author image is provided. + image: `data:image/svg+xml,${encodeURIComponent( + '' + )}`, + description: configuration.enableFileUpload + ? 'Ask a question or attach a document to get started.' + : 'Ask a question to get started.' + } + }], + suggestedActions: { + to: [], + actions: configuration.starterPrompts.map(prompt => ({ + type: 'messageBack', title: prompt.title, text: prompt.text + })) + } + }; +} + +function isNativeAnalysis(activity: Partial): boolean { + return !!activity.entities?.some(entity => { + const message = record(entity); + return message.type === 'https://schema.org/Message' && + Array.isArray(message.keywords) && message.keywords.indexOf('AnalysisMessage') !== -1; + }); +} + +export class NativeActivityAdapter { + private state = emptyActivityDetails(); + private turn = 0; + private outcome: 'finished' | 'interrupted' | undefined; + private sequenceIds = new Map(); + private timestamp = new Date().toISOString(); + private author = { id: 'agent', role: 'bot' }; + + constructor(private readonly configuration: ChatConfiguration) {} + + startTurn(): void { + this.state = emptyActivityDetails(); + this.sequenceIds.clear(); + this.timestamp = new Date().toISOString(); + this.outcome = undefined; + this.turn++; + } + + transform(activity: Partial): Partial[] { + if (activity.type === 'event' && activity.name === 'turn.complete') return this.finishTurn(); + if (hasDetailEntities(activity) || isInformative(activity) || isToolEvent(activity)) { + const previous = this.state; + this.state = updateActivityDetails(previous, activity, this.configuration); + if (typeof activity.from?.id === 'string') this.author = { id: activity.from.id, role: 'bot' }; + return this.state.entries + .filter(entry => previous.entries.find(candidate => candidate.id === entry.id) !== entry) + .map(entry => { + const sequenceId = record(activity.channelData)['webchat:sequence-id']; + if (!this.sequenceIds.has(entry.id) && typeof sequenceId === 'number') { + this.sequenceIds.set(entry.id, sequenceId); + } + return this.render(entry); + }); + } + if (isNativeAnalysis(activity) && !this.configuration.showReasoning) return []; + return [activity]; + } + + finishTurn(interrupted = false): Partial[] { + if (this.outcome) return []; + this.outcome = interrupted ? 'interrupted' : 'finished'; + return this.state.entries.map(entry => this.render(entry)); + } + + private render(entry: ActivityDetail): Partial { + const finished = entry.kind === 'tool' ? entry.status === 'finished' : this.outcome === 'finished'; + const failed = entry.status === 'failed'; + return { + type: 'message', + id: `sidepane:turn:${this.turn}:${entry.id}`, + from: this.author, + timestamp: this.timestamp, + // Keep the SDK's sequence scale and the step's original position on updates. + channelData: { 'webchat:sequence-id': this.sequenceIds.get(entry.id) }, + textFormat: 'plain', + text: entry.kind === 'reasoning' ? entry.text : failed ? 'Failed' : finished ? 'Finished' : this.outcome ? 'No finish event received.' : 'Started', + entities: [{ + type: 'https://schema.org/Message', '@context': 'https://schema.org', '@id': '', '@type': 'Message', + abstract: entry.kind === 'reasoning' ? 'Reasoning' : entry.text, + keywords: ['AnalysisMessage', 'Collapsible'], + position: this.state.entries.indexOf(entry) + 1, + creativeWorkStatus: failed ? 'Failed' : finished ? 'Published' : this.outcome ? undefined : 'Incomplete', + isPartOf: { + '@id': `sidepane:turn:${this.turn}`, + '@type': 'HowTo', + abstract: this.outcome === 'interrupted' ? 'Agent activity interrupted' : 'Agent activity', + creativeWorkStatus: this.outcome === 'interrupted' ? 'Failed' : this.outcome === 'finished' ? 'Published' : 'Incomplete' + } + }] + }; + } +} diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.module.scss b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.module.scss index 103c70de..5e17e360 100644 --- a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.module.scss +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.module.scss @@ -1,95 +1,36 @@ - - .sidePanel { - position: fixed; - top: 0; - right: -30vw; - width: 30vw; - height: 100vh; - background-color: white; - box-shadow: none; - transition: right 0.3s ease-in-out, box-shadow 0.3s ease-in-out; - z-index: 1000; - display: flex; - flex-direction: column; + font-family: "Segoe UI", Aptos, Calibri, sans-serif; - &.open { - right: 0; - box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1); + @media (prefers-reduced-motion: reduce) { + &, * { animation-duration: 0s !important; transition-duration: 0s !important; } } } -.chatContainer { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; - // Use SharePoint theme colors for gradient - background: linear-gradient(135deg, - "[theme:themeLighterAlt, default: #f3f2f1]" 0%, - "[theme:themeLighter, default: #d2d0ce]" 100%); - padding: 20px; - gap: 16px; -} - -.titleBar { - background: white; - border-radius: 12px; - padding: 16px 24px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +.header { display: flex; align-items: center; - justify-content: space-between; + gap: 12px; + flex: 0 0 auto; + min-width: 0; + padding: 16px; } .title { - margin: 0; - font-size: 16px; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin: 0 0 0 8px; + font-size: 20px; font-weight: 600; - color: "[theme:bodyText, default: #323130]"; + line-height: 1.4; } -.buttonGroup { +.launcher { display: flex; - gap: 8px; - align-items: center; + justify-content: flex-end; + padding: 4px 16px; + border-bottom: 1px solid "[theme:neutralLight, default: #edebe9]"; + background: "[theme:bodyBackground, default: #ffffff]"; } - -.iconButton { - background: none; - border: none; - cursor: pointer; - padding: 8px; - display: flex; - align-items: center; - justify-content: center; - color: "[theme:neutralSecondary, default: #605e5c]"; - border-radius: 4px; - transition: all 0.2s ease; - - &:hover { - background-color: "[theme:neutralLighter, default: #f3f2f1]"; - color: "[theme:neutralPrimary, default: #323130]"; - } -} - -.chatWrapper { - background: white; - border-radius: 12px; - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.4); - z-index: 999; -} \ No newline at end of file diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.tsx b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.tsx index 91ae6c20..d3213f37 100644 --- a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.tsx +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/SidePanel/SidePanel.tsx @@ -1,69 +1,61 @@ import * as React from 'react'; +import { IconButton, Panel, PanelType, useTheme } from '@fluentui/react'; import styles from './SidePanel.module.scss'; import Chat from '../Chat/Chat'; import { ISidePanelProps } from '../../models/ISidebarAgentProperties'; +import { normalizeConfiguration } from '../../configuration'; -export default class SidePanel extends React.Component { - public render(): React.ReactElement { - const { isOpen, properties, currentUserLogin, baseUrl, onDismiss, onNewConversation, chatKey } = this.props; - const agentTitle = properties.agentTitle || 'Copilot Studio Agent'; - - return ( - <> -
-
-
-

{agentTitle}

-
- - -
-
- -
- -
-
+const SidePanel: React.FC = ({ + isOpen, properties, currentUserLogin, baseUrl, onDismiss, onNewConversation, chatKey +}) => { + const theme = useTheme(); + const configuration = React.useMemo(() => normalizeConfiguration(properties), [properties]); + return ( + ( +
+

+ {properties.agentTitle || 'Copilot Studio Agent'} +

+ +
- - {isOpen &&
} - - ); - } -} - -const NewChatIcon: React.FC = () => ( - - - -); + )} + > + + + ); +}; -const ChevronRightIcon: React.FC = () => ( - - - -); \ No newline at end of file +export default SidePanel; diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/configuration.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/configuration.ts new file mode 100644 index 00000000..d852127c --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/configuration.ts @@ -0,0 +1,72 @@ +import { ISidebarAgentApplicationCustomizerProperties, IStarterPrompt } from './models/ISidebarAgentProperties'; + +export const DEFAULT_STARTER_PROMPTS: IStarterPrompt[] = [ + { title: 'Get started', text: 'What can you help me with?' } +]; + +export interface ChatConfiguration { + appClientId: string; + tenantId: string; + environmentId?: string; + schemaName?: string; + directConnectUrl?: string; + startConversation: boolean; + showTyping: boolean; + enableFileUpload: boolean; + showReasoning: boolean; + showToolCalls: boolean; + starterPrompts: IStarterPrompt[]; +} + +function optionalText(value: unknown): string | undefined { + if (value === undefined) return undefined; + if (typeof value !== 'string') throw new Error('Connection settings must be strings.'); + return value.trim() || undefined; +} + +function flag(value: unknown, fallback: boolean, name: string): boolean { + if (value === undefined) return fallback; + if (typeof value !== 'boolean') throw new Error(`${name} must be true or false (not a string).`); + return value; +} + +function starterPrompts(value: unknown): IStarterPrompt[] { + if (value === undefined) return DEFAULT_STARTER_PROMPTS.map(prompt => ({ ...prompt })); + if (!Array.isArray(value)) throw new Error('starterPrompts must be an array of { title, text } objects.'); + return value.map(prompt => { + if (!prompt || typeof prompt !== 'object' || Array.isArray(prompt) || + Object.keys(prompt).some(key => key !== 'title' && key !== 'text') || + typeof prompt.title !== 'string' || !prompt.title.trim() || + typeof prompt.text !== 'string' || !prompt.text.trim()) { + throw new Error('Each starter prompt must contain only a non-empty title and text.'); + } + return { title: prompt.title.trim(), text: prompt.text.trim() }; + }); +} + +export function normalizeConfiguration(properties: ISidebarAgentApplicationCustomizerProperties): ChatConfiguration { + const appClientId = optionalText(properties.appClientId); + const tenantId = optionalText(properties.tenantId); + const environmentId = optionalText(properties.environmentId); + const schemaName = optionalText(properties.schemaName) || optionalText(properties.agentIdentifier); + const directConnectUrl = optionalText(properties.directConnectUrl); + if (!appClientId || !tenantId) throw new Error('Configure appClientId and tenantId for Microsoft sign-in.'); + if (!directConnectUrl && (!environmentId || !schemaName)) { + throw new Error('Configure environmentId and schemaName for endpoint discovery, or an explicit directConnectUrl.'); + } + if (directConnectUrl) { + const url = new URL(directConnectUrl); + if (url.protocol !== 'https:' || url.username || url.password || url.hash) { + throw new Error('directConnectUrl must be an HTTPS URL without credentials or a fragment.'); + } + } + return { + appClientId, tenantId, environmentId, schemaName, directConnectUrl, + startConversation: flag(properties.startConversation, false, 'startConversation'), + showTyping: flag(properties.showTyping, true, 'showTyping'), + enableFileUpload: flag(properties.enableFileUpload, true, 'enableFileUpload'), + showReasoning: flag(properties.showReasoning, false, 'showReasoning'), + showToolCalls: flag(properties.showToolCalls, false, 'showToolCalls'), + starterPrompts: starterPrompts(properties.starterPrompts) + }; +} diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/models/ISidebarAgentProperties.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/models/ISidebarAgentProperties.ts index fa932070..6ddcd952 100644 --- a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/models/ISidebarAgentProperties.ts +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/models/ISidebarAgentProperties.ts @@ -1,10 +1,22 @@ +export interface IStarterPrompt { + title: string; + text: string; +} + export interface ISidebarAgentApplicationCustomizerProperties { appClientId: string; tenantId: string; environmentId?: string; + schemaName?: string; + /** Compatibility alias for schemaName. */ agentIdentifier?: string; directConnectUrl?: string; + startConversation?: boolean; showTyping?: boolean; + enableFileUpload?: boolean; + showReasoning?: boolean; + showToolCalls?: boolean; + starterPrompts?: IStarterPrompt[]; headerBackgroundColor?: string; agentTitle?: string; } @@ -13,6 +25,7 @@ export interface ISidebarAgentState { isPanelOpen: boolean; currentUserLogin?: string; chatKey: number; + hasOpened: boolean; } export interface ISidePanelProps { diff --git a/ui/embed/sharepoint-customizer/tests/activityDetails.test.cjs b/ui/embed/sharepoint-customizer/tests/activityDetails.test.cjs new file mode 100644 index 00000000..d571897e --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/activityDetails.test.cjs @@ -0,0 +1,119 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { emptyActivityDetails, updateActivityDetails } = require('../temp/tests/components/Chat/activityDetails'); +const { thought, toolCall } = require('./fixtures/sdkActivities.cjs'); +const tool = (valueType, value = {}) => ({ type: 'event', valueType, value: { taskDialogId: 'lookup', stepId: 'call1', ...value } }); +const visible = { showReasoning: true, showToolCalls: true }; + +for (const showReasoning of [false, true]) { + for (const showToolCalls of [false, true]) { + test(`independent visibility: reasoning=${showReasoning}, tools=${showToolCalls}`, () => { + const flags = { showReasoning, showToolCalls }; + let state = updateActivityDetails(emptyActivityDetails(), thought('Checking sources', 1), flags); + state = updateActivityDetails(state, tool('DynamicPlanStepTriggered'), flags); + assert.equal(state.entries.filter(e => e.kind === 'reasoning').length, Number(showReasoning)); + assert.equal(state.entries.filter(e => e.kind === 'tool').length, Number(showToolCalls)); + }); + } +} +test('cumulative text replaces, explicit deltas append, duplicate/old sequences are ignored', () => { + let state = emptyActivityDetails(); + for (const activity of [ + thought('Checking', 1), thought('Checking sources', 2, 'replace'), + thought(' now', 3, 'delta'), thought(' duplicate', 3, 'delta'), thought('old', 1) + ]) state = updateActivityDetails(state, activity, visible); + assert.equal(state.entries.length, 1); + assert.equal(state.entries[0].text, 'Checking sources now'); +}); +test('thought deltas use entity text and the initial activity ID, then a replacement snapshot replaces the accumulated text', () => { + let state = emptyActivityDetails(); + for (const activity of [thought('Check ', 1), thought('the list', 2), thought('Check the list', 3, 'replace')]) { + state = updateActivityDetails(state, activity, visible); + } + assert.equal(state.entries.length, 1); + assert.equal(state.entries[0].id, 'reasoning:answer-stream:0'); + assert.equal(state.entries[0].text, 'Check the list'); +}); +test('separate thought chains and sequence numbers do not share accumulated text', () => { + let state = emptyActivityDetails(); + for (const properties of [ + { chainOfThoughtId: 'chain-a', sequenceNumber: 0 }, + { chainOfThoughtId: 'chain-a', sequenceNumber: 1 }, + { chainOfThoughtId: 'chain-b', sequenceNumber: 0 } + ]) state = updateActivityDetails(state, thought('Check', 1, 'delta', properties), visible); + assert.equal(state.entries.length, 3); + assert.ok(state.entries.every(entry => entry.text === 'Check')); +}); +test('toolCall entities correlate by call ID even when multiple calls share a stream and tool name', () => { + let state = emptyActivityDetails(); + for (const activity of [ + toolCall('started', { toolCallId: 'first', streamSequence: 1 }), + toolCall('started', { toolCallId: 'second', streamSequence: 2 }), + toolCall('completed', { toolCallId: 'first', streamSequence: 3 }), + toolCall('started', { toolCallId: 'first', streamSequence: 4 }), + toolCall('started', { toolCallId: 'second', streamSequence: 1 }) + ]) state = updateActivityDetails(state, activity, visible); + assert.deepEqual(state.entries.map(({ id, kind, text, status }) => ({ id, kind, text, status })), [ + { id: 'tool:first', kind: 'tool', text: 'Get items', status: 'finished' }, + { id: 'tool:second', kind: 'tool', text: 'Get items', status: 'running' } + ]); + assert.ok(!JSON.stringify(state).includes('PRIVATE_')); +}); +test('untyped informative messages, malformed tool entities and unknown tool status do not become reasoning or tool calls', () => { + const state = emptyActivityDetails(); + for (const activity of [ + { type: 'typing', text: 'Get items completed', channelData: { streamType: 'informative' } }, + { type: 'typing', text: 'Working', entities: [{ type: 'streaminfo', streamType: 'informative' }] }, + { type: 'typing', entities: [{ type: 'thought', text: { private: true } }] }, + toolCall('started', { toolCallId: '' }), toolCall('unknown') + ]) assert.strictEqual(updateActivityDetails(state, activity, visible), state); +}); +test('entity-only stream metadata retains ordering and correlates the thought stream', () => { + const first = thought('Check', 1, 'replace'); + const second = thought('Check the list', 2, 'replace'); + delete first.channelData; + delete second.channelData; + let state = updateActivityDetails(emptyActivityDetails(), first, visible); + state = updateActivityDetails(state, second, visible); + assert.equal(state.entries.length, 1); + assert.equal(state.entries[0].text, 'Check the list'); + assert.equal(state.entries[0].sequence, 2); +}); +test('tool names and lifecycle are shown without arguments, outputs or thought payloads', () => { + let state = updateActivityDetails(emptyActivityDetails(), tool('DynamicPlanReceived', { + toolDefinitions: [{ schemaName: 'lookup', displayName: 'Look up a record' }] + }), visible); + state = updateActivityDetails(state, tool('DynamicPlanStepTriggered', { thought: 'private', arguments: { secret: 'hidden' } }), visible); + state = updateActivityDetails(state, tool('DynamicPlanStepFinished', { output: 'hidden' }), visible); + state = updateActivityDetails(state, tool('DynamicPlanStepTriggered'), visible); + assert.equal(state.entries[0].text, 'Look up a record'); + assert.equal(state.entries[0].status, 'finished'); + assert.ok(!JSON.stringify(state).includes('hidden')); + assert.ok(!JSON.stringify(state).includes('private')); +}); +test('ordinary answer streaming, malformed, and unknown events do not become reasoning or tools', () => { + const state = emptyActivityDetails(); + for (const activity of [null, {}, 'bad', { type: 'typing', text: 'answer', channelData: { streamType: 'streaming' } }, + tool('unknown'), { type: 'message', text: 'Final answer' }]) { + assert.strictEqual(updateActivityDetails(state, activity, visible), state); + } + assert.deepEqual(emptyActivityDetails().entries, []); +}); + +test('numeric step IDs preserve separate invocations of the same tool', () => { + let state = emptyActivityDetails(); + for (const stepId of [0, 1]) { + state = updateActivityDetails(state, tool('DynamicPlanStepTriggered', { stepId }), visible); + } + state = updateActivityDetails(state, tool('DynamicPlanStepFinished', { stepId: 0 }), visible); + assert.deepEqual(state.entries.map(({ id, status }) => ({ id, status })), [ + { id: 'tool:0', status: 'finished' }, { id: 'tool:1', status: 'running' } + ]); +}); + +test('unknown tool names cannot resolve to inherited object properties', () => { + const state = updateActivityDetails(emptyActivityDetails(), tool('DynamicPlanStepTriggered', { + taskDialogId: 'toString' + }), visible); + assert.equal(state.entries[0].text, 'toString'); +}); diff --git a/ui/embed/sharepoint-customizer/tests/attachments.test.cjs b/ui/embed/sharepoint-customizer/tests/attachments.test.cjs new file mode 100644 index 00000000..5d5280a2 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/attachments.test.cjs @@ -0,0 +1,36 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { MAX_FILE_BYTES, validateFile, prepareAttachments } = require('../temp/tests/components/Chat/attachments'); + +test('supported files include the exact byte limit but not one byte over', () => { + assert.equal(validateFile('REPORT.PDF', MAX_FILE_BYTES), 'application/pdf'); + assert.throws(() => validateFile('report.pdf', MAX_FILE_BYTES + 1), /15 MiB/); + for (const size of [0, -1, NaN, Infinity]) assert.throws(() => validateFile('empty.txt', size), /empty|read/); + assert.throws(() => validateFile('script.exe', 1), /not supported/); +}); +test('local bytes retain name and get a meaningful MIME type instead of the blob transport type', async () => { + const result = await prepareAttachments( + [{ name: 'notes.txt', contentType: 'application/octet-stream', contentUrl: 'blob:local' }], + true, + async () => new Response('a small fixture') + ); + assert.deepEqual(result, [{ + name: 'notes.txt', contentType: 'text/plain', + contentUrl: `data:text/plain;base64,${Buffer.from('a small fixture').toString('base64')}` + }]); +}); +test('multiple local attachments and attachment-only messages need no text', async () => { + const input = ['one.txt', 'two.csv'].map(name => ({ name, contentType: '', contentUrl: `blob:${name}` })); + const result = await prepareAttachments(input, true, async () => new Response('fixture')); + assert.equal(result.length, 2); + assert.equal(result[1].contentType, 'text/csv'); + assert.equal(input[0].contentUrl, 'blob:one.txt'); +}); +test('disabled uploads, remote URLs and unreadable files fail before sending', async () => { + const attachment = { name: 'file.txt', contentType: 'text/plain', contentUrl: 'blob:file' }; + await assert.rejects(prepareAttachments([attachment], false), /disabled/); + await assert.rejects(prepareAttachments([{ ...attachment, contentUrl: 'https://example.com/file' }], true), /local file/); + await assert.rejects(prepareAttachments([attachment], true, async () => new Response('', { status: 404 })), /Could not read/); + await assert.rejects(prepareAttachments([attachment], true, async () => { throw new Error('read failed'); }), /read failed/); + assert.deepEqual(await prepareAttachments([], false), []); +}); diff --git a/ui/embed/sharepoint-customizer/tests/auth.test.cjs b/ui/embed/sharepoint-customizer/tests/auth.test.cjs new file mode 100644 index 00000000..034a5ce4 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/auth.test.cjs @@ -0,0 +1,143 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const msal = require('@azure/msal-browser'); +const { + InteractionRequiredAuthError, BrowserAuthError, BrowserAuthErrorCodes, ServerError +} = msal; +const { acquireToken, SignInRequiredError } = require('../temp/tests/components/Chat/acquireToken'); + +let application = 0; + +function fixture(t, handlers = {}, accounts = []) { + const calls = []; + const settings = { + appClientId: `00000000-0000-4000-8000-${String(++application).padStart(12, '0')}`, + tenantId: '00000000-0000-4000-8000-000000000000', + currentUserLogin: 'user@example.com', + redirectUri: 'https://example.com/sites/review', + scopes: ['https://api.powerplatform.com/CopilotStudio.Copilots.Invoke'] + }; + t.mock.method(console, 'warn', () => {}); + const instance = { getAllAccounts: () => accounts }; + for (const method of ['initialize', 'acquireTokenSilent', 'ssoSilent', 'acquireTokenPopup']) { + instance[method] = async request => { + calls.push({ method, request }); + if (handlers[method]) return handlers[method](request); + if (method === 'initialize') return; + throw new Error(`Unexpected ${method}`); + }; + } + t.mock.method(msal, 'PublicClientApplication', function () { return instance; }); + return { settings, calls, methods: () => calls.map(call => call.method) }; +} + +const timeout = () => new BrowserAuthError(BrowserAuthErrorCodes.monitorWindowTimeout); +const interaction = () => new InteractionRequiredAuthError('interaction_required'); + +test('cached token uses the matching SharePoint user and requested SDK scope', async t => { + const account = { username: 'USER@example.com' }; + const f = fixture(t, { acquireTokenSilent: () => ({ accessToken: 'cached-token' }) }, [ + { username: 'someone-else@example.com' }, account + ]); + assert.equal(await acquireToken(f.settings), 'cached-token'); + assert.deepEqual(f.methods(), ['initialize', 'acquireTokenSilent']); + assert.equal(f.calls[1].request.account, account); + assert.deepEqual(f.calls[1].request.scopes, f.settings.scopes); +}); + +test('uncached user can still sign in silently with the SharePoint login hint', async t => { + const f = fixture(t, { ssoSilent: () => ({ accessToken: 'sso-token' }) }); + assert.equal(await acquireToken(f.settings), 'sso-token'); + assert.deepEqual(f.methods(), ['initialize', 'ssoSilent']); + assert.equal(f.calls[1].request.loginHint, f.settings.currentUserLogin); +}); + +for (const failure of [timeout, interaction]) { + for (const cached of [false, true]) { + test(`${failure().errorCode} offers sign-in without another iframe or automatic popup (cached=${cached})`, async t => { + const method = cached ? 'acquireTokenSilent' : 'ssoSilent'; + const f = fixture(t, { [method]: () => { throw failure(); } }, + cached ? [{ username: 'user@example.com' }] : []); + await assert.rejects(acquireToken(f.settings), SignInRequiredError); + assert.deepEqual(f.methods(), ['initialize', method]); + }); + } +} + +test('explicit sign-in goes directly to the popup after an iframe timeout', async t => { + const f = fixture(t, { + ssoSilent: () => { throw timeout(); }, + acquireTokenPopup: () => ({ accessToken: 'interactive-token' }) + }); + await assert.rejects(acquireToken(f.settings), SignInRequiredError); + assert.equal(await acquireToken(f.settings, true), 'interactive-token'); + assert.deepEqual(f.methods(), ['initialize', 'ssoSilent', 'acquireTokenPopup']); + assert.deepEqual(f.calls[2].request, { + scopes: f.settings.scopes, loginHint: f.settings.currentUserLogin + }); +}); + +test('popup cancellation is surfaced and an explicit retry does not repeat silent sign-in', async t => { + const failure = new BrowserAuthError(BrowserAuthErrorCodes.userCancelled); + let cancelled = true; + const f = fixture(t, { + acquireTokenPopup: () => { + if (cancelled) throw failure; + return { accessToken: 'retry-token' }; + } + }); + await assert.rejects(acquireToken(f.settings, true), error => error === failure); + cancelled = false; + assert.equal(await acquireToken(f.settings, true), 'retry-token'); + assert.deepEqual(f.methods(), ['initialize', 'acquireTokenPopup', 'acquireTokenPopup']); +}); + +test('unexpected silent errors are preserved instead of disguised as sign-in requests', async t => { + const failure = new Error('Authority configuration is invalid'); + const f = fixture(t, { ssoSilent: () => { throw failure; } }); + await assert.rejects(acquireToken(f.settings), error => error === failure); + assert.deepEqual(f.methods(), ['initialize', 'ssoSilent']); +}); + +test('Entra server errors retain their full AADSTS message', async t => { + const failure = new ServerError('invalid_request', + 'AADSTS50011: The redirect URI https://example.com/sites/review does not match the configured redirect URIs.'); + const f = fixture(t, { ssoSilent: () => { throw failure; } }); + await assert.rejects(acquireToken(f.settings), error => error === failure && error.message.includes('AADSTS50011')); +}); + +test('interaction-required failures retain the original Entra details when offering sign-in', async t => { + const failure = new InteractionRequiredAuthError('consent_required', + 'AADSTS65001: The user or administrator has not consented to use the application.'); + const f = fixture(t, { ssoSilent: () => { throw failure; } }); + await assert.rejects(acquireToken(f.settings), error => + error instanceof SignInRequiredError && error.cause === failure && error.message === failure.message); +}); + +test('an unrelated cached account is not used for the current SharePoint user', async t => { + const f = fixture(t, { ssoSilent: () => ({ accessToken: 'current-user-token' }) }, [ + { username: 'someone-else@example.com' } + ]); + assert.equal(await acquireToken(f.settings), 'current-user-token'); + assert.deepEqual(f.methods(), ['initialize', 'ssoSilent']); +}); + +test('no account or login hint offers sign-in without an invalid silent SSO request', async t => { + const f = fixture(t); + delete f.settings.currentUserLogin; + await assert.rejects(acquireToken(f.settings), SignInRequiredError); + assert.deepEqual(f.methods(), ['initialize']); +}); + +test('failed MSAL initialization can be retried', async t => { + const failure = new Error('Initialization unavailable'); + let failed = true; + const f = fixture(t, { + initialize: () => { if (failed) throw failure; }, + ssoSilent: () => ({ accessToken: 'retry-token' }) + }); + await assert.rejects(acquireToken(f.settings), error => error === failure); + failed = false; + assert.equal(await acquireToken(f.settings), 'retry-token'); + assert.deepEqual(f.methods(), ['initialize', 'initialize', 'ssoSilent']); +}); diff --git a/ui/embed/sharepoint-customizer/tests/configuration.test.cjs b/ui/embed/sharepoint-customizer/tests/configuration.test.cjs new file mode 100644 index 00000000..6b6b32ad --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/configuration.test.cjs @@ -0,0 +1,63 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { normalizeConfiguration } = require('../temp/tests/configuration'); +const { createSideloadUrl, COMPONENT_ID } = require('../scripts/sideload.cjs'); +const properties = { appClientId: 'client', tenantId: 'tenant', environmentId: 'environment', schemaName: 'agent' }; + +test('discovery settings default uploads and typing on, details off', () => { + assert.deepEqual(normalizeConfiguration(properties), { + ...properties, directConnectUrl: undefined, startConversation: false, showTyping: true, enableFileUpload: true, + showReasoning: false, showToolCalls: false, + starterPrompts: [{ title: 'Get started', text: 'What can you help me with?' }] + }); +}); +test('legacy identifier remains supported, canonical schema takes precedence', () => { + assert.equal(normalizeConfiguration({ ...properties, schemaName: undefined, agentIdentifier: 'legacy' }).schemaName, 'legacy'); + assert.equal(normalizeConfiguration({ ...properties, agentIdentifier: 'legacy' }).schemaName, 'agent'); +}); +test('explicit direct URLs remain supported without environment or schema', () => { + assert.equal(normalizeConfiguration({ appClientId: 'c', tenantId: 't', directConnectUrl: 'https://example.com/bot' }).directConnectUrl, 'https://example.com/bot'); + assert.throws(() => normalizeConfiguration({ ...properties, directConnectUrl: 'http://example.com' }), /HTTPS/); +}); +test('missing properties and malformed flags fail visibly', () => { + assert.throws(() => normalizeConfiguration({ ...properties, tenantId: ' ' }), /tenantId/); + assert.throws(() => normalizeConfiguration({ ...properties, schemaName: '' }), /schemaName/); + assert.throws(() => normalizeConfiguration({ ...properties, showReasoning: 'false' }), /boolean|true or false/); +}); +test('debug URL preserves page parameters and round-trips JSON exactly', () => { + const config = { ...properties, agentTitle: 'Help & files #1', startConversation: true, showReasoning: true, showToolCalls: false, + starterPrompts: [{ title: 'A & B', text: 'Explain A & B.' }] }; + const url = new URL(createSideloadUrl('https://example.sharepoint.com/sites/dev/?view=1', config)); + assert.equal(url.searchParams.get('view'), '1'); + assert.equal(url.searchParams.get('loadSPFX'), 'true'); + assert.equal(url.searchParams.get('debugManifestsFile'), 'https://localhost:4321/temp/build/manifests.js'); + assert.deepEqual(JSON.parse(url.searchParams.get('customActions'))[COMPONENT_ID], { + location: 'ClientSideExtension.ApplicationCustomizer', properties: config + }); + assert.equal(JSON.parse(url.searchParams.get('customActions'))[COMPONENT_ID].properties.directConnectUrl, undefined); +}); +test('automatic conversation start is opt-in and requires a boolean', () => { + assert.equal(normalizeConfiguration(properties).startConversation, false); + assert.equal(normalizeConfiguration({ ...properties, startConversation: true }).startConversation, true); + assert.equal(normalizeConfiguration({ ...properties, startConversation: false }).startConversation, false); + for (const startConversation of [null, 'false', 0, {}]) { + assert.throws(() => normalizeConfiguration({ ...properties, startConversation }), /startConversation/); + assert.throws(() => createSideloadUrl('https://example.com', { ...properties, startConversation }), /startConversation/); + } +}); +test('starter prompts accept custom text or an empty list, and reject malformed or extra fields', () => { + assert.deepEqual(normalizeConfiguration({ ...properties, starterPrompts: [] }).starterPrompts, []); + assert.deepEqual(normalizeConfiguration({ ...properties, starterPrompts: [{ title: ' Start ', text: ' Help me ' }] }).starterPrompts, + [{ title: 'Start', text: 'Help me' }]); + for (const starterPrompts of [null, 'text', [{}], [{ title: 'Start', text: '' }], + [{ title: 'Start', text: 'Hello', clientSecret: 'not-allowed' }]]) { + assert.throws(() => normalizeConfiguration({ ...properties, starterPrompts }), /starter prompt|starterPrompts/); + assert.throws(() => createSideloadUrl('https://example.com', { ...properties, starterPrompts }), /starterPrompts/); + } +}); +test('debug URL rejects credentials, unknown properties, missing settings, and non-HTTPS manifests', () => { + assert.throws(() => createSideloadUrl('https://example.com', { ...properties, accessToken: 'secret' }), /Unknown property/); + assert.throws(() => createSideloadUrl('https://example.com', {}), /Provide/); + assert.throws(() => createSideloadUrl('https://user:password@example.com', properties), /credentials/); + assert.throws(() => createSideloadUrl('https://example.com', properties, 'http://localhost:4321/manifest.js'), /HTTPS/); +}); diff --git a/ui/embed/sharepoint-customizer/tests/connection.test.cjs b/ui/embed/sharepoint-customizer/tests/connection.test.cjs new file mode 100644 index 00000000..f3a53917 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/connection.test.cjs @@ -0,0 +1,271 @@ +const { test, mock } = require('node:test'); +const assert = require('node:assert/strict'); +const { Observable, BehaviorSubject, Subject, take } = require('rxjs'); +const { Activity } = require('@microsoft/agents-activity'); +const { CopilotStudioWebChat } = require('@microsoft/agents-copilotstudio-client'); +const { createConnection, wrapConnection } = require('../temp/tests/components/Chat/createConnection'); +const { normalizeConfiguration } = require('../temp/tests/configuration'); + +function fixture(postActivity, visibility = {}, initializeConversation) { + const received = new Subject(); + const failures = []; + let starts = 0; + let ends = 0; + let subscriptions = 0; + const base = { + connectionStatus$: new BehaviorSubject(2), + activity$: new Observable(subscriber => { + subscriptions++; + return received.subscribe(subscriber); + }), + postActivity, + end() { received.complete(); }, + conversationId: undefined + }; + const connection = wrapConnection(base, { + startConversation: false, enableFileUpload: true, showTyping: false, + showReasoning: false, showToolCalls: false, starterPrompts: [], ...visibility + }, { + onError(error) { failures.push(error.message); }, + onTurn() { starts++; }, + onTurnEnd() { ends++; } + }, initializeConversation); + return { connection, received, failures, counts: () => ({ starts, ends, subscriptions }) }; +} + +test('both opening modes pass their startConversation setting to the SDK', () => { + const base = { + activity$: new Subject(), connectionStatus$: new BehaviorSubject(2), + postActivity: () => new Observable(), end() {} + }; + const create = mock.method(CopilotStudioWebChat, 'createConnection', () => base); + try { + for (const startConversation of [undefined, true, false]) { + const configuration = normalizeConfiguration({ + appClientId: 'client', tenantId: 'tenant', environmentId: 'environment', schemaName: 'agent', startConversation + }); + const connection = createConnection({}, configuration, { onTurn() {}, onTurnEnd() {}, onError() {} }); + assert.deepEqual(create.mock.calls.at(-1).arguments[1], { showTyping: true, startConversation: startConversation === true }); + connection.end(); + } + } finally { + create.mock.restore(); + } +}); + +test('automatic start hides local starter cards without dropping the agent greeting', () => { + const f = fixture(() => new Observable(), { startConversation: true }); + const visible = []; + const subscription = f.connection.activity$.subscribe(activity => visible.push(activity)); + assert.deepEqual(visible, []); + f.received.next({ type: 'message', id: 'greeting', text: 'Welcome from the agent' }); + assert.deepEqual(visible.map(activity => activity.text), ['Welcome from the agent']); + subscription.unsubscribe(); + f.connection.end(); +}); + +test('response errors are still observed after WebChat takes the first send acknowledgement', async () => { + let response; + const f = fixture(() => new Observable(subscriber => { + response = subscriber; + subscriber.next('sent-id'); + })); + const acknowledgements = []; + f.connection.postActivity(new Activity('message')).pipe(take(1)).subscribe(id => acknowledgements.push(id)); + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(acknowledgements, ['sent-id']); + assert.equal(response.closed, false); + response.error(new Error('Service response failed')); + assert.deepEqual(f.failures, ['Service response failed']); + assert.equal(f.counts().ends, 1); + f.connection.end(); +}); + +test('single incoming subscription filters private detail events but preserves answer streaming', () => { + const f = fixture(() => new Observable()); + const visible = []; + const subscription = f.connection.activity$.subscribe(activity => visible.push(activity)); + f.received.next({ type: 'typing', text: 'private progress', channelData: { streamType: 'informative' } }); + f.received.next({ type: 'event', valueType: 'DynamicPlanStepTriggered', value: { arguments: 'private' } }); + f.received.next({ type: 'typing' }); + f.received.next({ type: 'typing', text: 'Partial answer', channelData: { streamType: 'streaming' } }); + f.received.next({ type: 'message', id: 'final', text: 'Final answer' }); + assert.equal(f.counts().subscriptions, 1); + assert.deepEqual(visible.map(activity => activity.text), ['', 'Partial answer', 'Final answer']); + subscription.unsubscribe(); + f.connection.end(); +}); + +test('ending the connection prevents late activity and releases outstanding response subscriptions', async () => { + let response; + const f = fixture(() => new Observable(subscriber => { + response = subscriber; + subscriber.next('sent'); + })); + f.connection.postActivity(new Activity('message')).pipe(take(1)).subscribe(); + await new Promise(resolve => setImmediate(resolve)); + f.connection.end(); + assert.equal(response.closed, true); + response.error(new Error('Late failure')); + assert.deepEqual(f.failures, []); +}); + +test('startup messages are replayed to the transcript with one SDK subscription and no private replay', () => { + const f = fixture(() => new Observable()); + const startup = f.connection.activity$.subscribe(); + f.received.next({ type: 'message', text: 'Welcome' }); + f.received.next({ type: 'typing', text: 'Progress', channelData: { streamType: 'informative' } }); + f.received.next({ type: 'event', name: 'turn.complete' }); + const transcript = []; + const subscription = f.connection.activity$.subscribe(activity => transcript.push(activity)); + assert.equal(f.counts().subscriptions, 1); + assert.deepEqual(transcript.map(activity => activity.text), ['', 'Welcome']); + assert.ok(transcript[0].id); + assert.ok(transcript[1].id); + startup.unsubscribe(); + f.received.next({ type: 'message', text: 'Still connected' }); + assert.equal(transcript.length, 3); + subscription.unsubscribe(); + f.connection.end(); +}); + +test('native activity groups finish when the SDK response completes without a turn.complete event', async () => { + let response; + const f = fixture(() => new Observable(subscriber => { + response = subscriber; + subscriber.next('sent-id'); + }), { showReasoning: true }); + const visible = []; + const subscription = f.connection.activity$.subscribe(activity => visible.push(activity)); + f.connection.postActivity(new Activity('message')).pipe(take(1)).subscribe(); + await new Promise(resolve => setImmediate(resolve)); + f.received.next({ + type: 'typing', entities: [{ type: 'thought', text: 'Checking the request', sequenceNumber: 0 }], + channelData: { streamType: 'streaming', streamId: 'thought-stream', streamSequence: 1, chunkType: 'delta' } + }); + response.complete(); + const details = visible.filter(activity => activity.id.includes(':reasoning:')); + assert.equal(details.length, 2); + assert.equal(details[0].entities[0].isPartOf.creativeWorkStatus, 'Incomplete'); + assert.equal(details[1].entities[0].isPartOf.creativeWorkStatus, 'Published'); + assert.equal(details[0].id, details[1].id); + subscription.unsubscribe(); + f.connection.end(); +}); + +test('response failure stops native progress without claiming success', async () => { + let response; + const f = fixture(() => new Observable(subscriber => { + response = subscriber; + subscriber.next('sent-id'); + }), { showToolCalls: true }); + const visible = []; + const subscription = f.connection.activity$.subscribe(activity => visible.push(activity)); + f.connection.postActivity(new Activity('message')).pipe(take(1)).subscribe(); + await new Promise(resolve => setImmediate(resolve)); + f.received.next({ type: 'event', valueType: 'DynamicPlanStepTriggered', value: { taskDialogId: 'lookup', stepId: 1 } }); + response.error(new Error('Response interrupted')); + const last = visible[visible.length - 1]; + assert.equal(last.entities[0].isPartOf.creativeWorkStatus, 'Failed'); + assert.equal(last.entities[0].creativeWorkStatus, undefined); + assert.equal(last.text, 'No finish event received.'); + assert.deepEqual(f.failures, ['Response interrupted']); + subscription.unsubscribe(); + f.connection.end(); +}); + +test('cancellation before acknowledgment releases the busy state and response subscription', async () => { + let response; + const f = fixture(() => new Observable(subscriber => { response = subscriber; })); + const subscription = f.connection.postActivity(new Activity('message')).subscribe(); + await new Promise(resolve => setImmediate(resolve)); + subscription.unsubscribe(); + assert.equal(response.closed, true); + assert.deepEqual(f.counts(), { starts: 1, ends: 1, subscriptions: 0 }); + f.connection.end(); +}); + +test('synchronous send failures are surfaced and release the busy state', async () => { + const f = fixture(() => { throw new Error('Cannot send'); }); + const failures = []; + f.connection.postActivity(new Activity('message')).subscribe({ error: error => failures.push(error.message) }); + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(failures, ['Cannot send']); + assert.deepEqual(f.failures, ['Cannot send']); + assert.equal(f.counts().ends, 1); + f.connection.end(); +}); + +test('startup failures use DirectLine failed status instead of an unhandled observable error', () => { + const f = fixture(() => new Observable()); + let completed = false; + f.connection.activity$.subscribe({ complete: () => { completed = true; } }); + f.received.error(new Error('Discovery unavailable')); + assert.deepEqual(f.failures, ['Discovery unavailable']); + assert.equal(f.connection.connectionStatus$.value, 4); + assert.equal(completed, true); + f.connection.end(); +}); + +test('attachment preparation holds busy state and cancellation prevents a late send', async t => { + let resolveRead; + t.mock.method(globalThis, 'fetch', () => new Promise(resolve => { resolveRead = resolve; })); + let sends = 0; + const f = fixture(() => { sends++; return new Observable(); }); + const subscription = f.connection.postActivity({ + type: 'message', attachments: [{ name: 'notes.txt', contentType: 'text/plain', contentUrl: 'blob:pending' }] + }).subscribe(); + assert.equal(f.counts().starts, 1); + subscription.unsubscribe(); + assert.equal(f.counts().ends, 1); + resolveRead(new Response('fixture')); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(sends, 0); + assert.deepEqual(f.failures, []); + f.connection.end(); +}); + +test('attachment validation failure is reported and releases busy state without sending', async () => { + let initialized = false; + const f = fixture(() => { throw new Error('Must not send'); }, {}, async () => { initialized = true; }); + f.connection.postActivity({ + type: 'message', attachments: [{ name: 'notes.txt', contentType: 'text/plain', contentUrl: 'https://example.com/file' }] + }).subscribe({ error() {} }); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(f.counts().ends, 1); + assert.equal(initialized, false); + assert.match(f.failures[0], /local file/); + f.connection.end(); +}); + +test('lazy initialization failure is reported and releases busy state without sending the message', async () => { + let sends = 0; + const f = fixture(() => { sends++; return new Observable(); }, {}, async () => { throw new Error('Cannot create conversation'); }); + const failures = []; + f.connection.postActivity(new Activity('message')).subscribe({ error: error => failures.push(error.message) }); + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(failures, ['Cannot create conversation']); + assert.deepEqual(f.failures, failures); + assert.equal(f.counts().ends, 1); + assert.equal(sends, 0); + f.connection.end(); +}); + +for (const closeConnection of [false, true]) { + test(`cancellation during lazy initialization prevents a late send (closeConnection=${closeConnection})`, async () => { + let initialize; + let sends = 0; + const f = fixture(() => { sends++; return new Observable(); }, {}, () => new Promise(resolve => { initialize = resolve; })); + const subscription = f.connection.postActivity(new Activity('message')).subscribe(); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(f.counts().starts, 1); + if (closeConnection) f.connection.end(); + else subscription.unsubscribe(); + initialize(); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(sends, 0); + assert.deepEqual(f.failures, []); + subscription.unsubscribe(); + f.connection.end(); + }); +} diff --git a/ui/embed/sharepoint-customizer/tests/fixtures/sdkActivities.cjs b/ui/embed/sharepoint-customizer/tests/fixtures/sdkActivities.cjs new file mode 100644 index 00000000..4510c473 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/fixtures/sdkActivities.cjs @@ -0,0 +1,36 @@ +// Synthetic content using the entity/chunk shapes observed from the published SDK stream. +function thought(text, streamSequence = 1, chunkType = 'delta', properties = {}) { + const stream = { + streamType: 'streaming', streamSequence, + ...(streamSequence === 1 ? {} : { streamId: 'answer-stream' }) + }; + return { + type: 'typing', id: streamSequence === 1 ? 'answer-stream' : `answer-stream-${streamSequence - 1}`, + from: { id: 'bot', role: 'bot' }, text: '', + entities: [{ + type: 'thought', title: '', sequenceNumber: 0, status: 'complete', text, + reasonedForSeconds: 0, chainOfThoughtId: '', ...properties + }, { type: 'streaminfo', ...stream }], + channelData: { ...stream, chunkType, 'webchat:sequence-id': streamSequence } + }; +} + +function toolCall(status, { toolCallId = 'call', streamSequence = status === 'started' ? 12 : 13, ...properties } = {}) { + const stream = { streamType: 'informative', streamId: 'answer-stream', streamSequence }; + return { + type: 'typing', id: `answer-stream-${streamSequence - 1}`, + from: { id: 'bot', role: 'bot' }, text: status === 'started' ? 'Calling Get items...' : 'Get items completed', + entities: [{ + type: 'toolCall', toolCallId, toolName: 'get_items', toolDisplayName: 'Get items', + operationId: 'GetItems', connectorId: '/providers/Microsoft.PowerApps/apis/shared_sharepointonline', + componentSchemaName: 'sample.tool.GetItems', toolCategory: 'Connector', toolKind: 'tool', status, + description: 'PRIVATE_DESCRIPTION', + filledParameters: { value: 'PRIVATE_INPUT' }, hiddenFilledParameters: { value: 'PRIVATE_HIDDEN_INPUT' }, + result: { value: 'PRIVATE_RESULT' }, thought: 'PRIVATE_PLANNER_THOUGHT', + ...properties + }, { type: 'streaminfo', ...stream }], + channelData: { ...stream, 'webchat:sequence-id': streamSequence } + }; +} + +module.exports = { thought, toolCall }; diff --git a/ui/embed/sharepoint-customizer/tests/lazyStart.test.cjs b/ui/embed/sharepoint-customizer/tests/lazyStart.test.cjs new file mode 100644 index 00000000..325675f2 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/lazyStart.test.cjs @@ -0,0 +1,61 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { filter, firstValueFrom, lastValueFrom, timeout } = require('rxjs'); +const { Activity } = require('@microsoft/agents-activity'); +const { ConnectionSettings, CopilotStudioClient } = require('@microsoft/agents-copilotstudio-client'); +const { normalizeConfiguration } = require('../temp/tests/configuration'); +const { createConnection } = require('../temp/tests/components/Chat/createConnection'); + +test('lazy start creates a conversation silently before executing the first message, then reuses its header-only ID', async t => { + const requests = []; + const host = 'https://000000000000000000000000000000.01.environment.api.powerplatform.com'; + t.mock.method(globalThis, 'fetch', async (url, options) => { + const path = new URL(url).pathname; + if (path.endsWith('/conversation-endpoints')) { + requests.push({ operation: 'discovery' }); + return Response.json({ + createConversation: `${host}/conversations`, + executeTurn: `${host}/conversations/{conversationId}`, + continueTurn: `${host}/conversations/{conversationId}/continue` + }); + } + const body = JSON.parse(options.body); + const create = path === '/conversations'; + assert.ok(create || path === '/conversations/header-only-id', `Unexpected URL: ${url}`); + requests.push({ operation: create ? 'create' : 'execute', body }); + // A create endpoint establishes a session; it does not execute an activity in the request. + const activity = create + ? { type: 'typing', from: { id: 'agent', role: 'bot' } } + : { type: 'message', id: `answer-${requests.length}`, from: { id: 'agent', role: 'bot' }, text: `Reply: ${body.activity.text}` }; + return new Response(`event: activity\ndata: ${JSON.stringify(activity)}\n\nevent: end\ndata: done\n\n`, { + headers: { 'content-type': 'text/event-stream', 'x-ms-conversationid': 'header-only-id' } + }); + }); + const configuration = normalizeConfiguration({ + appClientId: 'client', tenantId: 'tenant', + environmentId: '00000000-0000-0000-0000-000000000001', schemaName: 'fixture' + }); + const client = new CopilotStudioClient(new ConnectionSettings(configuration), 'fixture-token'); + const failures = []; + const connection = createConnection(client, configuration, { + onTurn() {}, onTurnEnd() {}, onError(error) { failures.push(error.message); } + }); + t.after(() => connection.end()); + const received = []; + const subscription = connection.activity$.subscribe(activity => received.push(activity)); + t.after(() => subscription.unsubscribe()); + await firstValueFrom(connection.connectionStatus$.pipe(filter(status => status === 2), timeout(2000))); + assert.deepEqual(requests, [], 'Opening the panel must not start a remote conversation'); + const send = text => lastValueFrom(connection.postActivity(Activity.fromObject({ + type: 'message', text, from: { id: 'user', role: 'user' } + })).pipe(timeout(2000))); + await send('First question'); + assert.deepEqual(requests.map(request => request.operation), ['discovery', 'create', 'execute']); + assert.deepEqual(requests[1].body, { emitStartConversationEvent: false }); + assert.equal(requests[2].body.activity.text, 'First question'); + assert.ok(received.some(activity => activity.text === 'Reply: First question')); + await send('Follow-up'); + assert.deepEqual(requests.map(request => request.operation), ['discovery', 'create', 'execute', 'execute']); + assert.ok(received.some(activity => activity.text === 'Reply: Follow-up')); + assert.deepEqual(failures, []); +}); diff --git a/ui/embed/sharepoint-customizer/tests/nativeActivities.test.cjs b/ui/embed/sharepoint-customizer/tests/nativeActivities.test.cjs new file mode 100644 index 00000000..ec5a05a7 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/nativeActivities.test.cjs @@ -0,0 +1,132 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { normalizeConfiguration } = require('../temp/tests/configuration'); +const { createPreChatActivity, NativeActivityAdapter } = require('../temp/tests/components/Chat/nativeActivities'); +const { thought, toolCall } = require('./fixtures/sdkActivities.cjs'); + +const configuration = overrides => normalizeConfiguration({ + appClientId: 'client', tenantId: 'tenant', environmentId: 'environment', schemaName: 'agent', ...overrides +}); +const tool = (valueType, value = {}) => ({ + type: 'event', valueType, value: { taskDialogId: 'lookup', stepId: 'call', ...value } +}); + +test('welcome uses the native Fluent PreChatMessage and editable messageBack starter contract', () => { + const activity = createPreChatActivity(configuration()); + assert.equal(activity.type, 'message'); + assert.equal(activity.entities[0].type, 'https://schema.org/Message'); + assert.deepEqual(activity.entities[0].keywords, ['PreChatMessage']); + assert.equal(activity.entities[0].author.name, 'How can I help?'); + assert.ok(activity.entities[0].author.image.startsWith('data:image/svg+xml,')); + assert.equal(activity.channelData['webchat:sequence-id'], -1); + assert.deepEqual(activity.suggestedActions.to, []); + assert.deepEqual(activity.suggestedActions.actions, [ + { type: 'messageBack', title: 'Get started', text: 'What can you help me with?' } + ]); + const withoutFiles = createPreChatActivity(configuration({ enableFileUpload: false, starterPrompts: [] })); + assert.equal(withoutFiles.entities[0].author.description, 'Ask a question to get started.'); + assert.deepEqual(withoutFiles.suggestedActions.actions, []); +}); + +for (const showReasoning of [false, true]) { + for (const showToolCalls of [false, true]) { + test(`native groups respect reasoning=${showReasoning}, tools=${showToolCalls} without exposing raw events`, () => { + const adapter = new NativeActivityAdapter(configuration({ showReasoning, showToolCalls })); + adapter.startTurn(); + const activities = [ + ...adapter.transform(thought('Checking sources')), + ...adapter.transform(tool('DynamicPlanReceived', { + toolDefinitions: [{ schemaName: 'lookup', displayName: 'Search reference material' }] + })), + ...adapter.transform(tool('DynamicPlanStepTriggered', { arguments: 'PRIVATE_ARGUMENT', thought: 'PRIVATE_THOUGHT' })), + ...adapter.transform(tool('DynamicPlanStepBindUpdate', { arguments: 'PRIVATE_BINDING' })), + ...adapter.transform(tool('DynamicPlanStepFinished', { result: 'PRIVATE_RESULT' })) + ]; + assert.equal(activities.length, Number(showReasoning) + Number(showToolCalls) * 2); + assert.ok(!JSON.stringify(activities).includes('PRIVATE_')); + for (const activity of activities) { + assert.equal(activity.type, 'message'); + assert.equal(activity.entities[0]['@id'], ''); + assert.equal(activity.entities[0].isPartOf['@type'], 'HowTo'); + assert.ok(activity.entities[0].keywords.includes('Collapsible')); + assert.ok(!('value' in activity)); + } + if (showToolCalls) { + const last = activities[activities.length - 1]; + assert.equal(last.entities[0].abstract, 'Search reference material'); + assert.equal(last.text, 'Finished'); + } + }); + } +} + +test('native progress updates keep their IDs and order, finish once, and do not overwrite another turn', () => { + const adapter = new NativeActivityAdapter(configuration({ showReasoning: true })); + adapter.startTurn(); + const [first] = adapter.transform(thought('Checking')); + const [second] = adapter.transform(thought('Checking sources', 2, 'replace')); + assert.equal(first.id, second.id); + assert.equal(typeof first.timestamp, 'string'); + assert.equal(first.timestamp, second.timestamp); + assert.equal(first.channelData['webchat:sequence-id'], 1); + assert.equal(first.channelData['webchat:sequence-id'], second.channelData['webchat:sequence-id']); + assert.equal(second.text, 'Checking sources'); + assert.deepEqual(adapter.transform(thought('old', 1)), []); + const [finished] = adapter.transform({ type: 'event', name: 'turn.complete' }); + assert.equal(finished.id, first.id); + assert.equal(finished.entities[0].creativeWorkStatus, 'Published'); + assert.equal(finished.entities[0].isPartOf.creativeWorkStatus, 'Published'); + assert.deepEqual(adapter.finishTurn(), []); + adapter.startTurn(); + const [nextTurn] = adapter.transform(thought('Checking again')); + assert.notEqual(nextTurn.id, first.id); + assert.notEqual(nextTurn.entities[0].isPartOf['@id'], first.entities[0].isPartOf['@id']); +}); + +test('ordinary replies remain untouched and native analysis respects the reasoning flag', () => { + const adapter = new NativeActivityAdapter(configuration()); + const reply = { type: 'message', text: 'The answer', from: { id: 'bot', role: 'bot' } }; + assert.strictEqual(adapter.transform(reply)[0], reply); + const analysis = { ...reply, entities: [{ type: 'https://schema.org/Message', keywords: ['AnalysisMessage'] }] }; + assert.deepEqual(adapter.transform(analysis), []); + assert.strictEqual(new NativeActivityAdapter(configuration({ showReasoning: true })).transform(analysis)[0], analysis); +}); + +test('turn completion does not invent a tool-finished event', () => { + const adapter = new NativeActivityAdapter(configuration({ showToolCalls: true })); + adapter.transform(tool('DynamicPlanStepTriggered')); + const [finished] = adapter.finishTurn(); + assert.equal(finished.text, 'No finish event received.'); + assert.equal(finished.entities[0].creativeWorkStatus, undefined); +}); + +test('toolCall metadata produces a named native tool step, not a Reasoning step', () => { + const adapter = new NativeActivityAdapter(configuration({ showToolCalls: true })); + const [started] = adapter.transform(toolCall('started')); + const [finished] = adapter.transform(toolCall('completed')); + assert.equal(started.id, finished.id); + assert.equal(started.channelData['webchat:sequence-id'], finished.channelData['webchat:sequence-id']); + assert.equal(finished.entities[0].abstract, 'Get items'); + assert.equal(finished.text, 'Finished'); + assert.equal(finished.entities[0].creativeWorkStatus, 'Published'); + assert.ok(!JSON.stringify([started, finished]).includes('PRIVATE_')); +}); + +test('failed toolCall entities remain failed when the turn finishes', () => { + const adapter = new NativeActivityAdapter(configuration({ showToolCalls: true })); + adapter.transform(toolCall('started')); + const [failed] = adapter.transform(toolCall('failed')); + const [finished] = adapter.finishTurn(); + assert.equal(failed.text, 'Failed'); + assert.equal(failed.entities[0].creativeWorkStatus, 'Failed'); + assert.equal(finished.text, 'Failed'); + assert.equal(finished.entities[0].creativeWorkStatus, 'Failed'); +}); + +test('unclassified informative updates are not mislabeled or replayed into the transcript', () => { + const adapter = new NativeActivityAdapter(configuration({ showReasoning: true, showToolCalls: true })); + assert.deepEqual(adapter.transform({ type: 'typing', text: 'Working', channelData: { streamType: 'informative' } }), []); + assert.deepEqual(adapter.transform({ + type: 'typing', text: 'Working', entities: [{ type: 'streaminfo', streamType: 'informative' }] + }), []); +}); diff --git a/ui/embed/sharepoint-customizer/tests/sdkActivityMapping.test.cjs b/ui/embed/sharepoint-customizer/tests/sdkActivityMapping.test.cjs new file mode 100644 index 00000000..8aecd531 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/sdkActivityMapping.test.cjs @@ -0,0 +1,75 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { firstValueFrom, lastValueFrom, filter, timeout } = require('rxjs'); +const { Activity } = require('@microsoft/agents-activity'); +const { ConnectionSettings, CopilotStudioClient } = require('@microsoft/agents-copilotstudio-client'); +const { normalizeConfiguration } = require('../temp/tests/configuration'); +const { createConnection } = require('../temp/tests/components/Chat/createConnection'); +const { thought, toolCall } = require('./fixtures/sdkActivities.cjs'); + +for (const showReasoning of [false, true]) { + for (const showToolCalls of [false, true]) { + test(`real SDK entity mapping: reasoning=${showReasoning}, tools=${showToolCalls}`, async t => { + const host = 'https://000000000000000000000000000000.01.environment.api.powerplatform.com'; + const stream = { streamType: 'streaming', streamId: 'answer-stream', chunkType: 'delta' }; + t.mock.method(globalThis, 'fetch', async url => { + const path = new URL(url).pathname; + if (path.endsWith('/conversation-endpoints')) { + return Response.json({ + createConversation: `${host}/conversations`, + executeTurn: `${host}/conversations/{conversationId}`, + continueTurn: `${host}/conversations/{conversationId}/continue` + }); + } + assert.ok(path === '/conversations' || path === '/conversations/header-only-id'); + const activities = path === '/conversations' ? [{ type: 'typing' }] : [ + thought('Check ', 1), thought('the list', 2), thought('Check the list', 3, 'replace'), + toolCall('started', { streamSequence: 4 }), toolCall('completed', { streamSequence: 5 }), + { type: 'typing', id: 'answer-part-1', text: 'Here are ', channelData: { ...stream, streamSequence: 6 } }, + { type: 'typing', id: 'answer-part-2', text: 'the items.', channelData: { ...stream, streamSequence: 7 } }, + { type: 'message', id: 'answer', text: 'Here are the items.', channelData: { streamType: 'final', streamId: 'answer-stream' } }, + { type: 'event', name: 'turn.complete' } + ]; + return new Response(activities.map(activity => `event: activity\ndata: ${JSON.stringify(activity)}\n\n`).join('') + + 'event: end\ndata: done\n\n', { + headers: { 'content-type': 'text/event-stream', 'x-ms-conversationid': 'header-only-id' } + }); + }); + const configuration = normalizeConfiguration({ + appClientId: 'client', tenantId: 'tenant', + environmentId: '00000000-0000-0000-0000-000000000001', schemaName: 'fixture', + showReasoning, showToolCalls + }); + const failures = []; + const connection = createConnection(new CopilotStudioClient(new ConnectionSettings(configuration), 'fixture-token'), configuration, { + onTurn() {}, onTurnEnd() {}, onError(error) { failures.push(error.message); } + }); + const received = []; + const subscription = connection.activity$.subscribe(activity => received.push(activity)); + t.after(() => { subscription.unsubscribe(); connection.end(); }); + await firstValueFrom(connection.connectionStatus$.pipe(filter(status => status === 2), timeout(2000))); + await lastValueFrom(connection.postActivity(Activity.fromObject({ + type: 'message', text: 'List items', from: { id: 'user', role: 'user' } + })).pipe(timeout(2000))); + + const reasoning = received.filter(activity => activity.id?.includes(':reasoning:')); + const tools = received.filter(activity => activity.id?.includes(':tool:')); + assert.equal(new Set(reasoning.map(activity => activity.id)).size, Number(showReasoning)); + assert.equal(new Set(tools.map(activity => activity.id)).size, Number(showToolCalls)); + if (showReasoning) { + assert.equal(reasoning.at(-1).text, 'Check the list'); + assert.equal(reasoning.at(-1).entities[0].abstract, 'Reasoning'); + } + if (showToolCalls) { + assert.equal(tools.at(-1).entities[0].abstract, 'Get items'); + assert.equal(tools.at(-1).text, 'Finished'); + } + assert.ok(received.some(activity => activity.type === 'typing' && activity.text === 'Here are the items.')); + assert.ok(received.some(activity => activity.id === 'answer' && activity.text === 'Here are the items.')); + assert.ok(!received.some(activity => activity.text === 'Get items completed')); + assert.ok(!JSON.stringify(received).includes('PRIVATE_')); + assert.ok(!received.some(activity => activity.entities?.some(entity => ['thought', 'toolCall'].includes(entity.type)))); + assert.deepEqual(failures, []); + }); + } +} diff --git a/ui/embed/sharepoint-customizer/tsconfig.json b/ui/embed/sharepoint-customizer/tsconfig.json index 419cd861..d697c99c 100644 --- a/ui/embed/sharepoint-customizer/tsconfig.json +++ b/ui/embed/sharepoint-customizer/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./node_modules/@microsoft/rush-stack-compiler-5.3/includes/tsconfig-web.json", "compilerOptions": { - "target": "es5", + "target": "es2019", "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node", @@ -22,7 +22,7 @@ "webpack-env" ], "lib": [ - "es5", + "es2019", "dom", "es2015.collection", "es2015.promise" diff --git a/ui/embed/sharepoint-customizer/tsconfig.test.json b/ui/embed/sharepoint-customizer/tsconfig.test.json new file mode 100644 index 00000000..ba86cf08 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tsconfig.test.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "DOM"], + "strict": true, + "skipLibCheck": true, + "outDir": "temp/tests", + "rootDir": "src/extensions/sidebarAgent", + "types": [] + }, + "include": [ + "src/extensions/sidebarAgent/configuration.ts", + "src/extensions/sidebarAgent/components/Chat/acquireToken.ts", + "src/extensions/sidebarAgent/components/Chat/attachments.ts", + "src/extensions/sidebarAgent/components/Chat/activityDetails.ts", + "src/extensions/sidebarAgent/components/Chat/nativeActivities.ts", + "src/extensions/sidebarAgent/components/Chat/createConnection.ts" + ] +} From 35b63ca694ab149c6a00248db1a19c5efe213958 Mon Sep 17 00:00:00 2001 From: adilei Date: Fri, 23 Oct 2026 17:56:04 +0300 Subject: [PATCH 2/7] Map standard harness reasoning and tools to native Web Chat Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 940e4d9a-9134-45e2-888a-1d9c5ef96c58 --- ui/embed/sharepoint-customizer/README.md | 22 ++-- .../components/Chat/activityDetails.ts | 112 +++++++++++------- .../components/Chat/nativeActivities.ts | 7 +- .../tests/activityDetails.test.cjs | 84 ++++++++++++- .../tests/fixtures/sdkActivities.cjs | 25 +++- .../tests/nativeActivities.test.cjs | 47 +++++++- .../tests/sdkActivityMapping.test.cjs | 94 ++++++++++++++- 7 files changed, 328 insertions(+), 63 deletions(-) diff --git a/ui/embed/sharepoint-customizer/README.md b/ui/embed/sharepoint-customizer/README.md index d6e5303b..928e271e 100644 --- a/ui/embed/sharepoint-customizer/README.md +++ b/ui/embed/sharepoint-customizer/README.md @@ -49,7 +49,7 @@ On the first submitted message, the adapter uses `startConversationStreaming(fal Setting `startConversation: true` instead performs discovery and starts the conversation on opening, without showing the sample's starter cards. The sample does not construct agentic-runtime routes or start a separate subscription connection. The SDK retains conversation IDs, including IDs returned only in response headers. -A small adapter around `CopilotStudioWebChat.createConnection` validates local attachments before sending and translates client-visible `thought` and `toolCall` entities, plus supported legacy tool events, into WebChat's native Schema.org `Message`/`HowTo` activities. WebChat's Fluent pre-chat and collapsible part-group components render the welcome screen, starter cards, and activity groups; there are no custom reasoning/tool React components. A shared incoming subscription replays activities so WebChat's startup and transcript subscriptions do not lose greetings. Raw tool metadata, arguments, results, and embedded planner thoughts are never replayed into WebChat. Send acknowledgements do not end response monitoring: later response errors are still surfaced, and the composer stays disabled during file preparation and the response to prevent overlapping turns. +A small adapter around `CopilotStudioWebChat.createConnection` validates local attachments before sending and normalizes two payload families: GHCP/agentic-loop `thought`/`toolCall` entities and standard-harness `DynamicPlan*` events. Both map to WebChat's native Schema.org `Message`/`HowTo` activities. WebChat's Fluent pre-chat and collapsible part-group components render the welcome screen, starter cards, and activity groups; there are no custom reasoning/tool React components or harness-selection settings. A shared incoming subscription replays activities so WebChat's startup and transcript subscriptions do not lose greetings. Only allowlisted reasoning text and tool labels/status are replayed; raw events, arguments, and results are not. Send acknowledgements do not end response monitoring: later response errors are still surfaced, and the composer stays disabled during file preparation and the response to prevent overlapping turns. The Fluent **copilot** variant and SharePoint theme colors provide the presentation. WebChat's style options are memoized across turn-state updates so response completion does not reset transcript scrolling; theme or upload-setting changes still refresh them. The sample retains released WebChat `4.19.2` and React `17`, compatible with this SPFx version. @@ -207,16 +207,24 @@ Do not upload sensitive files merely to test the sample. Earlier uploaded conten **Agent activity** uses WebChat's native collapsible part groups, closed by default. Steps update in place while the response runs and remain in the transcript after it finishes. Each turn has its own group; WebChat owns rendering, focus, status icons, and expansion. -- `showReasoning` displays client-visible `entities[].type: "thought"` content from the SDK. Text comes from the entity's `text`, not the activity's answer text. Explicit `chunkType: "delta"` chunks append; replacement/cumulative snapshots replace. Thought chain/step identifiers keep separate thoughts apart, with the stream ID (or the initial activity ID) used when the chain ID is empty. Ordinary answer streaming is untouched. -- Native Schema.org messages tagged `AnalysisMessage` also respect `showReasoning`. The adapter uses WebChat's [documented reasoning activity format](https://github.com/microsoft/BotFramework-WebChat/blob/v4.19.2/docs/CHAIN_OF_THOUGHTS.md); it does not request hidden model chain-of-thought. -- `showToolCalls` displays `entities[].type: "toolCall"` as a named tool step. `toolCallId` correlates invocations, `toolDisplayName` provides the label, and `status` supplies started/completed/failed state. Multiple calls sharing one stream remain separate. -- Legacy `DynamicPlanReceived`, `DynamicPlanStepTriggered`, and `DynamicPlanStepFinished` event `valueType`s remain supported. Definitions provide display names; step identifiers correlate lifecycle updates. +The adapter selects the mapping from the incoming payload, not the agent's URL or display name. Both flags remain independent and default to `false`. + +| Runtime payload | `showReasoning` | `showToolCalls` | +|-----------------|-----------------|-----------------| +| GHCP / agentic loop | `entities[].type: "thought"` uses the entity's `text` | `entities[].type: "toolCall"` uses its display name and started/completed/failed status | +| Standard harness | `DynamicPlanStepTriggered.value.thought` becomes a per-step reasoning entry | `DynamicPlanReceived` provides names; `DynamicPlanStepTriggered` and `DynamicPlanStepBindUpdate` identify ongoing steps; `DynamicPlanStepFinished` finishes them | + +- GHCP thought deltas append only for explicit `chunkType: "delta"`; replacement/cumulative snapshots replace. Thought chain/step identifiers keep separate thoughts apart, with the stream ID (or the initial activity ID) used when the chain ID is empty. `toolCallId` correlates tools even when multiple invocations share one stream. +- Standard events use `valueType`, falling back to `name` when absent. Their top-level step `thought` is the planning explanation the published runtime sends to the client, not a token-by-token model reasoning stream. It is treated as a snapshot, keyed by plan and step; repeated updates replace rather than append. Tool display names survive clarification or connector-sign-in turns within the conversation. +- Native Schema.org messages tagged `AnalysisMessage` also respect `showReasoning`. The adapter uses WebChat's [documented reasoning activity format](https://github.com/microsoft/BotFramework-WebChat/blob/v4.19.2/docs/CHAIN_OF_THOUGHTS.md); it does not request hidden model chain-of-thought or retrieve authoring-only traces. - `streamType: "informative"` alone is **not** evidence of reasoning. Tool notifications can use it too. Unclassified informative updates are omitted rather than guessed into a reasoning/tool entry. -- Tool arguments, filled/hidden parameters, descriptions, results, and embedded planner `thought` fields are not displayed. Only explicitly client-visible `thought` entities are used for reasoning; the sample does not retrieve authoring-only traces. +- Tool arguments, filled/hidden parameters, descriptions, results, and arbitrary nested `thought` fields are not displayed. Standard reasoning is read only from the top-level `value.thought` of the supported step-trigger event. Binding updates never expose their arguments. - Unknown events are not guessed into a tool call. A flag being on cannot make an agent emit missing events, and these event families are not guaranteed across all models/harnesses. -- **Started** means a start event was received; **Finished** means a finish event was received, not necessarily that the operation succeeded. +- **Started** means tool-step start or binding activity was received; it does not prove that a connector has executed. **Finished** means a finish event was received, not necessarily that the operation succeeded. - The group completes when the turn finishes. Missing tool-finish events are explicitly labeled rather than invented; failed responses mark the group as interrupted. +Standard-harness plans can pause for clarification or a connector connection before executing a tool. A question or connection card is not evidence of tool completion. Regression tests cover both mappings and all four visibility combinations; verify successful connector execution separately in your environment. + Choose safe, read-only agent actions for review. Do not trigger a business-changing tool just to demonstrate the disclosure. ## Build and package diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/activityDetails.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/activityDetails.ts index 1b5ce1bc..52538942 100644 --- a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/activityDetails.ts +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/activityDetails.ts @@ -49,54 +49,69 @@ export function hasDetailEntities(activity: unknown): boolean { return entities(record(activity)).some(entity => entity.type === 'thought' || entity.type === 'toolCall'); } -export function isToolEvent(activity: unknown): boolean { - const item = record(activity); - return item.type === 'event' && [ +function planEventType(activity: Record): string { + return activity.type === 'event' ? text(activity.valueType) || text(activity.name) : ''; +} + +export function isPlanEvent(activity: unknown): boolean { + return [ 'DynamicPlanReceived', 'DynamicPlanStepTriggered', 'DynamicPlanStepBindUpdate', 'DynamicPlanStepFinished' - ].indexOf(text(item.valueType)) !== -1; + ].indexOf(planEventType(record(activity))) !== -1; } export function updateActivityDetails( state: ActivityDetailsState, activity: unknown, visibility: ActivityVisibility ): ActivityDetailsState { const item = record(activity); + if (hasDetailEntities(item)) return updateAgenticDetails(state, item, visibility); + if (isPlanEvent(item)) return updateStandardDetails(state, item, visibility); + return state; +} + +function updateAgenticDetails( + state: ActivityDetailsState, item: Record, visibility: ActivityVisibility +): ActivityDetailsState { const stream = streamMetadata(item); const sequence = typeof stream.streamSequence === 'number' ? stream.streamSequence : undefined; - if (hasDetailEntities(item)) { - let next = state; - for (const entity of entities(item)) { - if (entity.type === 'thought' && visibility.showReasoning) { - const content = text(entity.text); - if (!content) continue; - const chain = identifier(entity.chainOfThoughtId) || text(stream.streamId) || text(item.id) || 'current'; - const id = `reasoning:${chain}:${identifier(entity.sequenceNumber) || '0'}`; - const previous = next.entries.find(entry => entry.id === id); - if (previous?.sequence !== undefined && sequence !== undefined && sequence <= previous.sequence) continue; - // The SDK accumulates activity.text, not thought.text; thought deltas still need assembly. - const nextText = stream.chunkType === 'delta' ? (previous?.text || '') + content : content; - const entry: ActivityDetail = { id, kind: 'reasoning', text: nextText, sequence }; - next = { ...next, entries: upsert(next.entries, entry) }; - } else if (entity.type === 'toolCall' && visibility.showToolCalls) { - const callId = identifier(entity.toolCallId); - const status = entity.status === 'completed' ? 'finished' - : entity.status === 'started' ? 'running' : entity.status === 'failed' ? 'failed' : undefined; - if (!callId || !status) continue; - const id = `tool:${callId}`; - const previous = next.entries.find(entry => entry.id === id); - if (previous?.sequence !== undefined && sequence !== undefined && sequence <= previous.sequence) continue; - if (previous?.status && previous.status !== 'running' && status === 'running') continue; - const entry: ActivityDetail = { - id, kind: 'tool', status, sequence, - text: text(entity.toolDisplayName) || text(entity.toolName) || text(entity.operationId) || previous?.text || 'Agent action' - }; - next = { ...next, entries: upsert(next.entries, entry) }; - } + let next = state; + for (const entity of entities(item)) { + if (entity.type === 'thought' && visibility.showReasoning) { + const content = text(entity.text); + if (!content) continue; + const chain = identifier(entity.chainOfThoughtId) || text(stream.streamId) || text(item.id) || 'current'; + const id = `reasoning:${chain}:${identifier(entity.sequenceNumber) || '0'}`; + const previous = next.entries.find(entry => entry.id === id); + if (previous?.sequence !== undefined && sequence !== undefined && sequence <= previous.sequence) continue; + // The SDK accumulates activity.text, not thought.text; thought deltas still need assembly. + const nextText = stream.chunkType === 'delta' ? (previous?.text || '') + content : content; + const entry: ActivityDetail = { id, kind: 'reasoning', text: nextText, sequence }; + next = { ...next, entries: upsert(next.entries, entry) }; + } else if (entity.type === 'toolCall' && visibility.showToolCalls) { + const callId = identifier(entity.toolCallId); + const status = entity.status === 'completed' ? 'finished' + : entity.status === 'started' ? 'running' : entity.status === 'failed' ? 'failed' : undefined; + if (!callId || !status) continue; + const id = `tool:${callId}`; + const previous = next.entries.find(entry => entry.id === id); + if (previous?.sequence !== undefined && sequence !== undefined && sequence <= previous.sequence) continue; + if (previous?.status && previous.status !== 'running' && status === 'running') continue; + const entry: ActivityDetail = { + id, kind: 'tool', status, sequence, + text: text(entity.toolDisplayName) || text(entity.toolName) || text(entity.operationId) || previous?.text || 'Agent action' + }; + next = { ...next, entries: upsert(next.entries, entry) }; } - return next; } - if (!visibility.showToolCalls || !isToolEvent(item)) return state; + return next; +} + +function updateStandardDetails( + state: ActivityDetailsState, item: Record, visibility: ActivityVisibility +): ActivityDetailsState { + const event = planEventType(item); const value = record(item.value); - if (item.valueType === 'DynamicPlanReceived' && Array.isArray(value.toolDefinitions)) { + if (event === 'DynamicPlanReceived') { + if (!visibility.showToolCalls || !Array.isArray(value.toolDefinitions)) return state; const toolNames = { ...state.toolNames }; value.toolDefinitions.forEach(definition => { const tool = record(definition); @@ -105,19 +120,30 @@ export function updateActivityDetails( }); return { ...state, toolNames }; } - if (item.valueType !== 'DynamicPlanStepTriggered' && item.valueType !== 'DynamicPlanStepFinished') return state; const schema = text(value.taskDialogId); const step = identifier(value.stepId) || identifier(value.id) || schema; if (!step) return state; - const id = `tool:${step}`; - const previous = state.entries.find(entry => entry.id === id); - if (previous?.status === 'finished' && item.valueType === 'DynamicPlanStepTriggered') return state; + const scope = JSON.stringify([identifier(value.planIdentifier), step]); + let next = state; + const thought = text(value.thought); + if (visibility.showReasoning && event === 'DynamicPlanStepTriggered' && thought.trim()) { + const entry: ActivityDetail = { id: `reasoning:standard:${scope}`, kind: 'reasoning', text: thought }; + if (next.entries.find(candidate => candidate.id === entry.id)?.text !== thought) { + next = { ...next, entries: upsert(next.entries, entry) }; + } + } + if (!visibility.showToolCalls) return next; + const id = `tool:standard:${scope}`; + const previous = next.entries.find(entry => entry.id === id); + const status = event === 'DynamicPlanStepFinished' ? 'finished' : 'running'; + if (previous?.status === 'finished' && status === 'running') return next; const entry: ActivityDetail = { id, kind: 'tool', - text: text(state.toolNames[schema]) || schema || 'Agent action', - status: item.valueType === 'DynamicPlanStepFinished' ? 'finished' : 'running' + text: text(next.toolNames[schema]) || previous?.text || schema || 'Agent action', + status }; - return { ...state, entries: upsert(state.entries, entry) }; + if (previous?.text === entry.text && previous.status === status) return next; + return { ...next, entries: upsert(next.entries, entry) }; } function upsert(entries: ActivityDetail[], entry: ActivityDetail): ActivityDetail[] { diff --git a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/nativeActivities.ts b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/nativeActivities.ts index 6d612956..73e05ff2 100644 --- a/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/nativeActivities.ts +++ b/ui/embed/sharepoint-customizer/src/extensions/sidebarAgent/components/Chat/nativeActivities.ts @@ -1,6 +1,6 @@ import { Activity } from '@microsoft/agents-activity'; import { ChatConfiguration } from '../../configuration'; -import { ActivityDetail, emptyActivityDetails, hasDetailEntities, isInformative, isToolEvent, record, updateActivityDetails } from './activityDetails'; +import { ActivityDetail, emptyActivityDetails, hasDetailEntities, isInformative, isPlanEvent, record, updateActivityDetails } from './activityDetails'; export function createPreChatActivity(configuration: ChatConfiguration): Partial { return { @@ -53,7 +53,8 @@ export class NativeActivityAdapter { constructor(private readonly configuration: ChatConfiguration) {} startTurn(): void { - this.state = emptyActivityDetails(); + // Standard plans can resume after a clarification or connector sign-in on a later turn. + this.state = { ...emptyActivityDetails(), toolNames: this.state.toolNames }; this.sequenceIds.clear(); this.timestamp = new Date().toISOString(); this.outcome = undefined; @@ -62,7 +63,7 @@ export class NativeActivityAdapter { transform(activity: Partial): Partial[] { if (activity.type === 'event' && activity.name === 'turn.complete') return this.finishTurn(); - if (hasDetailEntities(activity) || isInformative(activity) || isToolEvent(activity)) { + if (hasDetailEntities(activity) || isInformative(activity) || isPlanEvent(activity)) { const previous = this.state; this.state = updateActivityDetails(previous, activity, this.configuration); if (typeof activity.from?.id === 'string') this.author = { id: activity.from.id, role: 'bot' }; diff --git a/ui/embed/sharepoint-customizer/tests/activityDetails.test.cjs b/ui/embed/sharepoint-customizer/tests/activityDetails.test.cjs index d571897e..93183b07 100644 --- a/ui/embed/sharepoint-customizer/tests/activityDetails.test.cjs +++ b/ui/embed/sharepoint-customizer/tests/activityDetails.test.cjs @@ -1,7 +1,7 @@ const { test } = require('node:test'); const assert = require('node:assert/strict'); const { emptyActivityDetails, updateActivityDetails } = require('../temp/tests/components/Chat/activityDetails'); -const { thought, toolCall } = require('./fixtures/sdkActivities.cjs'); +const { thought, toolCall, standardPlan, standardStep } = require('./fixtures/sdkActivities.cjs'); const tool = (valueType, value = {}) => ({ type: 'event', valueType, value: { taskDialogId: 'lookup', stepId: 'call1', ...value } }); const visible = { showReasoning: true, showToolCalls: true }; @@ -79,15 +79,16 @@ test('entity-only stream metadata retains ordering and correlates the thought st assert.equal(state.entries[0].text, 'Check the list'); assert.equal(state.entries[0].sequence, 2); }); -test('tool names and lifecycle are shown without arguments, outputs or thought payloads', () => { +test('tool names and lifecycle exclude arguments and outputs while step reasoning has its own entry', () => { let state = updateActivityDetails(emptyActivityDetails(), tool('DynamicPlanReceived', { toolDefinitions: [{ schemaName: 'lookup', displayName: 'Look up a record' }] }), visible); - state = updateActivityDetails(state, tool('DynamicPlanStepTriggered', { thought: 'private', arguments: { secret: 'hidden' } }), visible); + state = updateActivityDetails(state, tool('DynamicPlanStepTriggered', { thought: 'Look up the requested record.', arguments: { secret: 'hidden' } }), visible); state = updateActivityDetails(state, tool('DynamicPlanStepFinished', { output: 'hidden' }), visible); state = updateActivityDetails(state, tool('DynamicPlanStepTriggered'), visible); - assert.equal(state.entries[0].text, 'Look up a record'); - assert.equal(state.entries[0].status, 'finished'); + assert.equal(state.entries.find(entry => entry.kind === 'tool').text, 'Look up a record'); + assert.equal(state.entries.find(entry => entry.kind === 'tool').status, 'finished'); + assert.equal(state.entries.find(entry => entry.kind === 'reasoning').text, 'Look up the requested record.'); assert.ok(!JSON.stringify(state).includes('hidden')); assert.ok(!JSON.stringify(state).includes('private')); }); @@ -107,7 +108,7 @@ test('numeric step IDs preserve separate invocations of the same tool', () => { } state = updateActivityDetails(state, tool('DynamicPlanStepFinished', { stepId: 0 }), visible); assert.deepEqual(state.entries.map(({ id, status }) => ({ id, status })), [ - { id: 'tool:0', status: 'finished' }, { id: 'tool:1', status: 'running' } + { id: 'tool:standard:["","0"]', status: 'finished' }, { id: 'tool:standard:["","1"]', status: 'running' } ]); }); @@ -117,3 +118,74 @@ test('unknown tool names cannot resolve to inherited object properties', () => { }), visible); assert.equal(state.entries[0].text, 'toString'); }); + +for (const showReasoning of [false, true]) { + for (const showToolCalls of [false, true]) { + test(`standard mapping is independent: reasoning=${showReasoning}, tools=${showToolCalls}`, () => { + const flags = { showReasoning, showToolCalls }; + let state = emptyActivityDetails(); + for (const activity of [ + standardPlan(), + standardStep('DynamicPlanStepTriggered', { thought: 'Check the weather for the requested city.', state: 'inProgress' }), + standardStep('DynamicPlanStepBindUpdate', { arguments: { Location: 'PRIVATE_CITY' }, thought: 'PRIVATE_BIND_THOUGHT' }), + standardStep('DynamicPlanStepFinished', { result: 'PRIVATE_RESULT', thought: 'PRIVATE_FINISH_THOUGHT' }) + ]) state = updateActivityDetails(state, activity, flags); + assert.equal(state.entries.filter(entry => entry.kind === 'reasoning').length, Number(showReasoning)); + assert.equal(state.entries.filter(entry => entry.kind === 'tool').length, Number(showToolCalls)); + if (showReasoning) assert.equal(state.entries.find(entry => entry.kind === 'reasoning').text, 'Check the weather for the requested city.'); + if (showToolCalls) { + assert.equal(state.entries.find(entry => entry.kind === 'tool').text, 'Get current weather'); + assert.equal(state.entries.find(entry => entry.kind === 'tool').status, 'finished'); + } + assert.ok(!JSON.stringify(state).includes('PRIVATE_')); + }); + } +} + +test('standard step snapshots replace, deduplicate, and stay separate across steps and plans', () => { + let state = updateActivityDetails(emptyActivityDetails(), standardStep('DynamicPlanStepTriggered', { thought: 'Check Seattle.' }), visible); + const same = updateActivityDetails(state, standardStep('DynamicPlanStepTriggered', { thought: 'Check Seattle.' }), visible); + assert.strictEqual(same, state); + state = updateActivityDetails(state, standardStep('DynamicPlanStepTriggered', { thought: 'Check Seattle in metric units.' }), visible); + state = updateActivityDetails(state, standardStep('DynamicPlanStepTriggered', { stepId: 'second', thought: 'Check London.' }), visible); + state = updateActivityDetails(state, standardStep('DynamicPlanStepTriggered', { planIdentifier: 'another-plan', thought: 'Check Paris.' }), visible); + assert.deepEqual(state.entries.filter(entry => entry.kind === 'reasoning').map(entry => entry.text), + ['Check Seattle in metric units.', 'Check London.', 'Check Paris.']); + assert.equal(new Set(state.entries.map(entry => entry.id)).size, 6); +}); + +test('standard event names are accepted when valueType is absent, but arbitrary thought fields are not', () => { + const event = standardStep('DynamicPlanStepTriggered', { thought: 'Check the weather.' }); + delete event.valueType; + const state = updateActivityDetails(emptyActivityDetails(), event, visible); + assert.equal(state.entries.length, 2); + for (const activity of [ + { type: 'message', name: event.name, value: event.value }, + { type: 'event', name: 'UnknownEvent', value: event.value }, + standardStep('DynamicPlanStepTriggered', { thought: { text: 'PRIVATE_NESTED' } }), + standardStep('DynamicPlanStepTriggered', { thought: ' ' }), + standardStep('DynamicPlanStepTriggered', { thought: 'Uncorrelated', stepId: '', taskDialogId: '' }) + ]) { + const next = updateActivityDetails(emptyActivityDetails(), activity, { showReasoning: true, showToolCalls: false }); + assert.deepEqual(next.entries, []); + } +}); + +test('standard binding updates identify an ongoing tool step without exposing inputs or inventing completion', () => { + let state = updateActivityDetails(emptyActivityDetails(), standardPlan(), visible); + state = updateActivityDetails(state, standardStep('DynamicPlanStepBindUpdate', { arguments: { Location: 'PRIVATE_CITY' } }), visible); + assert.deepEqual(state.entries.map(({ kind, text, status }) => ({ kind, text, status })), + [{ kind: 'tool', text: 'Get current weather', status: 'running' }]); + state = updateActivityDetails(state, standardStep('DynamicPlanStepFinished'), visible); + const late = updateActivityDetails(state, standardStep('DynamicPlanStepBindUpdate'), visible); + assert.strictEqual(late, state); + assert.equal(state.entries[0].status, 'finished'); +}); + +test('standard correlation keeps opaque plan and step identifiers distinct', () => { + let state = emptyActivityDetails(); + for (const [planIdentifier, stepId] of [['a:b', 'c'], ['a', 'b:c'], ['plan', '\uD800']]) { + state = updateActivityDetails(state, standardStep('DynamicPlanStepTriggered', { planIdentifier, stepId }), visible); + } + assert.equal(new Set(state.entries.map(entry => entry.id)).size, 3); +}); diff --git a/ui/embed/sharepoint-customizer/tests/fixtures/sdkActivities.cjs b/ui/embed/sharepoint-customizer/tests/fixtures/sdkActivities.cjs index 4510c473..cd19941e 100644 --- a/ui/embed/sharepoint-customizer/tests/fixtures/sdkActivities.cjs +++ b/ui/embed/sharepoint-customizer/tests/fixtures/sdkActivities.cjs @@ -33,4 +33,27 @@ function toolCall(status, { toolCallId = 'call', streamSequence = status === 'st }; } -module.exports = { thought, toolCall }; +// Standard-harness event envelopes observed on the published SDK route; values are synthetic. +function standardPlan(value = {}) { + return { + type: 'event', name: 'DynamicPlanReceived', valueType: 'DynamicPlanReceived', + from: { id: 'bot', role: 'bot' }, + value: { + planIdentifier: 'weather-plan', steps: ['sample.CurrentWeather'], isFinalPlan: false, + toolDefinitions: [{ + schemaName: 'sample.CurrentWeather', displayName: 'Get current weather', + description: 'PRIVATE_DESCRIPTION', inputs: [{ name: 'PRIVATE_INPUT' }], outputs: [{ name: 'PRIVATE_OUTPUT' }] + }], + ...value + } + }; +} + +function standardStep(valueType, value = {}) { + return { + type: 'event', name: valueType, valueType, from: { id: 'bot', role: 'bot' }, + value: { planIdentifier: 'weather-plan', stepId: 'weather-step', taskDialogId: 'sample.CurrentWeather', ...value } + }; +} + +module.exports = { thought, toolCall, standardPlan, standardStep }; diff --git a/ui/embed/sharepoint-customizer/tests/nativeActivities.test.cjs b/ui/embed/sharepoint-customizer/tests/nativeActivities.test.cjs index ec5a05a7..99198c69 100644 --- a/ui/embed/sharepoint-customizer/tests/nativeActivities.test.cjs +++ b/ui/embed/sharepoint-customizer/tests/nativeActivities.test.cjs @@ -2,7 +2,7 @@ const { test } = require('node:test'); const assert = require('node:assert/strict'); const { normalizeConfiguration } = require('../temp/tests/configuration'); const { createPreChatActivity, NativeActivityAdapter } = require('../temp/tests/components/Chat/nativeActivities'); -const { thought, toolCall } = require('./fixtures/sdkActivities.cjs'); +const { thought, toolCall, standardPlan, standardStep } = require('./fixtures/sdkActivities.cjs'); const configuration = overrides => normalizeConfiguration({ appClientId: 'client', tenantId: 'tenant', environmentId: 'environment', schemaName: 'agent', ...overrides @@ -38,7 +38,7 @@ for (const showReasoning of [false, true]) { ...adapter.transform(tool('DynamicPlanReceived', { toolDefinitions: [{ schemaName: 'lookup', displayName: 'Search reference material' }] })), - ...adapter.transform(tool('DynamicPlanStepTriggered', { arguments: 'PRIVATE_ARGUMENT', thought: 'PRIVATE_THOUGHT' })), + ...adapter.transform(tool('DynamicPlanStepTriggered', { arguments: 'PRIVATE_ARGUMENT' })), ...adapter.transform(tool('DynamicPlanStepBindUpdate', { arguments: 'PRIVATE_BINDING' })), ...adapter.transform(tool('DynamicPlanStepFinished', { result: 'PRIVATE_RESULT' })) ]; @@ -130,3 +130,46 @@ test('unclassified informative updates are not mislabeled or replayed into the t type: 'typing', text: 'Working', entities: [{ type: 'streaminfo', streamType: 'informative' }] }), []); }); + +test('standard reasoning and tools share the native group but have separate stable identities', () => { + const adapter = new NativeActivityAdapter(configuration({ showReasoning: true, showToolCalls: true })); + adapter.startTurn(); + adapter.transform(standardPlan()); + const activity = standardStep('DynamicPlanStepTriggered', { thought: 'Check the requested city.' }); + activity.channelData = { 'webchat:sequence-id': 4 }; + const [reasoning, tool] = adapter.transform(activity); + assert.equal(reasoning.entities[0].abstract, 'Reasoning'); + assert.equal(reasoning.text, 'Check the requested city.'); + assert.equal(tool.entities[0].abstract, 'Get current weather'); + assert.notEqual(reasoning.id, tool.id); + assert.equal(reasoning.entities[0].isPartOf['@id'], tool.entities[0].isPartOf['@id']); + assert.equal(reasoning.channelData['webchat:sequence-id'], 4); + assert.equal(tool.channelData['webchat:sequence-id'], 4); + assert.deepEqual([reasoning.entities[0].position, tool.entities[0].position], [1, 2]); + const finished = adapter.transform(standardStep('DynamicPlanStepFinished')); + assert.equal(finished[0].id, tool.id); + assert.equal(finished[0].text, 'Finished'); + assert.equal(finished[0].channelData['webchat:sequence-id'], 4); + const settled = adapter.finishTurn(); + assert.equal(settled[0].id, reasoning.id); + assert.equal(settled[0].entities[0].creativeWorkStatus, 'Published'); +}); + +test('standard tool labels survive a clarification turn without carrying reasoning or leaking into a new conversation', () => { + const adapter = new NativeActivityAdapter(configuration({ showReasoning: true, showToolCalls: true })); + adapter.startTurn(); + adapter.transform(standardPlan()); + const started = adapter.transform(standardStep('DynamicPlanStepTriggered', { thought: 'Check the requested city.' })); + adapter.finishTurn(); + adapter.startTurn(); + const [resumed] = adapter.transform(standardStep('DynamicPlanStepBindUpdate', { arguments: 'PRIVATE_INPUT' })); + const [finished] = adapter.transform(standardStep('DynamicPlanStepFinished', { result: 'PRIVATE_RESULT' })); + assert.notEqual(resumed.id, started[1].id); + assert.equal(resumed.id, finished.id); + assert.equal(finished.entities[0].abstract, 'Get current weather'); + assert.equal(finished.text, 'Finished'); + assert.equal(adapter.finishTurn().length, 1); + assert.ok(!JSON.stringify([resumed, finished]).includes('PRIVATE_')); + const fresh = new NativeActivityAdapter(configuration({ showToolCalls: true })); + assert.equal(fresh.transform(standardStep('DynamicPlanStepFinished'))[0].entities[0].abstract, 'sample.CurrentWeather'); +}); diff --git a/ui/embed/sharepoint-customizer/tests/sdkActivityMapping.test.cjs b/ui/embed/sharepoint-customizer/tests/sdkActivityMapping.test.cjs index 8aecd531..11ab2cb9 100644 --- a/ui/embed/sharepoint-customizer/tests/sdkActivityMapping.test.cjs +++ b/ui/embed/sharepoint-customizer/tests/sdkActivityMapping.test.cjs @@ -5,7 +5,7 @@ const { Activity } = require('@microsoft/agents-activity'); const { ConnectionSettings, CopilotStudioClient } = require('@microsoft/agents-copilotstudio-client'); const { normalizeConfiguration } = require('../temp/tests/configuration'); const { createConnection } = require('../temp/tests/components/Chat/createConnection'); -const { thought, toolCall } = require('./fixtures/sdkActivities.cjs'); +const { thought, toolCall, standardPlan, standardStep } = require('./fixtures/sdkActivities.cjs'); for (const showReasoning of [false, true]) { for (const showToolCalls of [false, true]) { @@ -21,6 +21,7 @@ for (const showReasoning of [false, true]) { continueTurn: `${host}/conversations/{conversationId}/continue` }); } + assert.ok(path === '/conversations' || path === '/conversations/header-only-id'); const activities = path === '/conversations' ? [{ type: 'typing' }] : [ thought('Check ', 1), thought('the list', 2), thought('Check the list', 3, 'replace'), @@ -73,3 +74,94 @@ for (const showReasoning of [false, true]) { }); } } + +for (const showReasoning of [false, true]) { + for (const showToolCalls of [false, true]) { + test(`real SDK standard mapping: reasoning=${showReasoning}, tools=${showToolCalls}`, async t => { + const host = 'https://000000000000000000000000000000.01.environment.api.powerplatform.com'; + const base = `${host}/copilotstudio/dataverse-backed/authenticated/bots/fixture/conversations`; + const requests = []; + let turn = 0; + t.mock.method(globalThis, 'fetch', async (url, options) => { + const path = new URL(url).pathname; + requests.push({ path, method: options?.method || 'GET' }); + if (path.endsWith('/conversation-endpoints')) return Response.json({ + createConversation: `${base}?api-version=2022-03-01-preview`, + executeTurn: `${base}/{conversationId}?api-version=2022-03-01-preview`, + continueTurn: `${base}/{conversationId}/continue?api-version=2022-03-01-preview`, + subscribe: `${base}/{conversationId}/subscribe?api-version=2022-03-01-preview` + }); + const start = path.endsWith('/conversations'); + if (!start) { + assert.ok(path.endsWith('/conversations/standard-conversation')); + turn++; + } + const final = { + type: 'message', id: `standard-answer-${turn}`, from: { id: 'bot', role: 'bot' }, + text: turn === 1 ? 'Which units should I use?' : 'The weather is clear.', + entities: [{ type: 'streaminfo', streamType: 'final', streamId: `stream-${turn}` }], + channelData: { streamType: 'final', streamId: `stream-${turn}` } + }; + const activities = start ? [{ type: 'typing' }] : turn === 1 ? [ + { type: 'typing', text: 'Generating plan...', entities: [{ type: 'streaminfo', streamType: 'informative' }] }, + standardPlan(), + standardStep('DynamicPlanStepTriggered', { + thought: 'Check current conditions for the requested city.', state: 'inProgress', type: 'Action' + }), + standardStep('DynamicPlanStepBindUpdate', { arguments: { Location: 'PRIVATE_CITY' } }), + final + ] : [ + standardStep('DynamicPlanStepBindUpdate', { arguments: { units: 'PRIVATE_UNITS' } }), + // Completion is a regression fixture; the live weather test stopped at connector sign-in. + standardStep('DynamicPlanStepFinished', { result: 'PRIVATE_WEATHER_RESULT' }), + final + ]; + return new Response(activities.map(activity => `event: activity\ndata: ${JSON.stringify(activity)}\n\n`).join('') + + 'event: end\ndata: done\n\n', { + headers: { 'content-type': 'text/event-stream', 'x-ms-conversationid': 'standard-conversation' } + }); + }); + const configuration = normalizeConfiguration({ + appClientId: 'client', tenantId: 'tenant', + environmentId: '00000000-0000-0000-0000-000000000001', schemaName: 'fixture', + showReasoning, showToolCalls + }); + const failures = []; + let ended = 0; + const connection = createConnection(new CopilotStudioClient(new ConnectionSettings(configuration), 'fixture-token'), configuration, { + onTurn() {}, onTurnEnd() { ended++; }, onError(error) { failures.push(error.message); } + }); + const received = []; + const subscription = connection.activity$.subscribe(activity => received.push(activity)); + t.after(() => { subscription.unsubscribe(); connection.end(); }); + await firstValueFrom(connection.connectionStatus$.pipe(filter(status => status === 2), timeout(2000))); + for (const text of ['Check Seattle weather', 'Metric']) { + await lastValueFrom(connection.postActivity(Activity.fromObject({ + type: 'message', text, from: { id: 'user', role: 'user' } + })).pipe(timeout(2000))); + } + const reasoning = received.filter(activity => activity.id?.includes(':reasoning:')); + const tools = received.filter(activity => activity.id?.includes(':tool:')); + assert.equal(new Set(reasoning.map(activity => activity.id)).size, Number(showReasoning)); + assert.equal(new Set(tools.map(activity => activity.id)).size, Number(showToolCalls) * 2); + if (showReasoning) { + assert.equal(reasoning.at(-1).text, 'Check current conditions for the requested city.'); + assert.equal(reasoning.at(-1).entities[0].abstract, 'Reasoning'); + assert.equal(reasoning.at(-1).entities[0].creativeWorkStatus, 'Published'); + } + if (showToolCalls) { + assert.ok(tools.every(activity => activity.entities[0].abstract === 'Get current weather')); + assert.ok(tools.some(activity => activity.text === 'No finish event received.')); + assert.equal(tools.at(-1).text, 'Finished'); + } + assert.equal(ended, 2); + assert.equal(received.filter(activity => activity.id?.startsWith('standard-answer-')).length, 2); + assert.ok(!received.some(activity => activity.text === 'Generating plan...')); + assert.ok(!received.some(activity => activity.type === 'event')); + assert.ok(!JSON.stringify(received).includes('PRIVATE_')); + assert.deepEqual(failures, []); + assert.equal(requests.length, 4); + assert.ok(requests.every(request => !request.path.endsWith('/subscribe'))); + }); + } +} From 5c34b78894a3c4d915467b0122735e0ff2d8abd9 Mon Sep 17 00:00:00 2001 From: adilei Date: Fri, 23 Oct 2026 18:48:06 +0300 Subject: [PATCH 3/7] Add cross-platform site deployment for SharePoint sidepane Build configured packages in isolation, guard shared-catalog updates, reconcile site activation, and document automated/manual deployment. Add deployment regression tests and an OS packaging matrix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 940e4d9a-9134-45e2-888a-1d9c5ef96c58 --- .github/workflows/sharepoint-sidepane.yml | 45 ++ ui/embed/sharepoint-customizer/README.md | 73 ++- .../config/package-solution.json | 9 +- ui/embed/sharepoint-customizer/package.json | 5 +- .../sharepoint-customizer/scripts/deploy.cjs | 435 ++++++++++++++++++ .../scripts/properties.cjs | 41 ++ .../scripts/sideload.cjs | 27 +- .../sharepoint/assets/elements.xml | 1 + .../SidebarAgentApplicationCustomizer.tsx | 4 +- ui/embed/sharepoint-customizer/src/svg.d.ts | 4 + .../tests/deploy.test.cjs | 407 ++++++++++++++++ .../tests/fixtures/deployment.json | 14 + 12 files changed, 1022 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/sharepoint-sidepane.yml create mode 100644 ui/embed/sharepoint-customizer/scripts/deploy.cjs create mode 100644 ui/embed/sharepoint-customizer/scripts/properties.cjs create mode 100644 ui/embed/sharepoint-customizer/src/svg.d.ts create mode 100644 ui/embed/sharepoint-customizer/tests/deploy.test.cjs create mode 100644 ui/embed/sharepoint-customizer/tests/fixtures/deployment.json diff --git a/.github/workflows/sharepoint-sidepane.yml b/.github/workflows/sharepoint-sidepane.yml new file mode 100644 index 00000000..7af024e0 --- /dev/null +++ b/.github/workflows/sharepoint-sidepane.yml @@ -0,0 +1,45 @@ +name: SharePoint sidepane + +on: + pull_request: + paths: + - 'ui/embed/sharepoint-customizer/**' + - '.github/workflows/sharepoint-sidepane.yml' + push: + branches: [main] + paths: + - 'ui/embed/sharepoint-customizer/**' + - '.github/workflows/sharepoint-sidepane.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + package: + name: Package (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + defaults: + run: + working-directory: ui/embed/sharepoint-customizer + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: npm install --no-audit --no-fund + - run: npm test + - name: Build and inspect configured production package (no tenant access) + run: npm run deploy -- --site-url https://contoso.sharepoint.com/sites/team --app-client-id 11111111-1111-4111-8111-111111111111 --properties tests/fixtures/deployment.json --package-only + - name: Ensure tracked source was not modified + run: git diff --exit-code + - uses: actions/upload-artifact@v4 + with: + name: sidepane-${{ matrix.os }} + path: ui/embed/sharepoint-customizer/sharepoint/solution/sidebar-agent.sppkg + retention-days: 7 diff --git a/ui/embed/sharepoint-customizer/README.md b/ui/embed/sharepoint-customizer/README.md index 928e271e..99f38f78 100644 --- a/ui/embed/sharepoint-customizer/README.md +++ b/ui/embed/sharepoint-customizer/README.md @@ -61,7 +61,7 @@ The unmodified Copilot Studio SVG is from [Microsoft's official icon collection] ### 1. Configure Microsoft authentication -1. In [Entra app registrations](https://portal.azure.com/), create or select an application for this sample. For a new registration, choose accounts in your organization. +1. In [Entra app registrations](https://portal.azure.com/), create or select an application for this sample. For a new registration, choose accounts in your organization. See [Register an application](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) for the portal steps. 2. Add a **Single-page application** redirect URI matching the SharePoint **site URL without a trailing slash**, for example `https://contoso.sharepoint.com/sites/my-site`. 3. Add a redirect URI for each site where the extension will run. Do not enable implicit grant; MSAL uses authorization code with PKCE. 4. Under **API permissions > Add a permission > APIs my organization uses**, find **Power Platform API** (`8578e004-a5c6-46e7-913e-12f58912df43`). @@ -87,7 +87,7 @@ cd CopilotStudioSamples/ui/embed/sharepoint-customizer npm install ``` -### 4. Create a local review configuration +### 4. Create a local configuration Create `properties.local.json` in the sample directory. Files named `*.local.json` are gitignored. @@ -227,18 +227,71 @@ Standard-harness plans can pause for clarification or a connector connection bef Choose safe, read-only agent actions for review. Do not trigger a business-changing tool just to demonstrate the disclosure. -## Build and package +## Automated deployment (Windows, macOS, Linux) -```bash -npm test -npm run build -npx gulp bundle --ship -npx gulp package-solution --ship +Complete **Setup** above first. You need an **existing tenant app catalog**, permission to manage its apps, and permission to install apps/customize the target site. The deployment script uses the pinned local [CLI for Microsoft 365](https://pnp.github.io/cli-microsoft365/user-guide/connecting-microsoft-365/). + +**Two different identities:** `--app-client-id` is the existing **sidepane SPA registration** from Setup. The CLI uses a separate **deployment app/login**, with the SharePoint permissions and consent required by your chosen authentication method. Invocation consent for `CopilotStudio.Copilots.Invoke` does not grant SharePoint deployment permissions. Follow [Use your own CLI identity](https://pnp.github.io/cli-microsoft365/user-guide/using-own-identity/); the script does not create apps, change authentication, or grant permissions. + +1. Sign the local CLI in using your authorized deployment registration and an explicit tenant GUID. Skip this if the correct login is already active: + + ```text + npm exec -- m365 login --authType deviceCode --appId DEPLOYER_CLIENT_ID --tenant YOUR_TENANT_ID + npm exec -- m365 status + npm exec -- m365 spo tenant appcatalogurl get + ``` + +2. Use the discovered catalog URL, the target **site URL** (not a page/debug URL), and your public `properties.local.json` settings: + + ```text + npm run deploy -- --site-url https://contoso.sharepoint.com/sites/team --app-catalog-url https://contoso.sharepoint.com/sites/apps --app-client-id YOUR_APP_CLIENT_ID --properties properties.local.json + ``` + +3. Open a normal modern page on that site, without sideload/debug parameters. Confirm one Copilot Studio launcher, sign in, and send a message and a follow-up. No localhost server is needed. + +The script validates settings and the current login/catalog/site, builds in an isolated temporary directory, XML-escapes the configuration, and checks the production package. It then uploads/enables the app in the catalog, installs it **only on the target site**, and verifies the installed version and one Web-scoped customizer with the requested properties. Its feature creates the initial registration; the script never adds a second custom action. Tracked templates and local settings are not modified. + +`--app-client-id` is required and overrides `appClientId` in the JSON file; you can omit that JSON property for deployment. The script prints the exact SPA redirect URI to register, but does not inspect or change Entra registrations. Output is `sharepoint/solution/sidebar-agent.sppkg`, with verified deployment metadata in `sharepoint/solution/deployment.local.json`. Both are gitignored. The package contains public configuration, not secrets. + +### Preview, package only, and updates + +Append `--dry-run` to validate inputs and print the plan **without building or contacting Microsoft 365**. It is not an authentication/permission check. Append `--package-only` to build and inspect the `.sppkg` without signing in or contacting SharePoint; `--app-catalog-url` is optional in both offline modes. For example: + +```text +npm run deploy -- --site-url https://contoso.sharepoint.com/sites/team --app-client-id YOUR_APP_CLIENT_ID --properties properties.local.json --package-only ``` -The package is generated at `sharepoint/solution/sidebar-agent.sppkg`. Before packaged deployment, replace placeholders in `sharepoint/assets/elements.xml` (site custom action) and `sharepoint/assets/ClientSideInstance.xml` (tenant-wide extension instance) for the deployment method you use. Avoid registering both paths on the same site and producing duplicate launchers. +An identical package is reused on subsequent runs; an installed app is not installed again, and drifted site properties are updated on the existing action. For changed code or packaged configuration, increase **both** solution and feature versions in `config/package-solution.json`, then add `--replace` to authorize the catalog update. The script refuses downgrades and different content at the same version. + +{: .warning } +> A tenant catalog is shared. Replacing this solution can affect other installed sites, and its packaged settings become the defaults for future installations. `--replace` acknowledges that broader effect; it does not make the catalog update site-local. Do not run competing deployments or use replacement to configure unrelated sites blindly. + +Existing tenant-wide, Site-scoped, duplicate, or standalone registrations must be resolved explicitly before this site-install workflow. Nothing is automatically removed. On failure, the script stops, lists completed remote operations, and leaves the package available for inspection; it does not uninstall apps or roll back a shared catalog. Correct the reported permissions/state issue and rerun, or use the manual path below. A successful metadata check still requires the browser conversation check in step 3. + +Run `npm run deploy -- --help` for all options. + +## Manual deployment + +This is the same supported site-install model, without CLI deployment authentication. Follow Microsoft's [Deploy an SPFx extension](https://learn.microsoft.com/en-us/sharepoint/dev/spfx/extensions/get-started/serving-your-extension-from-sharepoint) and [Manage apps using the SharePoint Apps site](https://learn.microsoft.com/en-us/sharepoint/use-app-catalog) for the SharePoint screens. This sample uses **SPFx 1.21.1/Gulp**, not the Heft commands shown for newer versions. + +1. Complete **Setup**, including the runtime SPA registration, exact target-site redirect URI, delegated invocation permission, consent, and published-agent access. +2. Replace placeholders in `sharepoint/assets/elements.xml` with your runtime app client ID, tenant ID, environment ID, and agent schema name. Set any UI flags you need. Keep JSON booleans unquoted and preserve XML escaping: `"` for `"`, `&` for `&`, `<` for `<`, and `>` for `>`. Do not put credentials in this file. Alternatively, use `--package-only` above to generate the configured package without editing XML. +3. If you edited XML manually, build from the sample directory: + + ```text + npx gulp bundle --ship + npx gulp package-solution --ship + ``` + +4. Upload `sharepoint/solution/sidebar-agent.sppkg` to your **existing tenant Apps site/app catalog**. Select **Only enable this app**, not **Enable this app and add it to all sites**. +5. On the target SharePoint site, use **Site contents > New > App** to add the catalog app. Installation activates the package feature and registers the customizer; do **not** also register it manually. +6. Open a normal modern page, confirm one launcher, sign in if prompted, and verify a reply and same-conversation follow-up without a development server. + +The default package requires site installation (`skipFeatureDeployment: false`) and includes only `elements.xml`. `ClientSideInstance.xml` is not packaged and must not be enabled alongside this site action. For upgrades, bump solution/feature versions together, replace the catalog package deliberately, and update the app on the target site. Existing custom-action properties may need an explicit update; the automated path performs that reconciliation. + +### Validation -Upload the package to your app catalog only when ready for installation; local review does not require this step. See [Use the SharePoint app catalog](https://learn.microsoft.com/en-us/sharepoint/use-app-catalog). +Run `npm test` for configuration, attachment, dual-harness activity mapping, and deployment regression tests; `npm run build` checks the SPFx development build. The **SharePoint sidepane** GitHub Actions workflow runs tests and actual production `--package-only` builds on Windows, macOS, and Linux with synthetic settings. It has no tenant credentials and does not deploy to SharePoint. Live site installation, authentication/consent, and agent responses must be verified separately in your tenant. ## Troubleshooting diff --git a/ui/embed/sharepoint-customizer/config/package-solution.json b/ui/embed/sharepoint-customizer/config/package-solution.json index a4a21d53..98633c88 100644 --- a/ui/embed/sharepoint-customizer/config/package-solution.json +++ b/ui/embed/sharepoint-customizer/config/package-solution.json @@ -3,9 +3,9 @@ "solution": { "name": "sidebar-agent-client-side-solution", "id": "54cbba6a-2459-43a9-80a4-bc0675a81ac0", - "version": "1.0.0.0", + "version": "1.1.0.0", "includeClientSideAssets": true, - "skipFeatureDeployment": true, + "skipFeatureDeployment": false, "isDomainIsolated": false, "developer": { "name": "", @@ -30,11 +30,10 @@ "title": "Application Extension - Deployment of custom action", "description": "Deploys a custom action with ClientSideComponentId association", "id": "f757a2ed-fea6-4478-abd2-080c4a1f6a84", - "version": "1.0.0.0", + "version": "1.1.0.0", "assets": { "elementManifests": [ - "elements.xml", - "ClientSideInstance.xml" + "elements.xml" ] } } diff --git a/ui/embed/sharepoint-customizer/package.json b/ui/embed/sharepoint-customizer/package.json index 2528a8de..392eb74e 100644 --- a/ui/embed/sharepoint-customizer/package.json +++ b/ui/embed/sharepoint-customizer/package.json @@ -11,7 +11,8 @@ "clean": "gulp clean", "test": "tsc -p tsconfig.test.json && node --test tests/*.test.cjs", "serve": "gulp serve --nobrowser", - "sideload": "node scripts/sideload.cjs" + "sideload": "node scripts/sideload.cjs", + "deploy": "node scripts/deploy.cjs" }, "dependencies": { "@azure/msal-browser": "^4.13.1", @@ -35,10 +36,12 @@ "@microsoft/rush-stack-compiler-5.3": "0.1.0", "@microsoft/sp-build-web": "1.21.1", "@microsoft/sp-module-interfaces": "1.21.1", + "@pnp/cli-microsoft365": "11.5.0", "@rushstack/eslint-config": "4.0.1", "@types/webpack-env": "~1.15.2", "ajv": "^6.12.5", "eslint": "8.57.1", + "fflate": "0.8.2", "gulp": "4.0.2", "typescript": "~5.3.3" } diff --git a/ui/embed/sharepoint-customizer/scripts/deploy.cjs b/ui/embed/sharepoint-customizer/scripts/deploy.cjs new file mode 100644 index 00000000..45628f44 --- /dev/null +++ b/ui/embed/sharepoint-customizer/scripts/deploy.cjs @@ -0,0 +1,435 @@ +const fs = require('node:fs/promises'); +const path = require('node:path'); +const os = require('node:os'); +const { spawn } = require('node:child_process'); +const { parseArgs, isDeepStrictEqual } = require('node:util'); +const { createHash, randomUUID } = require('node:crypto'); +const { unzipSync, strFromU8 } = require('fflate'); +const { validateProperties } = require('./properties.cjs'); + +const ROOT = path.resolve(__dirname, '..'); +const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const LOCATION = 'ClientSideExtension.ApplicationCustomizer'; +const HELP = `Deploy the SharePoint sidepane to one site (Node 22, Windows/macOS/Linux). + +npm run deploy -- --site-url --app-catalog-url --app-client-id --properties + +--app-client-id Existing sidepane SPA registration; overrides appClientId in JSON. +--properties Tenant/agent settings and optional UI flags, never credentials. +--site-url Target site; also the SPA redirect URI, without a trailing slash. +--app-catalog-url Existing tenant app catalog. Required only for live deployment. +--dry-run Validate and describe the plan; no build, login, or remote calls. +--package-only Build and inspect the configured .sppkg; no login or remote calls. +--replace Authorize replacing a different catalog package with a NEWER version. +--help Show this help. + +Live deployment reuses your existing m365 login to the explicit tenant GUID. +The deployer's app/permissions are separate from the sidepane SPA registration. +The script never logs in, grants permissions, or enables tenant-wide activation. +Catalog replacement can affect other installations. Bump solution/feature versions +before deploying changed code or packaged defaults. Identical packages are reused. +Failures stop deployment and report completed remote operations; no automatic rollback.`; + +function siteUrl(value, name) { + if (!value) throw new Error(`${name} is required.`); + const url = new URL(value); + if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || + url.port || !/\.sharepoint\.(com|us|de|cn)$/i.test(url.hostname) || + /(?:^|\/)_layouts(?:\/|$)|\.aspx$/i.test(url.pathname)) { + throw new Error(`${name} must be a SharePoint HTTPS site URL, not a page, debug link, or URL with credentials.`); + } + return url.href.replace(/\/+$/, ''); +} + +function guid(value, name) { + if (typeof value !== 'string' || !GUID.test(value)) throw new Error(`${name} must be a GUID, not a placeholder.`); + return value.toLowerCase(); +} + +async function readJson(file) { + try { return JSON.parse(await fs.readFile(file, 'utf8')); } + catch (error) { throw new Error(`Cannot read JSON from ${file}: ${error.message}`); } +} + +async function loadOptions(argv, cwd = process.cwd(), root = ROOT) { + const { values } = parseArgs({ + args: argv, allowPositionals: false, + options: { + 'site-url': { type: 'string' }, 'app-catalog-url': { type: 'string' }, + 'app-client-id': { type: 'string' }, properties: { type: 'string' }, + 'dry-run': { type: 'boolean' }, 'package-only': { type: 'boolean' }, + replace: { type: 'boolean' }, help: { type: 'boolean' } + } + }); + if (values.help) return { help: true }; + if (values['dry-run'] && values['package-only']) throw new Error('Choose --dry-run or --package-only, not both.'); + if (!values.properties) throw new Error('--properties is required.'); + const fileProperties = await readJson(path.resolve(cwd, values.properties)); + if (!fileProperties || Array.isArray(fileProperties) || typeof fileProperties !== 'object') { + throw new Error('Properties must be a JSON object.'); + } + const properties = validateProperties({ ...fileProperties, appClientId: guid(values['app-client-id'], '--app-client-id') }); + properties.tenantId = guid(properties.tenantId, 'tenantId'); + if (properties.environmentId) properties.environmentId = guid(properties.environmentId, 'environmentId'); + for (const name of ['schemaName', 'agentIdentifier', 'directConnectUrl']) { + if (properties[name]?.includes('YOUR_')) throw new Error(`Replace the ${name} placeholder.`); + } + const target = siteUrl(values['site-url'], '--site-url'); + const catalog = values['app-catalog-url'] ? siteUrl(values['app-catalog-url'], '--app-catalog-url') : undefined; + if (!catalog && !values['package-only'] && !values['dry-run']) throw new Error('--app-catalog-url is required for live deployment.'); + if (catalog && new URL(catalog).hostname !== new URL(target).hostname) { + throw new Error('The site and tenant app catalog must be on the same SharePoint host.'); + } + const metadata = await readJson(path.join(root, 'config/package-solution.json')); + const manifest = await readJson(path.join(root, 'src/extensions/sidebarAgent/SidebarAgentApplicationCustomizer.manifest.json')); + const solution = metadata.solution; + if (solution.skipFeatureDeployment !== false || solution.includeClientSideAssets !== true || + solution.features?.length !== 1 || + !isDeepStrictEqual(solution.features[0].assets?.elementManifests, ['elements.xml'])) { + throw new Error('Use a site-installed package with included assets and only elements.xml; tenant-wide activation is not supported.'); + } + versionParts(solution.version); + if (solution.features[0].version !== solution.version) throw new Error('Keep solution and feature versions equal.'); + const packagePath = path.resolve(root, 'sharepoint', metadata.paths.zippedPackage); + if (path.dirname(packagePath) !== path.join(root, 'sharepoint/solution') || !packagePath.endsWith('.sppkg')) { + throw new Error('The package must be a .sppkg directly inside sharepoint/solution.'); + } + return { + root, properties, siteUrl: target, appCatalogUrl: catalog, packagePath, + version: solution.version, productId: guid(solution.id, 'solution.id'), + componentId: guid(manifest.id, 'component.id'), featureId: guid(solution.features[0].id, 'feature.id'), + dryRun: !!values['dry-run'], packageOnly: !!values['package-only'], replace: !!values.replace + }; +} + +function versionParts(version) { + if (typeof version !== 'string' || !/^\d+\.\d+\.\d+\.\d+$/.test(version) || + version.split('.').some(part => Number(part) > 65535)) throw new Error(`Invalid package version: ${version}`); + return version.split('.').map(Number); +} + +function compareVersions(a, b) { + const left = versionParts(a), right = versionParts(b); + for (let index = 0; index < 4; index++) if (left[index] !== right[index]) return Math.sign(left[index] - right[index]); + return 0; +} + +function xmlEscape(value) { + return value.replace(/[&<>"']/g, character => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[character]); +} + +function xmlUnescape(value) { + return value.replace(/&(amp|lt|gt|quot|apos);/g, (_, entity) => + ({ amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" })[entity]); +} + +function elementsXml(options) { + return ` + + + +`; +} + +function packageFingerprint(buffer) { + const entries = unzipSync(buffer); + const hash = createHash('sha256'); + for (const name of Object.keys(entries).sort()) { + let data = entries[name]; + // SPFx regenerates these packaging-only IDs on every build, even for identical inputs. + if (name.endsWith('.xml.config.xml')) { + data = strFromU8(data).replace(/[0-9a-f-]{36}<\/Id>/gi, 'generated'); + } else if (name === 'ClientSideAssets.xml') { + data = strFromU8(data).replace(/ Id="[0-9a-f-]{36}"/i, ' Id="generated"'); + } + hash.update(name).update('\0').update(data).update('\0'); + } + return hash.digest('hex'); +} + +function inspectPackage(buffer, options) { + const entries = unzipSync(buffer); + const xml = Object.entries(entries).filter(([name]) => /\.xml$/i.test(name)).map(([, data]) => strFromU8(data)).join('\n'); + if (/]*>/g)]; + if (actions.length !== 1) throw new Error('Package must contain exactly one customizer activation.'); + const properties = actions[0][0].match(/ClientSideComponentProperties="([^"]*)"/); + if (!properties || !isDeepStrictEqual(JSON.parse(xmlUnescape(properties[1])), options.properties) || + !actions[0][0].includes(`ClientSideComponentId="${options.componentId}"`)) { + throw new Error('Packaged customizer properties or component ID do not match the requested configuration.'); + } + const appManifest = strFromU8(entries['AppManifest.xml'] || new Uint8Array()); + const feature = strFromU8(entries[`feature_${options.featureId}.xml`] || new Uint8Array()); + if (!appManifest.includes(`ProductID="${options.productId}"`) || !appManifest.includes(`Version="${options.version}"`) || + !feature.includes(`Id="${options.featureId}"`) || !feature.includes(`Version="${options.version}"`) || !feature.includes('Scope="Web"')) { + throw new Error('Packaged solution identity/version does not match the source metadata.'); + } + const manifests = [...xml.matchAll(/ComponentManifest="([^"]*)"/g)].map(match => JSON.parse(xmlUnescape(match[1]))); + if (manifests.length !== 1 || manifests[0].id !== options.componentId || + !Object.keys(entries).some(name => /ClientSideAssets\/.*\.js$/i.test(name))) { + throw new Error('Production client assets are missing from the package.'); + } + for (const manifest of manifests) { + if (!isDeepStrictEqual(manifest.loaderConfig?.internalModuleBaseUrls, ['HTTPS://SPCLIENTSIDEASSETLIBRARY/'])) { + throw new Error('Package must load its included SharePoint assets, not localhost or an external CDN.'); + } + for (const resource of Object.values(manifest.loaderConfig.scriptResources)) { + if (resource.type === 'path' && !entries[`ClientSideAssets/${resource.path}`]) throw new Error(`Missing packaged script: ${resource.path}`); + } + } + return { fingerprint: packageFingerprint(buffer), entries: Object.keys(entries).length }; +} + +function runNode(entry, args, { cwd = ROOT, inherit = false, timeout = 120000 } = {}) { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [entry, ...args], { + cwd, shell: false, windowsHide: true, stdio: inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'] + }); + let stdout = '', stderr = '', timedOut = false, forceKill; + const timer = setTimeout(() => { + timedOut = true; + child.kill(); + forceKill = setTimeout(() => child.kill('SIGKILL'), 5000); + }, timeout); + child.stdout?.on('data', data => { stdout += data; }); + child.stderr?.on('data', data => { stderr += data; }); + child.once('error', error => { clearTimeout(timer); clearTimeout(forceKill); reject(error); }); + child.once('close', code => { + clearTimeout(timer); + clearTimeout(forceKill); + if (timedOut) reject(new Error(`${path.basename(entry)} timed out. Check remote state before retrying.`)); + else if (code !== 0) reject(new Error(`${path.basename(entry)} exited ${code}: ${(stderr || stdout).trim().slice(-8000)}`)); + else resolve(stdout); + }); + }); +} + +async function cli(args, run = runNode) { + const entry = path.join(path.dirname(require.resolve('@pnp/cli-microsoft365/package.json')), 'dist/index.js'); + const output = await run(entry, [...args, '--output', 'json']); + if (!output.trim()) return undefined; + try { return JSON.parse(output); } + catch { throw new Error(`CLI returned invalid JSON for ${args.slice(0, 4).join(' ')}.`); } +} + +async function buildPackage(options, { run = runNode, log = console.log } = {}) { + const staging = await fs.mkdtemp(path.join(os.tmpdir(), 'sidepane-deploy-')); + try { + for (const name of [ + 'src', 'config/config.json', 'config/package-solution.json', 'config/sass.json', 'config/write-manifests.json', + 'sharepoint/assets/elements.xml', 'package.json', 'tsconfig.json', 'gulpfile.js', '.eslintrc.js' + ]) { + await fs.cp(path.join(options.root, name), path.join(staging, name), { recursive: true }); + } + await fs.symlink(path.join(options.root, 'node_modules'), path.join(staging, 'node_modules'), + process.platform === 'win32' ? 'junction' : 'dir'); + await fs.writeFile(path.join(staging, 'sharepoint/assets/elements.xml'), elementsXml(options), 'utf8'); + const gulp = require.resolve('gulp/bin/gulp.js'); + log('Building an isolated, configured production package...'); + await run(gulp, ['bundle', '--ship'], { cwd: staging, inherit: true, timeout: 600000 }); + await run(gulp, ['package-solution', '--ship'], { cwd: staging, inherit: true, timeout: 180000 }); + const generated = path.join(staging, path.relative(options.root, options.packagePath)); + const buffer = await fs.readFile(generated); + const inspection = inspectPackage(buffer, options); + await fs.mkdir(path.dirname(options.packagePath), { recursive: true }); + const pending = `${options.packagePath}.${randomUUID()}.tmp`; + try { + await fs.copyFile(generated, pending); + await fs.rename(pending, options.packagePath); + } finally { await fs.rm(pending, { force: true }); } + log(`Verified package: ${options.packagePath}`); + return inspection; + } finally { + // Remove the dependency link first; never recursively walk the shared dependency directory. + await fs.rm(path.join(staging, 'node_modules'), { force: true }); + await fs.rm(staging, { recursive: true, force: true }); + } +} + +function array(value, label) { + if (!Array.isArray(value)) throw new Error(`Invalid ${label} response; expected an array.`); + return value; +} + +function object(value, label) { + if (!value || Array.isArray(value) || typeof value !== 'object') throw new Error(`Invalid ${label} response; expected an object.`); + return value; +} + +function matchingActions(actions, options) { + const matches = array(actions, 'custom actions').filter(action => + String(action.ClientSideComponentId).toLowerCase() === options.componentId && action.Location === LOCATION); + if (matches.length > 1 || matches.some(action => action.Scope !== 3)) { + throw new Error('Conflicting customizer registrations found. Resolve duplicate/Site-scoped actions explicitly; nothing was removed.'); + } + return matches; +} + +async function poll(read, ready, label, { timeout = 300000, interval = 5000, sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) } = {}) { + const deadline = Date.now() + timeout; + do { + const value = await read(); + if (ready(value)) return value; + if (Date.now() >= deadline) break; + await sleep(Math.min(interval, Math.max(0, deadline - Date.now()))); + } while (Date.now() <= deadline); + throw new Error(`Timed out waiting for ${label}. Inspect remote state before retrying.`); +} + +async function deploy(options, { command = cli, build = buildPackage, log = console.log, polling } = {}) { + log(`Target: ${options.siteUrl}\nCatalog: ${options.appCatalogUrl || '(not needed)'}\nPackage: ${options.productId} v${options.version}`); + log(`Sidepane app client ID: ${options.properties.appClientId}\nRequired SPA redirect: ${options.siteUrl}`); + if (options.dryRun) { + log('Dry run: validate existing deployer -> build isolated package -> upload/deploy -> install/upgrade on this site -> verify one customizer. No actions performed.'); + return { dryRun: true }; + } + if (options.packageOnly) return build(options, { log }); + let stage = 'preflight'; + const completed = []; + const catalogArgs = ['--appCatalogUrl', options.appCatalogUrl, '--appCatalogScope', 'tenant']; + const request = url => command(['request', '--url', url, '--method', 'get', '--accept', 'application/json;odata=nometadata']); + const getActions = () => command(['spo', 'customaction', 'list', '--webUrl', options.siteUrl, '--scope', 'All']); + const getApps = async () => array(await command(['spo', 'app', 'list', ...catalogArgs]), 'catalog apps'); + const findApp = apps => { + const matches = apps.filter(app => String(app.ProductId).toLowerCase() === options.productId); + if (matches.length > 1) throw new Error('Multiple catalog apps have the solution ProductId.'); + return matches[0]; + }; + const checkApp = app => { + object(app, 'app metadata'); + guid(app.ID, 'catalog app ID'); + if (String(app.ProductId).toLowerCase() !== options.productId || app.IsClientSideSolution !== true || app.ContainsTenantWideExtension !== false || + app.SkipDeploymentFeature !== false || app.IsPackageDefaultSkipFeatureDeployment !== false) { + throw new Error('Catalog app identity or activation scope conflicts with this site-only deployment.'); + } + if (app.IsValidAppPackage !== true) throw new Error(`Invalid catalog package: ${app.ErrorMessage || 'SharePoint rejected the package'}`); + if (typeof app.InstalledVersion !== 'string' || typeof app.Deployed !== 'boolean' || typeof app.CurrentVersionDeployed !== 'boolean') { + throw new Error('Incomplete app installation/deployment metadata.'); + } + return app; + }; + try { + const status = await command(['status']); + if (!status || typeof status !== 'object' || !status.connectedAs) { + throw new Error('CLI is logged out. Run npm exec -- m365 login with your separate deployment app and explicit tenant GUID, then retry.'); + } + if (String(status.appTenant).toLowerCase() !== options.properties.tenantId) { + throw new Error('CLI login tenant does not match tenantId. Sign in explicitly to the intended tenant; authentication was not changed.'); + } + const actualCatalog = await command(['spo', 'tenant', 'appcatalogurl', 'get']); + if (typeof actualCatalog !== 'string' || siteUrl(actualCatalog, 'Discovered catalog').toLowerCase() !== options.appCatalogUrl.toLowerCase()) { + throw new Error('The supplied app catalog does not match the authenticated tenant app catalog.'); + } + const web = object(await request(`${options.siteUrl}/_api/web?$select=Url`), 'target site'); + if (siteUrl(web.Url, 'Resolved site').toLowerCase() !== options.siteUrl.toLowerCase()) throw new Error('The target resolves to a different SharePoint site.'); + const tenantActions = array(await command(['spo', 'tenant', 'applicationcustomizer', 'list']), 'tenant-wide customizers'); + if (tenantActions.some(action => String(action.TenantWideExtensionComponentId).toLowerCase() === options.componentId)) { + throw new Error('A tenant-wide registration already exists for this component. Resolve it explicitly before site-only deployment.'); + } + const initialActions = matchingActions(await getActions(), options); + const existing = findApp(await getApps()); + if (existing) { + checkApp(existing); + if (compareVersions(options.version, existing.AppCatalogVersion) < 0) throw new Error('Refusing to downgrade the catalog package.'); + if (compareVersions(options.version, existing.AppCatalogVersion) > 0 && !options.replace) { + throw new Error('A catalog package already exists. Use --replace to authorize its upgrade; other installations can be affected.'); + } + const installed = checkApp(await request(`${options.siteUrl}/_api/web/tenantappcatalog/AvailableApps/GetById('${existing.ID}')`)); + if (installed.InstalledVersion && compareVersions(installed.InstalledVersion, options.version) > 0) throw new Error('Refusing to downgrade the site app.'); + if (initialActions.length && !installed.InstalledVersion) throw new Error('An orphaned customizer already exists without an installed app. Resolve it explicitly before installation.'); + } else { + if (initialActions.length) throw new Error('A standalone customizer already exists. Resolve it explicitly before installing another registration.'); + const filename = path.basename(options.packagePath).replace(/'/g, "''"); + const files = object(await request(`${options.appCatalogUrl}/_api/web/GetFolderByServerRelativePath(DecodedUrl='AppCatalog')/Files?$select=Name&$filter=Name eq '${encodeURIComponent(filename)}'`), 'catalog files'); + if (array(files.value, 'catalog files').length) throw new Error('The package filename is already occupied by a different or unprocessed app. Resolve it before deployment.'); + } + stage = 'package'; + const inspection = await build(options, { log }); + const upload = !existing || compareVersions(options.version, existing.AppCatalogVersion) > 0; + if (existing && !upload) { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), 'sidepane-catalog-')); + try { + const file = path.join(temporary, 'existing.sppkg'); + await command(['request', '--url', `${options.appCatalogUrl}/_api/web/GetFileById('${existing.ID}')/$value`, + '--method', 'get', '--accept', 'application/octet-stream', '--filePath', file]); + if (packageFingerprint(await fs.readFile(file)) !== inspection.fingerprint) { + throw new Error('Catalog content differs at the same version. Bump solution/feature versions and use --replace; nothing was overwritten.'); + } + } finally { await fs.rm(temporary, { recursive: true, force: true }); } + log('Identical catalog package found; reusing it.'); + } + if (upload) { + const latest = findApp(await getApps()); + if (latest?.ID !== existing?.ID || latest?.AppCatalogVersion !== existing?.AppCatalogVersion) { + throw new Error('The catalog changed during packaging. Inspect the current app and retry; nothing was overwritten.'); + } + stage = 'upload'; + log(`${existing ? 'Replacing' : 'Uploading'} catalog package, then enabling v${options.version} on ${options.siteUrl}.`); + await command(['spo', 'app', 'add', '--filePath', options.packagePath, ...catalogArgs, ...(existing ? ['--overwrite'] : [])]); + completed.push('uploaded package'); + } + const app = await poll(async () => findApp(await getApps()), value => { + if (!value || value.AppCatalogVersion !== options.version) return false; + checkApp(value); + return true; + }, 'catalog package processing', polling); + if (upload || !app.Deployed || !app.CurrentVersionDeployed) { + stage = 'deploy catalog package'; + await command(['spo', 'app', 'deploy', '--id', app.ID, ...catalogArgs]); + completed.push('deployed catalog package'); + } + await poll(async () => findApp(await getApps()), value => { + if (!value) return false; + checkApp(value); + return value.Deployed === true && value.CurrentVersionDeployed === true && value.AppCatalogVersion === options.version; + }, 'catalog deployment', polling); + const getSiteApp = async () => checkApp(await request(`${options.siteUrl}/_api/web/tenantappcatalog/AvailableApps/GetById('${app.ID}')`)); + let siteApp = await getSiteApp(); + if (siteApp.InstalledVersion && compareVersions(siteApp.InstalledVersion, options.version) > 0) throw new Error('Refusing to downgrade the site app.'); + if (!siteApp.InstalledVersion || compareVersions(siteApp.InstalledVersion, options.version) < 0) { + const operation = siteApp.InstalledVersion ? 'upgrade' : 'install'; + stage = `${operation} site app`; + await command(['spo', 'app', operation, '--id', app.ID, '--siteUrl', options.siteUrl, '--appCatalogScope', 'tenant']); + completed.push(`${operation} site app`); + } + stage = 'verify site installation'; + siteApp = await poll(getSiteApp, value => value.InstalledVersion === options.version, 'site app installation', polling); + let [action] = await poll(async () => matchingActions(await getActions(), options), + matches => matches.length === 1, 'feature-created Web customizer', polling); + stage = 'verify site properties'; + let currentProperties; + try { currentProperties = JSON.parse(action.ClientSideComponentProperties); } + catch { throw new Error('The installed customizer has invalid JSON properties. Resolve it before retrying.'); } + if (!isDeepStrictEqual(currentProperties, options.properties)) { + await command(['spo', 'customaction', 'set', '--webUrl', options.siteUrl, '--scope', 'Web', '--id', guid(action.Id, 'custom action ID'), + '--clientSideComponentProperties', JSON.stringify(options.properties)]); + completed.push('updated site customizer properties'); + } + [action] = await poll(async () => matchingActions(await getActions(), options), matches => { + if (matches.length !== 1) return false; + return isDeepStrictEqual(JSON.parse(matches[0].ClientSideComponentProperties), options.properties); + }, 'customizer property readback', polling); + const report = { + siteUrl: options.siteUrl, appCatalogUrl: options.appCatalogUrl, productId: options.productId, + catalogAppId: app.ID, version: siteApp.InstalledVersion, customActionId: action.Id, + componentId: options.componentId, properties: options.properties, verifiedAt: new Date().toISOString(), ...inspection, completed + }; + await fs.writeFile(path.join(path.dirname(options.packagePath), 'deployment.local.json'), JSON.stringify(report, null, 2), { mode: 0o600 }); + log(`Verified one site-scoped customizer at ${options.siteUrl}. Open a normal page without debug query parameters.`); + return report; + } catch (error) { + throw new Error(`Deployment stopped at "${stage}": ${error.message}\nCompleted remote operations: ${completed.join(', ') || 'none'}. No automatic rollback was attempted.`); + } +} + +if (require.main === module) { + loadOptions(process.argv.slice(2)).then(options => options.help ? console.log(HELP) : deploy(options)) + .catch(error => { console.error(error.message); process.exitCode = 1; }); +} + +module.exports = { + loadOptions, elementsXml, inspectPackage, packageFingerprint, runNode, buildPackage, cli, + deploy, matchingActions, compareVersions, poll, siteUrl +}; diff --git a/ui/embed/sharepoint-customizer/scripts/properties.cjs b/ui/embed/sharepoint-customizer/scripts/properties.cjs new file mode 100644 index 00000000..3c2c1707 --- /dev/null +++ b/ui/embed/sharepoint-customizer/scripts/properties.cjs @@ -0,0 +1,41 @@ +function validateProperties(properties) { + if (!properties || typeof properties !== 'object' || Array.isArray(properties)) { + throw new Error('Properties must be a JSON object.'); + } + const strings = [ + 'appClientId', 'tenantId', 'environmentId', 'schemaName', 'agentIdentifier', 'directConnectUrl', + 'agentTitle', 'headerBackgroundColor' + ]; + const flags = ['startConversation', 'showTyping', 'enableFileUpload', 'showReasoning', 'showToolCalls']; + const allowed = [...strings, ...flags, 'starterPrompts']; + for (const key of Object.keys(properties)) { + if (!allowed.includes(key)) throw new Error(`Unknown property "${key}". Never put credentials in extension properties.`); + } + for (const key of strings) { + if (properties[key] !== undefined && typeof properties[key] !== 'string') throw new Error(`${key} must be a string.`); + } + if (!properties.appClientId?.trim() || !properties.tenantId?.trim() || + !(properties.directConnectUrl?.trim() || + (properties.environmentId?.trim() && (properties.schemaName?.trim() || properties.agentIdentifier?.trim())))) { + throw new Error('Provide appClientId, tenantId, and environmentId/schemaName (or an explicit directConnectUrl).'); + } + if (properties.directConnectUrl) { + const url = new URL(properties.directConnectUrl); + if (url.protocol !== 'https:' || url.username || url.password || url.hash) { + throw new Error('directConnectUrl must be an HTTPS URL without credentials or a fragment.'); + } + } + for (const flag of flags) { + if (properties[flag] !== undefined && typeof properties[flag] !== 'boolean') throw new Error(`${flag} must be a boolean.`); + } + if (properties.starterPrompts !== undefined) { + if (!Array.isArray(properties.starterPrompts) || properties.starterPrompts.some(prompt => + !prompt || typeof prompt !== 'object' || Array.isArray(prompt) || + Object.keys(prompt).some(key => key !== 'title' && key !== 'text') || + typeof prompt.title !== 'string' || !prompt.title.trim() || typeof prompt.text !== 'string' || !prompt.text.trim() + )) throw new Error('starterPrompts must be an array of objects containing only a non-empty title and text.'); + } + return properties; +} + +module.exports = { validateProperties }; diff --git a/ui/embed/sharepoint-customizer/scripts/sideload.cjs b/ui/embed/sharepoint-customizer/scripts/sideload.cjs index f77c23d7..dc58ad91 100644 --- a/ui/embed/sharepoint-customizer/scripts/sideload.cjs +++ b/ui/embed/sharepoint-customizer/scripts/sideload.cjs @@ -1,4 +1,5 @@ const { readFileSync } = require('node:fs'); +const { validateProperties } = require('./properties.cjs'); const COMPONENT_ID = '4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2'; @@ -11,31 +12,7 @@ function createSideloadUrl(pageUrl, properties, manifestUrl = 'https://localhost if (manifest.protocol !== 'https:' || manifest.username || manifest.password) { throw new Error('The development manifest must be an HTTPS URL without credentials.'); } - if (!properties || typeof properties !== 'object' || Array.isArray(properties)) { - throw new Error('Properties must be a JSON object.'); - } - const allowed = [ - 'appClientId', 'tenantId', 'environmentId', 'schemaName', 'agentIdentifier', 'directConnectUrl', - 'startConversation', 'showTyping', 'enableFileUpload', 'showReasoning', 'showToolCalls', - 'agentTitle', 'headerBackgroundColor', 'starterPrompts' - ]; - for (const key of Object.keys(properties)) { - if (!allowed.includes(key)) throw new Error(`Unknown property "${key}". Never put credentials in a debug URL.`); - } - if (!properties.appClientId || !properties.tenantId || - !(properties.directConnectUrl || (properties.environmentId && (properties.schemaName || properties.agentIdentifier)))) { - throw new Error('Provide appClientId, tenantId, and environmentId/schemaName (or an explicit directConnectUrl).'); - } - for (const flag of ['startConversation', 'showTyping', 'enableFileUpload', 'showReasoning', 'showToolCalls']) { - if (properties[flag] !== undefined && typeof properties[flag] !== 'boolean') throw new Error(`${flag} must be a boolean.`); - } - if (properties.starterPrompts !== undefined) { - if (!Array.isArray(properties.starterPrompts) || properties.starterPrompts.some(prompt => - !prompt || typeof prompt !== 'object' || Array.isArray(prompt) || - Object.keys(prompt).some(key => key !== 'title' && key !== 'text') || - typeof prompt.title !== 'string' || !prompt.title.trim() || typeof prompt.text !== 'string' || !prompt.text.trim() - )) throw new Error('starterPrompts must be an array of objects containing only a non-empty title and text.'); - } + validateProperties(properties); page.searchParams.set('loadSPFX', 'true'); page.searchParams.set('debugManifestsFile', manifest.href); page.searchParams.set('customActions', JSON.stringify({ diff --git a/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml b/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml index 5c19c586..4f4afd76 100644 --- a/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml +++ b/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml @@ -1,6 +1,7 @@
} + onRenderIcon={() => } ariaLabel={`Open ${agentTitle}`} aria-expanded={isPanelOpen} aria-haspopup="dialog" diff --git a/ui/embed/sharepoint-customizer/src/svg.d.ts b/ui/embed/sharepoint-customizer/src/svg.d.ts new file mode 100644 index 00000000..788ae0d8 --- /dev/null +++ b/ui/embed/sharepoint-customizer/src/svg.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const url: string; + export default url; +} diff --git a/ui/embed/sharepoint-customizer/tests/deploy.test.cjs b/ui/embed/sharepoint-customizer/tests/deploy.test.cjs new file mode 100644 index 00000000..a3f76db8 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/deploy.test.cjs @@ -0,0 +1,407 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs/promises'); +const os = require('node:os'); +const path = require('node:path'); +const { randomUUID } = require('node:crypto'); +const { zipSync, strToU8 } = require('fflate'); +const { + loadOptions, elementsXml, inspectPackage, packageFingerprint, runNode, + buildPackage, cli, deploy, matchingActions, compareVersions, poll +} = require('../scripts/deploy.cjs'); +const { validateProperties } = require('../scripts/properties.cjs'); + +const ROOT = path.resolve(__dirname, '..'); +const CLIENT = '11111111-1111-4111-8111-111111111111'; +const CATALOG_ID = '44444444-4444-4444-8444-444444444444'; +const ACTION_ID = '55555555-5555-4555-8555-555555555555'; +const SITE = 'https://contoso.sharepoint.com/sites/team'; +const CATALOG = 'https://contoso.sharepoint.com/sites/apps'; +const args = [ + '--site-url', SITE, '--app-catalog-url', CATALOG, '--app-client-id', CLIENT, + '--properties', 'tests/fixtures/deployment.json' +]; +const options = extra => loadOptions([...args, ...extra], ROOT); +const quiet = () => {}; + +async function temporary(t) { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "sidepane tests ' & é-")); + t.after(() => fs.rm(directory, { recursive: true, force: true })); + return directory; +} + +function packageEntries(config) { + const manifest = JSON.stringify({ + id: config.componentId, + loaderConfig: { + internalModuleBaseUrls: ['HTTPS://SPCLIENTSIDEASSETLIBRARY/'], + scriptResources: { main: { type: 'path', path: 'main.js' } } + } + }).replace(/"/g, '"'); + return { + 'AppManifest.xml': ``, + [`feature_${config.featureId}.xml`]: ``, + [`${config.featureId}/elements.xml`]: elementsXml(config), + [`${config.featureId}/Extension.xml`]: ``, + 'ClientSideAssets.xml': ``, + 'ClientSideAssets.xml.config.xml': `${randomUUID()}`, + 'ClientSideAssets/main.js': 'console.log("synthetic fixture");' + }; +} + +function archive(entries) { + return zipSync(Object.fromEntries(Object.entries(entries).map(([name, value]) => [name, strToU8(value)]))); +} + +function metadata(config, extra = {}) { + return { + ID: CATALOG_ID, ProductId: config.productId, AppCatalogVersion: config.version, + InstalledVersion: '', Deployed: true, CurrentVersionDeployed: true, + ContainsTenantWideExtension: false, SkipDeploymentFeature: false, + IsPackageDefaultSkipFeatureDeployment: false, IsValidAppPackage: true, + IsClientSideSolution: true, ...extra + }; +} + +function action(config, extra = {}) { + return { + Id: ACTION_ID, ClientSideComponentId: config.componentId, + Location: 'ClientSideExtension.ApplicationCustomizer', Scope: 3, + ClientSideComponentProperties: JSON.stringify(config.properties), ...extra + }; +} + +async function scenario(t, settings = {}) { + const config = { ...await options([]), ...settings.options }; + config.packagePath = path.join(await temporary(t), 'sidebar-agent.sppkg'); + const buffer = archive(packageEntries(config)); + const calls = []; + const state = { + app: settings.existing ? metadata(config, settings.existing) : undefined, + installed: settings.installed || '', + actions: settings.actions || (settings.installed ? [action(config)] : []), + builds: 0, siteReads: 0 + }; + const command = async commandArgs => { + calls.push(commandArgs); + const name = commandArgs.slice(0, commandArgs.indexOf('--output') > -1 ? commandArgs.indexOf('--output') : undefined).join(' '); + if (settings.fail && name.startsWith(settings.fail)) throw new Error('403 Access denied (fixture)'); + if (name === 'status') return settings.status === undefined ? { connectedAs: 'fixture', appTenant: config.properties.tenantId } : settings.status; + if (name === 'spo tenant appcatalogurl get') return settings.catalog || CATALOG; + if (name === 'spo tenant applicationcustomizer list') return settings.tenantActions || []; + if (name.startsWith('spo customaction list')) return state.actions; + if (name.startsWith('spo app list')) return state.app ? [state.app] : []; + if (name.startsWith('request')) { + const url = commandArgs[commandArgs.indexOf('--url') + 1]; + if (url.endsWith('/_api/web?$select=Url')) return { Url: SITE }; + if (url.includes('/Files?')) return { value: settings.files || [] }; + if (url.includes('/GetFileById(')) { + await fs.writeFile(commandArgs[commandArgs.indexOf('--filePath') + 1], settings.remotePackage || buffer); + return undefined; + } + if (url.includes('/AvailableApps/GetById(')) { + state.siteReads++; + if (state.pendingInstall && state.siteReads >= state.pendingInstall) { + state.installed = config.version; + state.actions = [action(config)]; + } + return metadata(config, { InstalledVersion: state.installed }); + } + } + if (name.startsWith('spo app add')) { + state.app = metadata(config, { Deployed: false, CurrentVersionDeployed: false }); + // The upload response is NOT the product ID and is deliberately ignored. + return { UniqueId: randomUUID(), Name: 'sidebar-agent.sppkg' }; + } + if (name.startsWith('spo app deploy')) { + state.app.Deployed = state.app.CurrentVersionDeployed = true; + return undefined; + } + if (name.startsWith('spo app install') || name.startsWith('spo app upgrade')) { + state.pendingInstall = state.siteReads + (settings.installDelay || 1); + return undefined; + } + if (name.startsWith('spo customaction set')) { + state.actions = [action(config, { ClientSideComponentProperties: commandArgs[commandArgs.indexOf('--clientSideComponentProperties') + 1] })]; + return undefined; + } + throw new Error(`Unexpected fixture command: ${name}`); + }; + const build = async () => { + state.builds++; + if (settings.buildFailure) throw new Error('Build failed (fixture)'); + await fs.writeFile(config.packagePath, buffer); + return inspectPackage(buffer, config); + }; + const run = () => deploy(config, { command, build, log: quiet, polling: { timeout: 200, interval: 0 } }); + const mutations = () => calls.filter(call => + ['spo app add', 'spo app deploy', 'spo app install', 'spo app upgrade', 'spo customaction set'].some(prefix => call.join(' ').startsWith(prefix))); + return { config, state, calls, run, mutations }; +} + +test('deployment loads supported settings and explicitly overrides the runtime app ID', async () => { + const config = await options([]); + assert.equal(config.properties.appClientId, CLIENT); + assert.equal(config.properties.showReasoning, true); + assert.equal(config.properties.showToolCalls, false); + assert.equal(config.siteUrl, SITE); + assert.equal(config.componentId, '4c6e29f2-7eee-4f9f-bbd2-20c8859d0ba2'); + assert.equal((await loadOptions(['--help'])).help, true); + assert.equal((await options(['--site-url', `${SITE}/`])).siteUrl, SITE); +}); + +for (const [name, extra, error] of [ + ['unknown option', ['--secret', 'bad'], /Unknown option/], + ['runtime client placeholder', ['--app-client-id', 'YOUR_APP_ID'], /GUID/], + ['HTTP site', ['--site-url', 'http://contoso.sharepoint.com/sites/team'], /HTTPS/], + ['non-SharePoint site', ['--site-url', 'https://example.com'], /SharePoint/], + ['URL credentials', ['--site-url', 'https://user:password@contoso.sharepoint.com'], /credentials/], + ['debug URL', ['--site-url', `${SITE}?loadSPFX=true`], /debug/], + ['page URL', ['--site-url', `${SITE}/SitePages/Home.aspx`], /page/], + ['other tenant catalog', ['--app-catalog-url', 'https://other.sharepoint.com/sites/apps'], /same SharePoint host/], + ['mutually exclusive modes', ['--dry-run', '--package-only'], /not both/] +]) { + test(`deployment rejects ${name}`, async () => assert.rejects(options(extra), error)); +} + +test('deployment requires explicit client ID/catalog but offline modes need no catalog', async () => { + const without = flag => args.filter((_, index) => args[index] !== flag && args[index - 1] !== flag); + await assert.rejects(loadOptions(without('--app-client-id'), ROOT), /--app-client-id must be a GUID/); + await assert.rejects(loadOptions(without('--app-catalog-url'), ROOT), /required for live/); + assert.equal((await loadOptions([...without('--app-catalog-url'), '--package-only'], ROOT)).packageOnly, true); + assert.equal((await loadOptions([...without('--app-catalog-url'), '--dry-run'], ROOT)).dryRun, true); +}); + +test('properties validator rejects credentials, bad types, malformed prompts and unsafe URLs', async () => { + const config = await options([]); + for (const extra of [ + { clientSecret: 'not-a-real-secret' }, { showReasoning: 'false' }, { agentTitle: 4 }, + { starterPrompts: [{ title: '', text: 'hello' }] }, { directConnectUrl: 'http://example.com' }, + { directConnectUrl: 'https://user:password@example.com' } + ]) assert.throws(() => validateProperties({ ...config.properties, ...extra })); +}); + +test('deployment rejects malformed JSON, placeholders, arrays and unknown properties from disk', async t => { + const file = path.join(await temporary(t), 'settings.json'); + for (const value of ['{', '[]', '{"tenantId":"YOUR_TENANT_ID"}', '{"token":"never"}']) { + await fs.writeFile(file, value); + await assert.rejects(options(['--properties', file])); + } +}); + +test('XML/package inspection round-trips special characters and rejects bad content', async () => { + const config = await options([]); + const entries = packageEntries(config); + assert.match(elementsXml(config), /&/); + assert.match(elementsXml(config), /</); + assert.match(elementsXml(config), /'/); + assert.equal(inspectPackage(archive(entries), config).entries, Object.keys(entries).length); + const rejects = (changes, expected) => assert.throws(() => inspectPackage(archive({ ...entries, ...changes }), config), expected); + rejects({ 'tenant.xml': '' }, /tenant-wide/); + rejects({ 'AppManifest.xml': '' }, /identity/); + rejects({ [`${config.featureId}/elements.xml`]: elementsXml({ ...config, properties: { ...config.properties, showToolCalls: true } }) }, /properties/); + rejects({ [`${config.featureId}/Extension.xml`]: entries[`${config.featureId}/Extension.xml`].replace('HTTPS://SPCLIENTSIDEASSETLIBRARY/', 'https://localhost:4321/') }, /localhost/); + rejects({ [`feature_${config.featureId}.xml`]: entries[`feature_${config.featureId}.xml`].replace('Scope="Web"', 'Scope="Site"') }, /identity/); + delete entries['ClientSideAssets/main.js']; + assert.throws(() => inspectPackage(archive(entries), config), /assets/); +}); + +test('package reuse ignores ZIP timestamps and generated packaging IDs, not code or properties', async () => { + const config = await options([]); + const first = archive(packageEntries(config)); + const nextEntries = packageEntries(config); + assert.equal(packageFingerprint(first), packageFingerprint(archive(nextEntries))); + nextEntries['ClientSideAssets/main.js'] += '\n// changed'; + assert.notEqual(packageFingerprint(first), packageFingerprint(archive(nextEntries))); + const changed = packageEntries({ ...config, properties: { ...config.properties, showReasoning: false } }); + assert.notEqual(packageFingerprint(first), packageFingerprint(archive(changed))); +}); + +test('version comparison is numeric and refuses malformed/out-of-range versions', () => { + assert.equal(compareVersions('1.10.0.0', '1.9.0.0'), 1); + assert.equal(compareVersions('1.0.0.0', '1.0.0.0'), 0); + assert.equal(compareVersions('1.0.0.0', '2.0.0.0'), -1); + assert.throws(() => compareVersions('1.0', '1.0.0.0'), /Invalid/); + assert.throws(() => compareVersions('65536.0.0.0', '1.0.0.0'), /Invalid/); +}); + +test('Node subprocess arguments preserve quotes, Unicode and shell metacharacters literally', async t => { + const directory = await temporary(t); + const file = path.join(directory, "runner ' & é.cjs"); + await fs.writeFile(file, 'console.log(JSON.stringify(process.argv.slice(2)));'); + const inputs = ['spaces here', '"quotes"', 'é', '$HOME', '$(echo injected)', '& echo injected', '{"title":"A & B"}']; + assert.deepEqual(JSON.parse(await runNode(file, inputs, { cwd: directory })), inputs); + await assert.rejects(runNode(file, [], { cwd: path.join(directory, 'missing') })); + await fs.writeFile(file, 'console.error("intentional failure"); process.exit(2);'); + await assert.rejects(runNode(file, []), /exited 2.*intentional failure/); + await fs.writeFile(file, 'setTimeout(() => {}, 10000);'); + await assert.rejects(runNode(file, [], { timeout: 50 }), /timed out/); +}); + +test('CLI wrapper uses the local JS entry, JSON output, and fails on invalid JSON', async () => { + const value = await cli(['status'], async (entry, commandArgs) => { + assert.match(entry.replace(/\\/g, '/'), /@pnp\/cli-microsoft365\/dist\/index\.js$/); + assert.deepEqual(commandArgs, ['status', '--output', 'json']); + return '"Logged out"'; + }); + assert.equal(value, 'Logged out'); + assert.equal(await cli(['spo', 'app', 'deploy'], async () => ''), undefined); + await assert.rejects(cli(['status'], async () => 'not JSON'), /invalid JSON/); + await assert.rejects(cli(['status'], async () => { throw new Error('denied'); }), /denied/); +}); + +for (const fail of [false, true]) { + test(`isolated staging preserves source and dependencies and cleans up on ${fail ? 'failure' : 'success'}`, async t => { + const root = await temporary(t); + const config = { ...await options([]), root, packagePath: path.join(root, 'sharepoint/solution/sidebar-agent.sppkg') }; + for (const name of ['src', 'config', 'sharepoint/assets', 'node_modules']) await fs.mkdir(path.join(root, name), { recursive: true }); + for (const name of [ + 'package.json', 'tsconfig.json', 'gulpfile.js', '.eslintrc.js', 'sharepoint/assets/elements.xml', 'node_modules/keep.txt', + 'config/config.json', 'config/package-solution.json', 'config/sass.json', 'config/write-manifests.json', 'config/deploy-azure-storage.json' + ]) { + await fs.writeFile(path.join(root, name), 'original'); + } + await fs.writeFile(path.join(root, 'private.local.json'), 'must not be staged'); + let staging; + const run = async (entry, commandArgs, settings) => { + staging = settings.cwd; + assert.notEqual(staging, root); + assert.equal(await fs.readFile(path.join(staging, 'sharepoint/assets/elements.xml'), 'utf8'), elementsXml(config)); + await assert.rejects(fs.access(path.join(staging, 'private.local.json')), { code: 'ENOENT' }); + await assert.rejects(fs.access(path.join(staging, 'config/deploy-azure-storage.json')), { code: 'ENOENT' }); + assert.equal(await fs.readFile(path.join(staging, 'node_modules/keep.txt'), 'utf8'), 'original'); + assert.ok(['bundle', 'package-solution'].includes(commandArgs[0])); + assert.equal(commandArgs[1], '--ship'); + if (fail) throw new Error('intentional build failure'); + if (commandArgs[0] === 'package-solution') { + await fs.mkdir(path.join(staging, 'sharepoint/solution'), { recursive: true }); + await fs.writeFile(path.join(staging, 'sharepoint/solution/sidebar-agent.sppkg'), archive(packageEntries(config))); + } + }; + if (fail) await assert.rejects(buildPackage(config, { run, log: quiet }), /intentional/); + else await buildPackage(config, { run, log: quiet }); + await assert.rejects(fs.access(staging), { code: 'ENOENT' }); + assert.equal(await fs.readFile(path.join(root, 'sharepoint/assets/elements.xml'), 'utf8'), 'original'); + assert.equal(await fs.readFile(path.join(root, 'node_modules/keep.txt'), 'utf8'), 'original'); + if (fail) await assert.rejects(fs.access(config.packagePath), { code: 'ENOENT' }); + else inspectPackage(await fs.readFile(config.packagePath), config); + }); +} + +test('dry run does not build or call CLI; package-only does not call CLI', async t => { + const dry = await scenario(t, { options: { dryRun: true } }); + await dry.run(); + assert.equal(dry.state.builds, 0); + assert.deepEqual(dry.calls, []); + const offline = await scenario(t, { options: { packageOnly: true } }); + await offline.run(); + assert.equal(offline.state.builds, 1); + assert.deepEqual(offline.calls, []); +}); + +test('first install uses the catalog ID, feature activation and verified asynchronous readback', async t => { + const fixture = await scenario(t, { installDelay: 3 }); + const report = await fixture.run(); + assert.equal(report.customActionId, ACTION_ID); + assert.equal(report.catalogAppId, CATALOG_ID); + assert.equal(report.version, fixture.config.version); + assert.deepEqual(report.properties, fixture.config.properties); + assert.deepEqual(fixture.mutations().map(call => call.slice(0, 3).join(' ')), ['spo app add', 'spo app deploy', 'spo app install']); + assert.ok(fixture.state.siteReads >= 4); + for (const call of fixture.mutations()) { + assert.ok(!call.includes('--skipFeatureDeployment')); + assert.ok(!call.includes('--overwrite')); + if (call.includes('--id')) assert.equal(call[call.indexOf('--id') + 1], CATALOG_ID); + } + const persisted = JSON.parse(await fs.readFile(path.join(path.dirname(fixture.config.packagePath), 'deployment.local.json'), 'utf8')); + assert.deepEqual(persisted, report); +}); + +test('identical rerun does not upload, install or duplicate the customizer', async t => { + const fixture = await scenario(t, { existing: {}, installed: '1.1.0.0' }); + await fixture.run(); + assert.deepEqual(fixture.mutations(), []); +}); + +test('rerun updates only drifted site properties using one literal JSON argument', async t => { + const config = await options([]); + const fixture = await scenario(t, { + existing: {}, installed: '1.1.0.0', + actions: [action(config, { ClientSideComponentProperties: '{"old":"settings"}' })] + }); + await fixture.run(); + const writes = fixture.mutations(); + assert.equal(writes.length, 1); + assert.deepEqual(writes[0].slice(0, 3), ['spo', 'customaction', 'set']); + assert.deepEqual(JSON.parse(writes[0].at(-1)), config.properties); +}); + +test('explicit newer-version replacement uploads, deploys and upgrades only the target', async t => { + const fixture = await scenario(t, { existing: { AppCatalogVersion: '1.0.0.0' }, installed: '1.0.0.0', options: { replace: true } }); + await fixture.run(); + const writes = fixture.mutations(); + assert.deepEqual(writes.map(call => call.slice(0, 3).join(' ')), ['spo app add', 'spo app deploy', 'spo app upgrade']); + assert.ok(writes[0].includes('--overwrite')); + assert.equal(writes[2][writes[2].indexOf('--siteUrl') + 1], SITE); +}); + +for (const [name, settings, error] of [ + ['logged out', { status: 'Logged out' }, /logged out/], + ['wrong tenant', { status: { connectedAs: 'fixture', appTenant: 'wrong' } }, /tenant/], + ['wrong catalog', { catalog: 'https://contoso.sharepoint.com/sites/wrong' }, /does not match/], + ['permission denial', { fail: 'spo app list' }, /403/], + ['filename collision', { files: [{ Name: 'sidebar-agent.sppkg' }] }, /filename/], + ['unapproved replacement', { existing: { AppCatalogVersion: '1.0.0.0' } }, /--replace/], + ['downgrade', { existing: { AppCatalogVersion: '9.0.0.0' }, options: { replace: true } }, /downgrade/], + ['tenant-wide package', { existing: { SkipDeploymentFeature: true } }, /activation scope/], + ['invalid package', { existing: { IsValidAppPackage: false, ErrorMessage: 'invalid XML' } }, /invalid XML/], + ['incomplete metadata', { existing: { InstalledVersion: undefined } }, /Incomplete/], + ['build failure', { buildFailure: true }, /Build failed/] +]) { + test(`deployment stops without remote writes on ${name}`, async t => { + const fixture = await scenario(t, settings); + await assert.rejects(fixture.run(), error); + assert.deepEqual(fixture.mutations(), []); + }); +} + +test('different content at the same version cannot be overwritten even with --replace', async t => { + const config = await options([]); + const entries = packageEntries(config); + entries['ClientSideAssets/main.js'] = 'different code'; + const fixture = await scenario(t, { existing: {}, installed: '1.1.0.0', remotePackage: archive(entries), options: { replace: true } }); + await assert.rejects(fixture.run(), /same version/); + assert.deepEqual(fixture.mutations(), []); +}); + +test('duplicate, Site-scoped and tenant-wide registrations are never silently removed', async t => { + const config = await options([]); + assert.throws(() => matchingActions([action(config), action(config)], config), /Conflicting/); + assert.throws(() => matchingActions([action(config, { Scope: 2 })], config), /Conflicting/); + const fixture = await scenario(t, { tenantActions: [{ TenantWideExtensionComponentId: config.componentId }] }); + await assert.rejects(fixture.run(), /tenant-wide registration/); + assert.deepEqual(fixture.mutations(), []); +}); + +test('standalone and orphaned actions stop installation before creating a duplicate', async t => { + const config = await options([]); + for (const existing of [undefined, {}]) { + const fixture = await scenario(t, { existing, actions: [action(config)] }); + await assert.rejects(fixture.run(), /standalone|orphaned/); + assert.deepEqual(fixture.mutations(), []); + } +}); + +test('failed install reports earlier mutations and does not write customizer properties', async t => { + const fixture = await scenario(t, { fail: 'spo app install' }); + await assert.rejects(fixture.run(), /stopped at "install site app".*403[\s\S]*uploaded package, deployed catalog package/); + assert.ok(!fixture.calls.some(call => call.join(' ').startsWith('spo customaction set'))); + await assert.rejects(fs.access(path.join(path.dirname(fixture.config.packagePath), 'deployment.local.json')), { code: 'ENOENT' }); +}); + +test('polling is bounded and does not swallow access/response errors', async () => { + await assert.rejects(poll(async () => false, Boolean, 'fixture', { timeout: 0, interval: 0 }), /Timed out.*fixture/); + await assert.rejects(poll(async () => { throw new Error('403'); }, Boolean, 'fixture'), /403/); + let reads = 0; + assert.equal(await poll(async () => ++reads, value => value === 3, 'fixture', { interval: 0 }), 3); +}); diff --git a/ui/embed/sharepoint-customizer/tests/fixtures/deployment.json b/ui/embed/sharepoint-customizer/tests/fixtures/deployment.json new file mode 100644 index 00000000..46a9f2f3 --- /dev/null +++ b/ui/embed/sharepoint-customizer/tests/fixtures/deployment.json @@ -0,0 +1,14 @@ +{ + "tenantId": "22222222-2222-4222-8222-222222222222", + "environmentId": "33333333-3333-4333-8333-333333333333", + "schemaName": "contoso_agent", + "agentTitle": "Team & \"Support\" 'Équipe'", + "startConversation": false, + "showTyping": true, + "enableFileUpload": true, + "showReasoning": true, + "showToolCalls": false, + "starterPrompts": [ + { "title": "Get started", "text": "How can you help R&D?" } + ] +} From 3888b5820e29b04e4017cdbcff64909f8a5a7418 Mon Sep 17 00:00:00 2001 From: adilei Date: Fri, 23 Oct 2026 18:58:11 +0300 Subject: [PATCH 4/7] Handle explicitly authorized upgrades from the original sidepane package Allow newer site-only packages to replace inactive legacy defaults only with --replace, continue refusing active global deployment, and document explicit SharePoint host selection to avoid extra Graph discovery permissions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 940e4d9a-9134-45e2-888a-1d9c5ef96c58 --- ui/embed/sharepoint-customizer/README.md | 5 +++++ .../sharepoint-customizer/scripts/deploy.cjs | 13 ++++++++----- .../sharepoint-customizer/tests/deploy.test.cjs | 17 ++++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/ui/embed/sharepoint-customizer/README.md b/ui/embed/sharepoint-customizer/README.md index 99f38f78..cb6a0d30 100644 --- a/ui/embed/sharepoint-customizer/README.md +++ b/ui/embed/sharepoint-customizer/README.md @@ -238,9 +238,12 @@ Complete **Setup** above first. You need an **existing tenant app catalog**, per ```text npm exec -- m365 login --authType deviceCode --appId DEPLOYER_CLIENT_ID --tenant YOUR_TENANT_ID npm exec -- m365 status + npm exec -- m365 spo set --url https://contoso.sharepoint.com npm exec -- m365 spo tenant appcatalogurl get ``` + Set the tenant's root SharePoint URL explicitly as shown; this avoids needing Graph `Sites.Read.All` just for CLI host autodiscovery. + 2. Use the discovered catalog URL, the target **site URL** (not a page/debug URL), and your public `properties.local.json` settings: ```text @@ -263,6 +266,8 @@ npm run deploy -- --site-url https://contoso.sharepoint.com/sites/team --app-cli An identical package is reused on subsequent runs; an installed app is not installed again, and drifted site properties are updated on the existing action. For changed code or packaged configuration, increase **both** solution and feature versions in `config/package-solution.json`, then add `--replace` to authorize the catalog update. The script refuses downgrades and different content at the same version. +An explicitly authorized upgrade can replace the original, older tenant-capable package if it was not deployed globally and no matching tenant-wide registration is active. The new package is still site-only. Active global deployment is never silently converted. + {: .warning } > A tenant catalog is shared. Replacing this solution can affect other installed sites, and its packaged settings become the defaults for future installations. `--replace` acknowledges that broader effect; it does not make the catalog update site-local. Do not run competing deployments or use replacement to configure unrelated sites blindly. diff --git a/ui/embed/sharepoint-customizer/scripts/deploy.cjs b/ui/embed/sharepoint-customizer/scripts/deploy.cjs index 45628f44..2b48ea5f 100644 --- a/ui/embed/sharepoint-customizer/scripts/deploy.cjs +++ b/ui/embed/sharepoint-customizer/scripts/deploy.cjs @@ -297,11 +297,13 @@ async function deploy(options, { command = cli, build = buildPackage, log = cons if (matches.length > 1) throw new Error('Multiple catalog apps have the solution ProductId.'); return matches[0]; }; - const checkApp = app => { + const checkApp = (app, allowLegacyDefaults = false) => { object(app, 'app metadata'); guid(app.ID, 'catalog app ID'); - if (String(app.ProductId).toLowerCase() !== options.productId || app.IsClientSideSolution !== true || app.ContainsTenantWideExtension !== false || - app.SkipDeploymentFeature !== false || app.IsPackageDefaultSkipFeatureDeployment !== false) { + if (String(app.ProductId).toLowerCase() !== options.productId || app.IsClientSideSolution !== true || + app.SkipDeploymentFeature !== false || typeof app.ContainsTenantWideExtension !== 'boolean' || + typeof app.IsPackageDefaultSkipFeatureDeployment !== 'boolean' || + (!allowLegacyDefaults && (app.ContainsTenantWideExtension || app.IsPackageDefaultSkipFeatureDeployment))) { throw new Error('Catalog app identity or activation scope conflicts with this site-only deployment.'); } if (app.IsValidAppPackage !== true) throw new Error(`Invalid catalog package: ${app.ErrorMessage || 'SharePoint rejected the package'}`); @@ -331,12 +333,13 @@ async function deploy(options, { command = cli, build = buildPackage, log = cons const initialActions = matchingActions(await getActions(), options); const existing = findApp(await getApps()); if (existing) { - checkApp(existing); if (compareVersions(options.version, existing.AppCatalogVersion) < 0) throw new Error('Refusing to downgrade the catalog package.'); if (compareVersions(options.version, existing.AppCatalogVersion) > 0 && !options.replace) { throw new Error('A catalog package already exists. Use --replace to authorize its upgrade; other installations can be affected.'); } - const installed = checkApp(await request(`${options.siteUrl}/_api/web/tenantappcatalog/AvailableApps/GetById('${existing.ID}')`)); + const replacingOlderPackage = options.replace && compareVersions(options.version, existing.AppCatalogVersion) > 0; + checkApp(existing, replacingOlderPackage); + const installed = checkApp(await request(`${options.siteUrl}/_api/web/tenantappcatalog/AvailableApps/GetById('${existing.ID}')`), replacingOlderPackage); if (installed.InstalledVersion && compareVersions(installed.InstalledVersion, options.version) > 0) throw new Error('Refusing to downgrade the site app.'); if (initialActions.length && !installed.InstalledVersion) throw new Error('An orphaned customizer already exists without an installed app. Resolve it explicitly before installation.'); } else { diff --git a/ui/embed/sharepoint-customizer/tests/deploy.test.cjs b/ui/embed/sharepoint-customizer/tests/deploy.test.cjs index a3f76db8..c8a88705 100644 --- a/ui/embed/sharepoint-customizer/tests/deploy.test.cjs +++ b/ui/embed/sharepoint-customizer/tests/deploy.test.cjs @@ -105,7 +105,7 @@ async function scenario(t, settings = {}) { state.installed = config.version; state.actions = [action(config)]; } - return metadata(config, { InstalledVersion: state.installed }); + return metadata(config, { ...state.app, InstalledVersion: state.installed }); } } if (name.startsWith('spo app add')) { @@ -345,6 +345,21 @@ test('explicit newer-version replacement uploads, deploys and upgrades only the assert.equal(writes[2][writes[2].indexOf('--siteUrl') + 1], SITE); }); +test('explicit upgrade accepts an older unused tenant-capable package, not active tenant deployment', async t => { + const existing = { + AppCatalogVersion: '1.0.0.0', ContainsTenantWideExtension: true, IsPackageDefaultSkipFeatureDeployment: true + }; + const fixture = await scenario(t, { existing, options: { replace: true } }); + await fixture.run(); + assert.deepEqual(fixture.mutations().map(call => call.slice(0, 3).join(' ')), ['spo app add', 'spo app deploy', 'spo app install']); + const active = await scenario(t, { existing: { ...existing, SkipDeploymentFeature: true }, options: { replace: true } }); + await assert.rejects(active.run(), /activation scope/); + assert.deepEqual(active.mutations(), []); + const unapproved = await scenario(t, { existing }); + await assert.rejects(unapproved.run(), /--replace/); + assert.deepEqual(unapproved.mutations(), []); +}); + for (const [name, settings, error] of [ ['logged out', { status: 'Logged out' }, /logged out/], ['wrong tenant', { status: { connectedAs: 'fixture', appTenant: 'wrong' } }, /tenant/], From 05b87a7f8b4a4ecdd8feba65777d974fe5f73770 Mon Sep 17 00:00:00 2001 From: adilei Date: Fri, 23 Oct 2026 19:02:38 +0300 Subject: [PATCH 5/7] Highlight the sidepane's current beta SDK dependency Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 940e4d9a-9134-45e2-888a-1d9c5ef96c58 --- ui/embed/sharepoint-customizer/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/embed/sharepoint-customizer/README.md b/ui/embed/sharepoint-customizer/README.md index cb6a0d30..1fc928c1 100644 --- a/ui/embed/sharepoint-customizer/README.md +++ b/ui/embed/sharepoint-customizer/README.md @@ -6,13 +6,16 @@ nav_order: 6 --- # Copilot Studio side panel for SharePoint +Beta SDK dependency +{: .label .label-yellow } + Embed a Microsoft-authenticated Copilot Studio agent in a SharePoint Framework (SPFx) Application Customizer. The panel uses the Microsoft 365 Agents SDK to discover the agent's conversation endpoints and WebChat's Fluent composer for messages, local files, cards, and suggested replies. {: .important } > This is an open-source reference sample, not a production-supported SharePoint channel. For production deployments, consider the [built-in SharePoint channel](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel). Its capabilities differ from this custom SDK integration; this sample does not enable file uploads in the built-in channel. {: .warning } -> Endpoint discovery currently requires the pinned public SDK preview, `1.10.0-beta.7.g65f69dfa27`. Stable SDK `1.9.1` does not contain discovery. WebChat and its Fluent theme are pinned together at `4.19.2`. Review preview dependency suitability before deploying. +> **This sample currently uses a beta SDK client.** Both `@microsoft/agents-copilotstudio-client` and `@microsoft/agents-activity` are pinned to `1.10.0-beta.7.g65f69dfa27`. The beta is required for endpoint discovery for now; stable SDK `1.9.1` does not contain discovery. These SDK dependencies are prerelease, so review their suitability before production use. WebChat and its Fluent theme use the released version `4.19.2`. ## Features From f55b226d0a4e7c2fe440967d718eb8cbf92178a5 Mon Sep 17 00:00:00 2001 From: adilei Date: Fri, 23 Oct 2026 19:10:20 +0300 Subject: [PATCH 6/7] Fix SharePoint feature activation and surface installation errors Remove the unsupported CustomAction Name attribute, prevent it from passing package inspection, and report actual SharePoint app-instance errors during installation polling. Ship corrected package v1.1.0.1 after live catalog and site verification. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 940e4d9a-9134-45e2-888a-1d9c5ef96c58 --- .../config/package-solution.json | 4 ++-- .../sharepoint-customizer/scripts/deploy.cjs | 24 +++++++++++++++++-- .../sharepoint/assets/elements.xml | 1 - .../tests/deploy.test.cjs | 24 +++++++++++++++---- 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/ui/embed/sharepoint-customizer/config/package-solution.json b/ui/embed/sharepoint-customizer/config/package-solution.json index 98633c88..1255abe9 100644 --- a/ui/embed/sharepoint-customizer/config/package-solution.json +++ b/ui/embed/sharepoint-customizer/config/package-solution.json @@ -3,7 +3,7 @@ "solution": { "name": "sidebar-agent-client-side-solution", "id": "54cbba6a-2459-43a9-80a4-bc0675a81ac0", - "version": "1.1.0.0", + "version": "1.1.0.1", "includeClientSideAssets": true, "skipFeatureDeployment": false, "isDomainIsolated": false, @@ -30,7 +30,7 @@ "title": "Application Extension - Deployment of custom action", "description": "Deploys a custom action with ClientSideComponentId association", "id": "f757a2ed-fea6-4478-abd2-080c4a1f6a84", - "version": "1.1.0.0", + "version": "1.1.0.1", "assets": { "elementManifests": [ "elements.xml" diff --git a/ui/embed/sharepoint-customizer/scripts/deploy.cjs b/ui/embed/sharepoint-customizer/scripts/deploy.cjs index 2b48ea5f..eb16295a 100644 --- a/ui/embed/sharepoint-customizer/scripts/deploy.cjs +++ b/ui/embed/sharepoint-customizer/scripts/deploy.cjs @@ -126,7 +126,7 @@ function xmlUnescape(value) { function elementsXml(options) { return ` - @@ -155,6 +155,7 @@ function inspectPackage(buffer, options) { if (/]*>/g)]; if (actions.length !== 1) throw new Error('Package must contain exactly one customizer activation.'); + if (/\sName="/.test(actions[0][0])) throw new Error('CustomAction Name is not supported by the SharePoint feature XML schema; use Title.'); const properties = actions[0][0].match(/ClientSideComponentProperties="([^"]*)"/); if (!properties || !isDeepStrictEqual(JSON.parse(xmlUnescape(properties[1])), options.properties) || !actions[0][0].includes(`ClientSideComponentId="${options.componentId}"`)) { @@ -388,7 +389,26 @@ async function deploy(options, { command = cli, build = buildPackage, log = cons checkApp(value); return value.Deployed === true && value.CurrentVersionDeployed === true && value.AppCatalogVersion === options.version; }, 'catalog deployment', polling); - const getSiteApp = async () => checkApp(await request(`${options.siteUrl}/_api/web/tenantappcatalog/AvailableApps/GetById('${app.ID}')`)); + const getSiteApp = async () => { + const state = checkApp(await request(`${options.siteUrl}/_api/web/tenantappcatalog/AvailableApps/GetById('${app.ID}')`)); + if (state.InstalledVersion !== options.version) { + const instances = array(await command(['spo', 'app', 'instance', 'list', '--siteUrl', options.siteUrl]), 'site app instances'); + for (const instance of instances.filter(value => String(value.ProductId).toLowerCase() === options.productId)) { + // AppTiles uses AppStatus: 6 = Initialized, 9 = Disabled (not AppInstanceStatus). + if (instance.AppStatus === 6 || instance.AppStatus === 9 || instance.AppStatus === 0) { + const instanceId = guid(instance.AppId, 'app instance ID'); + const details = object(await command(['request', '--url', + `${options.siteUrl}/_api/web/GetAppInstanceById('${instanceId}')/GetErrorDetails()`, + '--method', 'post', '--accept', 'application/json;odata=nometadata']), 'installation errors'); + const errors = array(details.value, 'installation errors'); + if (errors.length) { + throw new Error(`SharePoint installation failed: ${errors.map(error => error.ExceptionMessage || error.ErrorDetail).join('\n')}`); + } + } + } + } + return state; + }; let siteApp = await getSiteApp(); if (siteApp.InstalledVersion && compareVersions(siteApp.InstalledVersion, options.version) > 0) throw new Error('Refusing to downgrade the site app.'); if (!siteApp.InstalledVersion || compareVersions(siteApp.InstalledVersion, options.version) < 0) { diff --git a/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml b/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml index 4f4afd76..5c19c586 100644 --- a/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml +++ b/ui/embed/sharepoint-customizer/sharepoint/assets/elements.xml @@ -1,7 +1,6 @@ = state.pendingInstall) { + if (state.pendingInstall && state.siteReads >= state.pendingInstall && !settings.installFailure) { state.installed = config.version; state.actions = [action(config)]; } @@ -195,9 +202,11 @@ test('XML/package inspection round-trips special characters and rejects bad cont assert.match(elementsXml(config), /&/); assert.match(elementsXml(config), /</); assert.match(elementsXml(config), /'/); + assert.doesNotMatch(elementsXml(config), /\sName="/); assert.equal(inspectPackage(archive(entries), config).entries, Object.keys(entries).length); const rejects = (changes, expected) => assert.throws(() => inspectPackage(archive({ ...entries, ...changes }), config), expected); rejects({ 'tenant.xml': '' }, /tenant-wide/); + rejects({ [`${config.featureId}/elements.xml`]: elementsXml(config).replace('' }, /identity/); rejects({ [`${config.featureId}/elements.xml`]: elementsXml({ ...config, properties: { ...config.properties, showToolCalls: true } }) }, /properties/); rejects({ [`${config.featureId}/Extension.xml`]: entries[`${config.featureId}/Extension.xml`].replace('HTTPS://SPCLIENTSIDEASSETLIBRARY/', 'https://localhost:4321/') }, /localhost/); @@ -318,7 +327,7 @@ test('first install uses the catalog ID, feature activation and verified asynchr }); test('identical rerun does not upload, install or duplicate the customizer', async t => { - const fixture = await scenario(t, { existing: {}, installed: '1.1.0.0' }); + const fixture = await scenario(t, { existing: {}, installed: CURRENT_VERSION }); await fixture.run(); assert.deepEqual(fixture.mutations(), []); }); @@ -326,7 +335,7 @@ test('identical rerun does not upload, install or duplicate the customizer', asy test('rerun updates only drifted site properties using one literal JSON argument', async t => { const config = await options([]); const fixture = await scenario(t, { - existing: {}, installed: '1.1.0.0', + existing: {}, installed: CURRENT_VERSION, actions: [action(config, { ClientSideComponentProperties: '{"old":"settings"}' })] }); await fixture.run(); @@ -384,7 +393,7 @@ test('different content at the same version cannot be overwritten even with --re const config = await options([]); const entries = packageEntries(config); entries['ClientSideAssets/main.js'] = 'different code'; - const fixture = await scenario(t, { existing: {}, installed: '1.1.0.0', remotePackage: archive(entries), options: { replace: true } }); + const fixture = await scenario(t, { existing: {}, installed: CURRENT_VERSION, remotePackage: archive(entries), options: { replace: true } }); await assert.rejects(fixture.run(), /same version/); assert.deepEqual(fixture.mutations(), []); }); @@ -414,6 +423,13 @@ test('failed install reports earlier mutations and does not write customizer pro await assert.rejects(fs.access(path.join(path.dirname(fixture.config.packagePath), 'deployment.local.json')), { code: 'ENOENT' }); }); +test('SharePoint feature activation errors are reported immediately rather than timing out', async t => { + const fixture = await scenario(t, { installFailure: true }); + await assert.rejects(fixture.run(), /SharePoint installation failed: Feature XML validation failed/); + assert.ok(fixture.calls.some(call => call.includes('--method') && call.includes('post') && call.some(value => value.includes('/GetErrorDetails()')))); + assert.ok(!fixture.calls.some(call => call.join(' ').startsWith('spo customaction set'))); +}); + test('polling is bounded and does not swallow access/response errors', async () => { await assert.rejects(poll(async () => false, Boolean, 'fixture', { timeout: 0, interval: 0 }), /Timed out.*fixture/); await assert.rejects(poll(async () => { throw new Error('403'); }, Boolean, 'fixture'), /403/); From 66511252b458138baafa2851b3a9920dfe87ed61 Mon Sep 17 00:00:00 2001 From: adilei Date: Fri, 23 Oct 2026 19:20:39 +0300 Subject: [PATCH 7/7] Add sidepane screenshots and simplify user-facing documentation Show the deployed welcome, conversation, and activity experiences with requester names redacted. Retain the required Jekyll navigation front matter and beta notice, and describe GHCP and standard harness compatibility without endpoint or payload internals. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 940e4d9a-9134-45e2-888a-1d9c5ef96c58 --- ui/embed/README.md | 2 +- ui/embed/sharepoint-customizer/README.md | 87 ++++++++---------- .../images/sidepane-agent-activity.png | Bin 0 -> 162998 bytes .../images/sidepane-conversation.png | Bin 0 -> 173000 bytes .../images/sidepane-welcome.png | Bin 0 -> 55363 bytes 5 files changed, 40 insertions(+), 49 deletions(-) create mode 100644 ui/embed/sharepoint-customizer/images/sidepane-agent-activity.png create mode 100644 ui/embed/sharepoint-customizer/images/sidepane-conversation.png create mode 100644 ui/embed/sharepoint-customizer/images/sidepane-welcome.png diff --git a/ui/embed/README.md b/ui/embed/README.md index 84ead4f8..f4682dce 100644 --- a/ui/embed/README.md +++ b/ui/embed/README.md @@ -18,7 +18,7 @@ Plug a Copilot Studio chat agent into an existing platform. | [minimizable-widget/](./minimizable-widget/) | Minimizable/expandable WebChat widget for any website | | [pcf-canvas-app/](./pcf-canvas-app/) | PCF control to embed a chat agent in Power Apps canvas apps | | [servicenow-widget/](./servicenow-widget/) | Floating chat widget for ServiceNow Service Portal | -| [sharepoint-customizer/](./sharepoint-customizer/) | Fluent SharePoint side panel with SSO, endpoint discovery, local files, and optional agent activity | +| [sharepoint-customizer/](./sharepoint-customizer/) | Fluent SharePoint side panel for GHCP and standard harness agents, with SSO, local files, and optional agent activity | | [typeahead-suggestions/](./typeahead-suggestions/) | Typeahead suggestions for WebChat | | [WebChat React](../custom-ui/webchat-react/) | WebChat React client with auth (Node) — *M365 Agents SDK repo* | | [Web Client](../custom-ui/webclient/) | Web client with auth (Node) — *M365 Agents SDK repo* | diff --git a/ui/embed/sharepoint-customizer/README.md b/ui/embed/sharepoint-customizer/README.md index 1fc928c1..15185c7d 100644 --- a/ui/embed/sharepoint-customizer/README.md +++ b/ui/embed/sharepoint-customizer/README.md @@ -9,17 +9,25 @@ nav_order: 6 Beta SDK dependency {: .label .label-yellow } -Embed a Microsoft-authenticated Copilot Studio agent in a SharePoint Framework (SPFx) Application Customizer. The panel uses the Microsoft 365 Agents SDK to discover the agent's conversation endpoints and WebChat's Fluent composer for messages, local files, cards, and suggested replies. +Embed a Microsoft-authenticated Copilot Studio agent in a SharePoint Framework (SPFx) Application Customizer. The sample works with **both GHCP and standard harness agents**, using the Microsoft 365 Agents SDK and WebChat's Fluent composer for messages, local files, cards, and suggested replies. {: .important } > This is an open-source reference sample, not a production-supported SharePoint channel. For production deployments, consider the [built-in SharePoint channel](https://learn.microsoft.com/en-us/microsoft-copilot-studio/publication-add-bot-to-sharepoint#configure-the-sharepoint-channel). Its capabilities differ from this custom SDK integration; this sample does not enable file uploads in the built-in channel. {: .warning } -> **This sample currently uses a beta SDK client.** Both `@microsoft/agents-copilotstudio-client` and `@microsoft/agents-activity` are pinned to `1.10.0-beta.7.g65f69dfa27`. The beta is required for endpoint discovery for now; stable SDK `1.9.1` does not contain discovery. These SDK dependencies are prerelease, so review their suitability before production use. WebChat and its Fluent theme use the released version `4.19.2`. +> **This sample currently uses a beta SDK client.** Both `@microsoft/agents-copilotstudio-client` and `@microsoft/agents-activity` are pinned to `1.10.0-beta.7.g65f69dfa27`. These SDK dependencies are prerelease, so review their suitability before production use. WebChat and its Fluent theme use the released version `4.19.2`. + +## Screenshots + +The deployed side panel with an example SharePoint list-management agent. Select a screenshot to view it full size. Requester names are redacted in the list-results image. + +| Welcome and starters | Agent conversation | Agent activity and list results | +|----------------------|--------------------|---------------------------------| +| [Side panel welcome screen with a starter card, attachment picker, refresh, and collapse controls](./images/sidepane-welcome.png) | [Agent replying to the starter prompt with its SharePoint list-management capabilities](./images/sidepane-conversation.png) | [Follow-up conversation showing collapsed agent activity and SharePoint list results, with requester names redacted](./images/sidepane-agent-activity.png) | ## Features -- **Discovery-first connection:** configure an environment ID and agent schema name; the SDK resolves the correct published conversation endpoints. +- **Agent compatibility:** works with both GHCP and standard harness agents. Configure the environment ID and agent schema name; no harness-selection setting is needed. - **Microsoft sign-in:** try silent authentication with the current SharePoint user's account, then offer a **Sign in** button when interaction is required or the silent iframe times out. - **Clean Fluent panel:** responsive sizing, site theme integration, accessible controls, Escape/light-dismiss, and focus restoration. - **Choose the opening experience:** default to native Fluent starter cards without starting a conversation, or opt into the agent's conversation-start flow instead. @@ -38,28 +46,18 @@ There is no SharePoint/OneDrive picker, voice integration, or cross-page convers - A modern SharePoint site for local sideloading. An app catalog and deployment permissions are only needed for packaged installation. - Node.js **22.14 or later within v22**, npm, and the SPFx 1.21.1 toolchain. - For file analysis, an agent/runtime that accepts uploaded files. For standard-harness agents, enable **Settings > Generative AI > File processing capabilities > File uploads**. -- For activity details, a model/runtime that emits the supported client-visible activities described below. Enabling a UI flag does not enable server-side telemetry. +- For activity details, an agent that provides reasoning or tool activity to its users. Enabling a UI flag does not make an agent provide additional activity. -## Architecture +## How it works -The Application Customizer hosts a Fluent panel. It creates the chat only after the user opens it. MSAL acquires a Power Platform API token. By default, opening the panel does not start an agent conversation: the SDK discovers endpoints on the first submitted message through: +The Application Customizer adds a Copilot Studio launcher to SharePoint's supported Top placeholder. Opening it displays a Fluent chat panel that follows the site's theme and signs users in with Microsoft. -```text -GET https:///copilotstudio/agents//conversation-endpoints -``` +By default, the panel shows a welcome screen and starter cards; the agent conversation begins when the user sends a message. Set `startConversation: true` to use the agent's own welcome flow instead. -On the first submitted message, the adapter uses `startConversationStreaming(false)` to establish a conversation without firing the welcome event, then sends the message to its execute-turn endpoint. The create-conversation endpoint must not be treated as an execute-turn endpoint: it can return only a session ID and typing activity without processing a supplied message. Subsequent messages reuse the conversation. - -Setting `startConversation: true` instead performs discovery and starts the conversation on opening, without showing the sample's starter cards. The sample does not construct agentic-runtime routes or start a separate subscription connection. The SDK retains conversation IDs, including IDs returned only in response headers. - -A small adapter around `CopilotStudioWebChat.createConnection` validates local attachments before sending and normalizes two payload families: GHCP/agentic-loop `thought`/`toolCall` entities and standard-harness `DynamicPlan*` events. Both map to WebChat's native Schema.org `Message`/`HowTo` activities. WebChat's Fluent pre-chat and collapsible part-group components render the welcome screen, starter cards, and activity groups; there are no custom reasoning/tool React components or harness-selection settings. A shared incoming subscription replays activities so WebChat's startup and transcript subscriptions do not lose greetings. Only allowlisted reasoning text and tool labels/status are replayed; raw events, arguments, and results are not. Send acknowledgements do not end response monitoring: later response errors are still surfaced, and the composer stays disabled during file preparation and the response to prevent overlapping turns. - -The Fluent **copilot** variant and SharePoint theme colors provide the presentation. WebChat's style options are memoized across turn-state updates so response completion does not reset transcript scrolling; theme or upload-setting changes still refresh them. The sample retains released WebChat `4.19.2` and React `17`, compatible with this SPFx version. +Both GHCP and standard harness agents use the same panel and configuration. File attachments and optional reasoning/tool activity are available when supported by the configured agent. Collapsing the panel preserves the current conversation; **New conversation** clears it. The unmodified Copilot Studio SVG is from [Microsoft's official icon collection](https://learn.microsoft.com/en-us/power-platform/guidance/icons) and is included for this sample/training experience. Microsoft's separate icon usage terms apply; review them before reusing the logo in a production application. -`gulpfile.js` maps the SDK beta's Activity browser crypto shim to its **shipped JavaScript** file, because that beta's package mapping otherwise points SPFx at uncompiled TypeScript. No Node crypto polyfill or vendored SDK is required. - ## Setup ### 1. Configure Microsoft authentication @@ -80,7 +78,7 @@ If Power Platform API is missing from your tenant, see [Power Platform API authe In Copilot Studio, open the published agent's metadata and record its **environment ID** and **schema name**. Use the schema name, not the display name or agent GUID. -The recommended configuration omits `directConnectUrl`. Supplying one explicitly bypasses SDK discovery, even when environment/schema values are also present. This override remains available for existing deployments. +Use the environment ID and schema name for the normal configuration. Leave `directConnectUrl` unset unless you need the advanced connection override retained for existing deployments. ### 3. Install dependencies @@ -116,10 +114,10 @@ Create `properties.local.json` in the sample directory. Files named `*.local.jso |----------|----------|-----------------------| | `appClientId` | Yes | Entra SPA application ID | | `tenantId` | Yes | Directory/tenant ID | -| `environmentId` | For discovery | Power Platform environment ID | -| `schemaName` | For discovery | Agent schema name | +| `environmentId` | For the default connection | Power Platform environment ID | +| `schemaName` | For the default connection | Agent schema name | | `agentIdentifier` | No | Legacy alias for `schemaName`; canonical `schemaName` takes precedence | -| `directConnectUrl` | No | Explicit HTTPS connection URL; bypasses discovery | +| `directConnectUrl` | No | Advanced HTTPS connection override; leave unset for the normal configuration | | `agentTitle` | No | `Copilot Studio Agent` | | `headerBackgroundColor` | No | Optional launcher-bar CSS color override; otherwise follows the site theme | | `startConversation` | No | `false`: show the local welcome/starter cards and wait for the first submitted message. `true`: start the agent's conversation immediately and hide the local welcome/starter cards | @@ -184,12 +182,12 @@ Keep actions confined to the review tab. On completion, use `detach` for an atta Review the default configuration first, then generate URLs with `showReasoning` and `showToolCalls` enabled independently. Check: -- Discovery and a follow-up prompt that retains context. -- With the default `startConversation: false`, no conversation request until the first submission, editable starter text, and a follow-up that keeps the conversation ID. +- A reply and a follow-up prompt that retains context. +- With the default `startConversation: false`, editable starter text and a conversation that begins only after the first submission. - With `startConversation: true`, an agent-supplied welcome message and no local starter cards. - A small, synthetic local document whose contents the agent can identify. - Picker, paste/drop, attachment removal, rejection of oversized/unsupported files, and attachment-only messages. -- Supported reasoning/tool events, collapsed details, and no detail text when flags are disabled. +- Reasoning/tool activity when supported, collapsed details, and no detail text when flags are disabled. - Long streamed replies: follow the latest text through completion, but preserve the reading position when the user deliberately scrolls up. - Close/reopen, **New conversation**, keyboard focus/Escape, narrow viewports, and error/reconnect behavior. @@ -199,34 +197,27 @@ An upload appearing in the transcript is **not** proof that the agent processed The sample accepts DOCX, CSV, PDF, TXT, JPG/JPEG, PNG, WebP, and GIF filenames, with a **15 MiB (15 × 1024 × 1024 bytes)** individual-file cap. Empty files are rejected. The checks apply to picker, paste/drop, and outgoing attachments; invalid selections are removed with an explanation before sending. -Files are sent directly in the authenticated conversation as data-URL attachments with their name and MIME type. They are not uploaded into a SharePoint library or a new storage service. Failure to read a local blob aborts the send instead of transmitting an unusable `blob:` URL. +Files are sent directly in the authenticated conversation. They are not uploaded into a SharePoint library or a new storage service. If a local file cannot be read, the send is stopped and an error is shown. {: .note } -> This is a conservative client policy, not a guarantee of service acceptance. Standard-harness documentation describes a 15 MB file limit, nonanimated GIFs only, PDF page/processed-size restrictions, and text-character limits. The service must still validate actual content, animation, protection, and model limits. Agentic-runtime support can differ. See [Allow file input from users](https://learn.microsoft.com/en-us/microsoft-copilot-studio/image-input-analysis). +> This is a conservative client policy, not a guarantee that an agent can process every accepted file. Supported formats and processing limits can differ between agents. See [Allow file input from users](https://learn.microsoft.com/en-us/microsoft-copilot-studio/image-input-analysis). Do not upload sensitive files merely to test the sample. Earlier uploaded content may continue to count against the agent's conversation limits; start a new conversation when appropriate. ## Reasoning and tool visibility -**Agent activity** uses WebChat's native collapsible part groups, closed by default. Steps update in place while the response runs and remain in the transcript after it finishes. Each turn has its own group; WebChat owns rendering, focus, status icons, and expansion. +**Agent activity** is supported for both GHCP and standard harness agents. It appears in a collapsible group, closed by default, and remains in the conversation after the response finishes. -The adapter selects the mapping from the incoming payload, not the agent's URL or display name. Both flags remain independent and default to `false`. +The two settings are independent and both default to `false`; no harness-selection setting is needed. -| Runtime payload | `showReasoning` | `showToolCalls` | -|-----------------|-----------------|-----------------| -| GHCP / agentic loop | `entities[].type: "thought"` uses the entity's `text` | `entities[].type: "toolCall"` uses its display name and started/completed/failed status | -| Standard harness | `DynamicPlanStepTriggered.value.thought` becomes a per-step reasoning entry | `DynamicPlanReceived` provides names; `DynamicPlanStepTriggered` and `DynamicPlanStepBindUpdate` identify ongoing steps; `DynamicPlanStepFinished` finishes them | +| Setting | What it shows | +|---------|---------------| +| `showReasoning` | User-visible reasoning provided by the agent | +| `showToolCalls` | Tool names and activity status provided by the agent | -- GHCP thought deltas append only for explicit `chunkType: "delta"`; replacement/cumulative snapshots replace. Thought chain/step identifiers keep separate thoughts apart, with the stream ID (or the initial activity ID) used when the chain ID is empty. `toolCallId` correlates tools even when multiple invocations share one stream. -- Standard events use `valueType`, falling back to `name` when absent. Their top-level step `thought` is the planning explanation the published runtime sends to the client, not a token-by-token model reasoning stream. It is treated as a snapshot, keyed by plan and step; repeated updates replace rather than append. Tool display names survive clarification or connector-sign-in turns within the conversation. -- Native Schema.org messages tagged `AnalysisMessage` also respect `showReasoning`. The adapter uses WebChat's [documented reasoning activity format](https://github.com/microsoft/BotFramework-WebChat/blob/v4.19.2/docs/CHAIN_OF_THOUGHTS.md); it does not request hidden model chain-of-thought or retrieve authoring-only traces. -- `streamType: "informative"` alone is **not** evidence of reasoning. Tool notifications can use it too. Unclassified informative updates are omitted rather than guessed into a reasoning/tool entry. -- Tool arguments, filled/hidden parameters, descriptions, results, and arbitrary nested `thought` fields are not displayed. Standard reasoning is read only from the top-level `value.thought` of the supported step-trigger event. Binding updates never expose their arguments. -- Unknown events are not guessed into a tool call. A flag being on cannot make an agent emit missing events, and these event families are not guaranteed across all models/harnesses. -- **Started** means tool-step start or binding activity was received; it does not prove that a connector has executed. **Finished** means a finish event was received, not necessarily that the operation succeeded. -- The group completes when the turn finishes. Missing tool-finish events are explicitly labeled rather than invented; failed responses mark the group as interrupted. +Only activity made available by the agent is shown. These settings do not expose hidden model reasoning, tool arguments, or raw tool results. The amount of detail varies by agent. -Standard-harness plans can pause for clarification or a connector connection before executing a tool. A question or connection card is not evidence of tool completion. Regression tests cover both mappings and all four visibility combinations; verify successful connector execution separately in your environment. +Tools can pause for clarification or connector sign-in. A started step or connection card is not proof that an action completed successfully. Missing completion information is labeled, and failed responses show interrupted activity. Choose safe, read-only agent actions for review. Do not trigger a business-changing tool just to demonstrate the disclosure. @@ -245,9 +236,9 @@ Complete **Setup** above first. You need an **existing tenant app catalog**, per npm exec -- m365 spo tenant appcatalogurl get ``` - Set the tenant's root SharePoint URL explicitly as shown; this avoids needing Graph `Sites.Read.All` just for CLI host autodiscovery. + Set the tenant's root SharePoint URL explicitly as shown; this avoids requiring an additional Graph `Sites.Read.All` permission for CLI setup. -2. Use the discovered catalog URL, the target **site URL** (not a page/debug URL), and your public `properties.local.json` settings: +2. Use the returned catalog URL, the target **site URL** (not a page/debug URL), and your public `properties.local.json` settings: ```text npm run deploy -- --site-url https://contoso.sharepoint.com/sites/team --app-catalog-url https://contoso.sharepoint.com/sites/apps --app-client-id YOUR_APP_CLIENT_ID --properties properties.local.json @@ -299,21 +290,21 @@ The default package requires site installation (`skipFeatureDeployment: false`) ### Validation -Run `npm test` for configuration, attachment, dual-harness activity mapping, and deployment regression tests; `npm run build` checks the SPFx development build. The **SharePoint sidepane** GitHub Actions workflow runs tests and actual production `--package-only` builds on Windows, macOS, and Linux with synthetic settings. It has no tenant credentials and does not deploy to SharePoint. Live site installation, authentication/consent, and agent responses must be verified separately in your tenant. +Run `npm test` for configuration, attachments, GHCP and standard harness agent support, and deployment regression tests; `npm run build` checks the SPFx development build. The **SharePoint sidepane** GitHub Actions workflow runs tests and actual production `--package-only` builds on Windows, macOS, and Linux with synthetic settings. It has no tenant credentials and does not deploy to SharePoint. Live site installation, authentication/consent, and agent responses must be verified separately in your tenant. ## Troubleshooting | Symptom | Check | |---------|-------| | Debug scripts do not load | Local server, trusted HTTPS certificate, actual manifest URL, debug-script/local-network prompts, and tenant restrictions | -| No discovery request | Remove the explicit `directConnectUrl`; check environment ID and schema name | -| Discovery fails | Agent publication/sharing, tenant, delegated invocation permission, service availability; the sample does not silently switch transports | +| Agent does not connect | Verify the environment ID and schema name; leave `directConnectUrl` unset for the normal configuration | +| Agent is unavailable | Verify publication, sharing, tenant, delegated invocation permission, and service availability | | Sign-in/redirect failure | Register the exact SharePoint site redirect without a trailing slash, verify consent, and permit the interactive sign-in popup | | `AADSTS50011` | Add the exact **Redirect URI** shown in the panel to the indicated **Application ID** under **Authentication > Single-page application** in Entra. An unregistered URI can also leave an error inside the silent iframe and appear only as an MSAL timeout | | `monitor_window_timeout` during silent sign-in | Select **Sign in** to view Microsoft's interactive sign-in page and any detailed error. A timeout alone does not identify the cause. The panel preserves the full error text returned by MSAL and shows the client ID and redirect URI, but cannot read a cross-origin Microsoft error page that never redirects back. Do not relax iframe sandbox restrictions; see [MSAL timeout guidance](https://learn.microsoft.com/en-us/entra/msal/javascript/browser/errors#monitor_window_timeout) | | Passkey crashes in an automated browser | Use another tenant-approved sign-in method, or attach to a normally signed-in Edge instance; do not bypass MFA | | File appears but is not analyzed | Verify agent file-input settings, supported content/model/channel, and service-side limits | -| No reasoning/tool details | Enable the correct flag and verify that the published runtime actually sends a supported event family | +| No reasoning/tool details | Enable the correct flag and verify that the published agent provides that activity | | Response fails after sending | A request may already have reached the agent. **Reconnect** starts a new conversation; avoid blindly repeating operations with side effects | ## Resources diff --git a/ui/embed/sharepoint-customizer/images/sidepane-agent-activity.png b/ui/embed/sharepoint-customizer/images/sidepane-agent-activity.png new file mode 100644 index 0000000000000000000000000000000000000000..65eb7efb3ac8784aba37b65214c83ff1a2120452 GIT binary patch literal 162998 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

A5e55;{?XnsE7y2gxY=Gz+Zte?42cpZ3}dwO3<` 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

IfuFznm!Ac0bGB;mE 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π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�(7oJkVkiAPJ&woqFOvyR;Ha;5k~Jf{Y${Mzv+ zpstdB)DQ7cA6ADRK$L_U$w2%FFI|6Quht>>_`3_b`TP4pV#KD8{aykbLcJGmjq}Dt zv5D%D!#TLIwyp%gu8gRCV{@S5*=URdJ0wKmSoStIAAHKoj2;2F5|b|TNAg_9Fq;MV z8DpTmE^#{Z0@KLgx4(swxj+*h_;Dxp?pJQ^G151x)TV)9v9VIcA8TJ%+G4%_6a#%M z6z;;?ZwKS8R|X%e{Rk?x*U->-KS0M8{^&Kyt$Tu>IRc)z!ZMhmW3;uXWo={x5`;3V zaZqdf@;&m>ctj}4{Q(7P`f0#agnsT5$3#voF=Fa=#Fkk#e@JJYw-JW^wh~ZbG z-P-zFTU(`$m$2Oy0e#I^I)zjK{+!Q_bvEM0m6_Cm7x7X_Nr#!(*B47iM`va$%}5x; zj*3c3{(gQUpUqc*8vMd^eWp>8grk=)@y?KxLW~gGb?F)jez8b`H?c479*}T2T#fI_ zqs?pS{N=Pac2Rb5>xG1ySGl3!xUl%^qp1Wz=$UG*WBDqch8gPW7K4e&*$Hk?;o`s= zjjZR}Re=y^8Q4mW_f|6HUdd?6S7VDAgQWgZzFtybYwrFyU~#MYz#xFX_1f>w99F?Jkg$h^-5#d8fj_;L~Mn{i8O9!LEtEhDV>fgL| zb8b!>1;yDkcB!qyTAJQUp5(;oATRZu%hJlq70=!tC9=s7qz239U0 zc0{CM_a1n2fw&2AABJy0`)RrSOE>+XLhSViDD8 z4SrA=83yj79NFiUtQnC}l+~)ZNU+b@5y3E9| zVnBjk?Kh<>=nqdr@v%Lns6lO4q$A0A#qkPz#`U}v8C_lK&{pG~L}5@P#?=1We!^>T zMk5YInw6D5Uzv<9=oM(#zJw_O`VPn)i6LJaY;zAe%iP*Q=o8Z9gSk~fT=2*p)kT9J zrPPW)Pi~{}9kZgMqWSd~x3K&8mxgX(^*LWG3c@OwWHhm@xIaq+mW01wZK>g4pYULAmRRq+|Esplazm-b^3{Uw&}+8)QIO zf>X@vSObSJ7q^f1|9SI$5}t3O3jVSE)L6&f>{~ZcE?)~Di{h-6zLZMHr4vnsLV-qG ziggWjYf`s#)Eq+;<1o0o0Y|AN-pi;fuH3F%XqR8{!QZz;=tj#gkVI?gSCZpj1gZVKbnt~eW5~Y#RQTzv z_bKp-d>{JKqYN-#9^P_ptf^D90Zu)l-eVEXi(gUN$>|(yu)J}|`ZJQ+lYMU?_-P5M zoe!9}A7yWumOD@K;^H2-ATD$j0$^)#l8Lu`dUq=i76;h>n?8{Zz?;6gUWt;TMl>`w z!M5erwU9(c`|~H-4iER(?97a;64J25*jN@@0kQ{6iF%#6-=PFLgmn338*k)=)UU?|<5XTjX>6APOc1J4dHKm(bGbN%B-V zIHbGl<9V)O;^3@~SI7V^CM!reWv29^oj-InGciC($i4oL<)68^=;p$^+~)V6JZbLe zFh24N1M)T0yi}s0LL%}-*BiwX{MO6HYn=ILX~pE^ECw=6<{Vig%=HjfYeWf%rIvVyS!}oNr zf7lkdy9(GiEE%FmxqZ-+5J#|T7RK_zefW79GK9RR#oAE}CY zQDXM#1R@34)o>Sfj^g5sK60Oj=ctB#`4a7fihCW4^W$=I_NP^r$B+A)@jBJm0yK!x zm}_apo&@LadO?E!Su zbLOwel-Gu-Ti*2ugH+Opj*d^2mE2mCS`T$}wzr_{17O&dzq{Pp2)2uKx>O->{f8X{ zd|b66mnB{kgUPgRVP9|W4qFPuO+!|scBl}-z!6kw!65b6Jv>`Vq(2D8kE?p>kdqYHw8hHGALqZyosa*Nr zX=%Y8^lJ2N8SvVu*}ze+y`w`)qWwkX<>5+qc4RsuK{D3yPJ3Yv$_{-aqbC#kKQ)+% zp-Rum&VgYFN#0LM2^h62K=$4b)nX(EOK|G1l)M7!72NAj_l?+Hm3JtDUbXB(-`C&$d-_bxJHrMNj=8# z_jmBEn&|vxKa&;}#?TUhZ6+-SNsvWN&E>78;8r#ASu`)7ZJ7YKiTZN3@Q!&zK z6@Wd6$DX62p5l^oeF+YBy~s$-<<;5#(yEa9{=Egd%MKCK50lXw7t0`qgk;dQp87o~ zPAP~qSfeOKOHpbI7al{M)~YUkc@@p4e|iED51zRxQ81r_Yw6(F1tB!KYDLS#g)S1} z0#l2d_1)%B`mB?Y9WDSywlj7$zFupqYe!^Xtzy){yNH3>Df zYP~$Qtd}`2m6d7Y?~sx4Y8@jHm^ZK-cjotgGpb(0-bR@MV?x84IA5#PA+XZ_h3CA# zGKz7LZ_qcNQ12C-CxRKF=ipdX`FVKsS?vB^mGi{}_(HsrqCO8RDA>wuouOx zpyLqV-^!`6>56kfIBTj3s;b)OER=&3Q;aW~-w-1YQq;gFaUBZ8Lwsl|O-60tR-SS; z9sYiFUS$uhyv@cq(ZxqF2m;FxmW0ipp8g+9Mqd_g+5#?|B1t4DDB$CcBr(~Zqq90c zQ-(`+gFQShj`#f{o)qm^vB^p8bN2?ba_EaetYN)2ve(k1#r|7x?=0cC;Bhab*?1*f zj~g>>YOnAUS1k9URzl?IahKy`IqZ(FF-Mh@~sK$O`B>}p)8C-0jY5Dg^mU@&Sq0O_? zB26BZxw3S3#>%YhPl`qvP{k`9dicB&F zKt4{##PoZz>oJl_d-D7WOr1k%`2lH2sLXyNs{Dk3tuWJ{fsP(HY}i)?POGwVC)`Am zP+jhdzfbprI{?PXAV|HRG23b`m=)x?K7JVDDOkN9FFH&0kEW}r|<6{7#%4Z z`2G8HV4(f+oVbM?ONkqU4o`ok<`q6i7$9bZr|RaZ@%)+llT_`fRqzTPL10Ay>LEfH z4C>Uu+}Atne!%L2w1M=SUUqZ_bEIl6o1{j*c9aTkLiZ(JH4+Gtkn$s&N|kKG@iL=ob(Gi@TJj zye6D1-Ivs2(=W;`>YLo$9E2Y}#K6GNx)u7PzaO-%0Emc(Qs;mlz+8*mObxv6>%uhf zyC2tke^%gxP*?T4((!^6W21?r8_67rHIau&_GOL+IuOGQOc z_Iz!*9moCg6P72Cqz1%#5w)5H5Y=}T0Za?y@ev6b^~Wp^VD+CFD=S3vM1z(kPcQ&_ zLYw6wC=CN;&f@&8^}S&tiokrxA^P>i9jT=b+Gq&OsAqPNPS4ErGb0L8I`5U48mPgf z`VQb01b{<@Sy61#+0pU6un?{ppw_U-lsnDp28=l3)K`0W$mx9RrQPdB$Rs|#rf zDMcu~tk*IU6GOt|2=Cnv!y^xjmz}-Jr1PXUY%vAcPk;Wv_9Rp_U^1A}q)BWaQkakE4g}@qutxj4@d}X4$5xq?;gQI{(fz1X;u`czVwYLSoL~o zK%VhXYSsOH~A*( z;+*C?qLx>lxA`?*&U7|%-*S;3Y*Y9_7Q7>T=KFk`{4_p?O&15xdhZ)>;1J@x$;cY= zl=$Y|u_7ZP4Etk<4)%42@=MSg$09$iqU?HLfbRpFz%-e-DJAJI8uOp-Py{mTPa!bu z%j4tYTP%q5XfW*UWV`jY>%c|t*-&4F%?eC>6}GoFH_f+m9>Q#Yb|k67oyT###;WDI z3Ir5sZ&AQonm;q$y&+l|W?#Pl)bfy7&HyeYL?XkN2Hs_H8$*G>#xJd*@Dnfnr+@vT z{;XbVURM`4k)b8(fF9(8R6r>2mA0JlYKtM+Fy9&9trM6FrT{k3rhr>_tc z3nvUvl9y*|pjh_;mdyH^>UJGGc{2(M;LkN~2zlCia5aha(x4Cxe#UzZ(;UwG%4pj0 zG@Xufy3?xUYPdiG-bE|SEF0{DcW>FUKX%KsJM_Ufg${*^&lV^*NlEVRJZJ0U73})~ z4IWo3i7A@5_DOh+^_`q}E%KpQ1L$I|FEhHADHw znBL5=0qF>asOafKjIfWthK!40A1!z$r3<`XS+j^6OV^STGf@#zgltf1+&G}%I0k*e zih4mp!UNmRj;||B=O$2!qvMjHB7l&0^WwwL2CQCN+TSp2jPkuDyc1=;GI1RN4EVA) zOmau14a^YfwuQeq&mo|OZ6Cs=xC-`4jkq&v7X}#_LbDT~=xv_m;WBtYlCY+=O>W@lkIDh zbwv1FDDRe$J?WTmPdD~CB&Vg|W0*~pV<8~606!0-vNusHkyINwjYG-8I#uh1kl<%2J=l3=Bq^nmCA9MJ(Ua zlnfU5wP4AO0DZQMu#`QY(nZa7?LV+Qc6k_akbd&lMFF{kf~Mq@}25s6-59G4RvE!@o$ya1D%) zS6#TE6iJwXk(sWM5v)bCWgj}fWGFO$zDL75E-52{T?j!hE&z|iJOww8A0Jb%Q{-oN zw^*g_`ybZk^*K-)KyV#m%KT|*bAmq=CuB%RM@Ka_HgOm?0xKej{rij~U08ixVK4af zUvVjqj<#*4Wn{d9kfN26kz`|G{{!#T!ee^OLH{W^nQPLgQFHp&gI`Jw)s2j+M-EPQ zb}CsfYcMnB=M%L8M3M{)^?`K`;@gYW@YYsDEfHn|!}S9))ASw>!QY0V1wZl~K%rrn zVh*U$d5zk|&cVUXj=e;~*`J*wn2@0&s`V%y!h=0oEqpmMj;Yrzw0Niwqwd2f`QJl6 z_>_;|RuYqug+qI5!;RS7v4;UFX#Ht>vG+D`_sCC*qS>-O;=2$O|D493mZJ8r*C-rb zJv_8U(1t4x+&2`8!wvIl*G9ii0u;8L#q{*yLfvxv4$xQU77|Jyo60(qEqwdbNchFA~=Y)onD##0yBpurV|wbbC@FfA7!=$ zM#f_*O4(|+9mSzIlzv;4|LuNKFT=GB(#Ig%-I@^&bEJIM-q4TaX($GQ?ls zs8jzZpKAyWKSKjHw@mr?+39O6I8e}2Xui2^k{KHlLz!;e4fo8OxTlWGREl?y%%Zde z07}_?PF+@2P!+{Mu(CNXSB{H(02dT(5yg98Vrw}B7E1p{|Ip4XZy zEZH$xmUaNsr?;ON0q9Zh(!Pd8$OW62oDEMufNb7dLv; z;opU}Mf5m`($Z3B%MPFudXlRSqfss}w(TT+@;iI)3-Nua#GZEOXCcBI%#a93Np}vn zl4xY&rJ|7rK(Q*i_ZAA}Lk3S1@VQDOghYrk*2c`kcV zpgzi3SbX4kwmh(XyB#_)LBS*#)-W+K!Aunh!Z00p_I}d^sVw{QD7-+RP?dY*mNr+dzCi5frESwA%xLsLOR+xRMi zn`{sez*Vc4VL?|BFuju%CcAENA@{~rPM$)8nvtG9TSntsZ=Bg>k(&0xac?;+Vvt2w zqRne>1?Re|c$u?AQV;qK5~O+ew{)|~&-UcFr@Kp#tIVisC=;#t?8J;sfems0#L0C? zgbMM46`5^?`lShd(!4e&{h{fWo_4b2#@$CcjWehzLT^cO(bXN_KO9O_h`L=z(T}PvigK z+=)`HajYMy$iP+wGlz9pv(Vi&Oij-hw6ZWI70YD6Vt&30O zHO9l1-JW}1Y-msy%E zI^^c9=e^W|;D&{mvKD4G9x|s7+h1QBh3PMFkjMfLY9=leLIvE({>ycvAV2^rxICE+ zEBF z;}n=&UqgWIiJim@GzPL6KOiOt338 zqzGE7rOK^26M=+pBF+TZo^B^oM7Ie;#!aD3HxG3;H2?6Sryp{E8D5Cde*^||k~i*i z?85d*F{tu3zh1W14K;iFylU}l)LVX@Z9arIPe8OsWYMl75Ik8Wii^O+#?F-3r8oZo*nuJlx4 zAb_xulK<^nW+h4yiS&RKxYP9Z%5QN$g6xd%mX<)*p&=jq{d*nebi%@o?aFzZO5tCU zC~NMMBih>Ag~NCA)T=T_?PTtj;8UX1c6Dv+EY!knB1%I8rZ`ROxa>D5#^_7FealB# zkP7-~BkMrK0xY^URf9EJl@inIAryW-T5qEq;<)7$*nI3vUWcc)Q;4k_ zgxH?I9|h`TLG*i#`Q+?Z_fy1aLeQ`9i;K4=dS2taTMPtLpsP8zdGgpzQEWr?Qkoh% zwhlMN>E<+#g?6Wk40ry48`@550ZHkb^)f5HMzm@!HW+&Y?~^QIWqO_L&XHutI7s9M zi9KxwmIvTc;-G6yyP*M-Xbz=FvvC`U4lr5av^vkR+5&+|Mr!Iy*`ONR)|q~FN>5KO zuNQI1V6k^hfAlls0HYbmQl4%)cnW?2V^u~(h);KTM7Seas$bXwja^$ixxg1|qMF@J z)DG;^VOq;!!3>V+Q1Du}8_;|P>z<+jY2aYMo?V2^EZ=pGkbm{pwL*wD-04>~Bjop% zez90z<8ojasNGpet~l+ zEEJup9C@#+RFcb59^-g-4%dMFtUHFPstP2$LoP&fb0c7;044xICH|>2_>F@F3@G^O zkF5uD)#Q{E3e!fr;@HxXljnea3fB~nELK*I7n0IvgtwJc|i`y3J+ zT$e-eEj36Sgd>-!RJPMULz`ucM~49TWC|FhN=BC=0`Flz&4 z0ep0k$>3*^6k#X-j*gb00;@sLRa5+|H@X|$tN190^)+nFaCrt`ypxlYeHkA>NeO*0 zKrc^BM;>WTY!Zb|~&E-r9=e%FnFbf@f)44FCaLjRmWB zQwzkhS2>ZXD$3LF9e&rT8vFij^34F#INjq#(H z&WV~DbX?G8CxL2gW31o|=C4r98i5_Xv1UlyG$`Ex1_E==)uFO1Uax!@WPlQP{D*~> z?W-X$Xf-hzg%V)i@d83sb_I)bjbG4--s&e{J%!OO<(QYvBEMNe4+R%jbnq7sjmW~n zc3?_1bZtPf2~G|GwIJSkjAGJl^p;r>F&M2JmX`kCvfj?lDr~Ozey1E0Ssf{2qk8)4 zZB^ri-GOQJxhxMvx<6Z*wSv%L8FIdnk>csCk3c@kSi7pm0MMyu;2o=-rU1PzRMabc z)=MKr0N<{zD8s6`#HJh6N2PS}(&oLT-TCTkb93gh$DSTuZ)%)6q5bU`djt44Ks=AA zZz0a_@WtIF%*e#5EFS*`Ara6G!vYAPp*1MlX`a|@%{;W#Zh{~!Fx#esusgtb?s0WX zcS>>IeF5jm#q|n98v_87t$9cTzkt!B-SG0!`?APDuaZc@b3-A2eUfk{Rlp8@m~F%X zbSe|iJAO-qlLWyo80+oAy+d%br>XB?a8ma@8a zm-UKro(lLVkl(v^9r5cIQ)ze=G;Uynk&uv3`Uepp0z$cq8monyP1$mCa&Wr?Iq_y6 zp_n2P>|$Y{7||;AK-Wr|5rV>~0l+DeyTZZ##qt>Nqw4B3NPPA-Q;<*Z{~aFg|6z5t zFAaD7sX8Ux{-;Q`u;K4IS($#zNzH+q_kR=61N`!#zDar}VHb`*ce1p=Xz*9WW3?H_ zz!nxxkOu(4&QG7`S@+UtiHX0D59YNK62kr4m#rNDUP(jWrWJsGW-~jVQ)Ryod30{H zx5Fer`ESA~9NG9l&+x+Gd0@2WZ<2ZeszEi;dNpN|VujK(!>D zgHf)ab35;^PP=1hYn0oUtOG#~2i-j%VLZ@g4vNc*)1APUu7@wCiq>X_08OO(uy_U zjuX8_N~A!d+Eiu#6^l4ED-9GNTosAw0X41wrg(T<#i`%DzH4p0mx;wwH@gL*B?kJo z(H{l%>KqQdHA((#h}atQ=VND}oXLNc4FjN24wK{#No6fS54BjLUJt*1!z3dk(*!7*z*ph&na|XbHZX3l`7-uaX~Hnve-&IMbg^))RTos@!MV~&^{@IcYlPk-4y=kLH9 z52a)x2=mPEJ{(v{%(6_Ce)E?UJGP3M-zWOWs| zqkwA`#NF91cQ;2$>4J9%lx*Wu1A9Pve|`l6loc^0rh|+~QJxb)c9~9%!oBM_IP`1e z%N(wQ`&B_dwZXpe*|Q3+UF-zer*^xv&<+k54YvAgGexv(vqGvHAYkx_s}>n5g*7Go zvsC>Lj^E9A*i*6L+JHm`m?&92j*9BWyzv`=dqkHX!>qW6?1cQ6PvadAjji$efS66! zx_2;%o*Kp_QID(+cSD&vKY43M|1JF7OsSf3 zZ|NLADSb(ScmDz9yGf@rYMJOynW(P%=AbzKY=7^H%j?30Q-hIEN^e}SFo)Z3Z>f){ zNKx=e%Xqx_t@K;Rozwud5L9*!Qs79#?jo4hCzn+3T&H{N-Hp}B9F2k2xXpix1u=RM zDTgv}1+~WosuBzg41g1PKAtASpJYU?I&3>21vuHG#TLi15)w2@b^xa4aqrf zLRJ%(&LxAbpGIqsP=c|S)+?11P(>;)A~E{KPn z6^Fq!qd`n_k(3zBG8B}RaoGuhZ|SN_63xBzAxU(n)QS-e0kFKzT5I4gsfBBpG^uD*E-zRYD5n8 za~$tSH#_#fJkE!fsOZGM8{}z<#BZi3(9(7w6Tv5-R*2vPf=Iv|7K4t)Rn+Ga z5^b0AU`?$R<4Dyk3?gh#5ITyBi(6N`g{=?jaiE%t>0xzBi68AQLH+v5$;ruaxe(^Y z;H$*MQH+w4gUW;aj#~Q^2JZ@{i0_N1CU-8$xbPO~wm~yEB zU&nvvjDj$#F^>>$%K5N60UGjC84wE5g{Dcxv8MdHYbKbCemGQyCl0~{jRVPYF>vqQ zEW-?Kz03LYIwFq8IUyrM{9K|9O#a-ifi*yd-9y0V@d`WsPqtc=%+%t$K<7Aiqv8s0 zQIm~17aBM)*O_a(9wbU z(bkc}%r{U}W)2uZQpOfvibE4L^Puo5pl`Gx2>>vEvM4SN)?({*IXGYT2dh|^n~xbz zG}IDva;kn?y#=UnlkYzasnaMn-6`_L_x6))dM)jdvubLvT*DZoKCq>|eROC9GE06$ z&w$O@;bJglR~#C+Io@2;egL=6OwG+s!BhieAMrlv&E8X<*v);2sR0)U^aoc^{=c!B z=t+`28S}3+$6$->T9F95Zf@LSZP;F7fbOd-^9D zR5?((0Q(Hg>vi<>Fr1EeArcy%@v|mrk^t&IY;jMg9gq9zaO$(cH?^87nGyq)}TYDT;R-+{U8J8&b6*C*M; z^X@%P*!4WRd5ngDW^D9_f#7MZM9Ti^fd8R};P^GC5lIKGdFwH7@l`I$%*b+o&v~P& za?|ca4>%PbuTfz}x3f_LC0@{B);}x&tU>-+81=s2_21kq2^-voY{ZN56GY__ihAo&mKjY%EEW1tgD?VbL ztGc>6BjYYCn(!>Bjt&KpT7XIlZtsDMv8Eie$4`Ds+6W@}s9i?7rP6HS5f_qUY)7g`=xA3R?T+XyHN zA$t-A;1uedT$~x1ncKFNT$o8u%*L;}fS*~qf6ha@y<>8OP_vLbK^(}cV=-I+cgvJ4 zp~inMD0l_2y|dl(?~gg%rTs$A!t?E8%B^PWDHm=(5)A05()-m3g<)7x&rBZ{9uu!Oxb1JFm-1oQKzk2!T{<8=P8@duFcA_)9FHG;(?Bv=uPE2KVI zLn!>OUzeq))3S#tj5dplzd%P+^WRr9D>X~8t@5p(Hiw%X^Qhv+k8XWe?=R11|JG#I zg;7o4G6qUmrN6)6$HF58r`~@sW&~`rzjJo7mz6!1BkFZ$MNn_bfDOQXLVN3^|3{Fy zH!1h!0Y!ITpEuV-B@_ez|Gty3qey0#Y;JCX#h$3JsD{9Upv0d8BFoZs!pmT~qkQ(M z=$ZkD?-bLvQ|rEsjl%kGE(^g#(;n_< zwJGD9P;?Qw1MQF`@Ya6#=U0N>WOOqCM^VFW38C4+ftjOqn< zZ()OS1w8KlK+~hGZLF_v3&IXT3|z5~>P&&x5MW6;UU87V91AGq!18M!;sKa3WW;=I z3gkLEzlR{8C%S2k2*k|I&US11rv@a@JAQJ%@9I25>Gf-}LCA3-;yaLE64-2guqi7q z?-nhUivj`gZ)ZvxEG&QAc@wH4Px0WEgq)m)BleXruSS4Y-IkMID z^oRT~-;dwNRg*D)>tv@A{bi4u0Q-Lj>ikN8fkGHyq2&Uvk9}#uKkb9HFI4-oF+U4#9^?vrf3KjSukR6)3G-g@ zkCvAR8iAdKMZpY)we@=x(Enx8Q(l#yX&<>gACW7bvcEol0!A3+<#rIHH`!IAL0(zS zKqyFw5PR_=oEF=^f)mt%GY0z9mmt@p;Mk6IlUZuz9yRM&v~)#?iAVSZ1Y~qsV@htp zn%TdG&F8U1C>Hs5X}Kr;AfLL+Ma;xa7!{4Sy?L!2f_b9d?5z{7Dh!PPM3cuciBHr zv3Um^(m3W6lGS^6(-l>q?I=}J@^b(Ot?0rHxM+Hfj|qxaUwA$$WbZ#o}Vv!I`iJ%p8NU)pbi$4%803*BjEZ= zONjx-4XnAlcvCXrqo63^t8~Ui?C;;;OF5D#sR1tI;E|e=^7y~VQ0j3XD6hfQ5*xuw z^Wu=l5t}M&669WlJ_9k3|G4gH4~Pd@pr}}HmU7OV^7&D_Uy7eze{XLzw+=SqFm9{r z!x~9hu7son=yvVgegt9u7s@bvkBH;6N`>&d=g-e)9{LNrxqAFXY~V~Pj&UuHSCmk! zgn|ej26DtWO#l=@W7DcT41~UiDsvO#6-I;E;!*_Hr-|;~^>|F&jraGy`1&Z-S$D!q zSDWqa(raJEK53t%&_38nnAEG^!>n5Qu{zAEd-?_?|7K!?=E;1Iv2Ltv9fn+wsI3Mv z!+z_T7-yRRo4H2VF-b0i#=)T};2uM--Twj(i^kuIi1f5?A~XP1#2WJX>#u? zu(c0MpYq1T&2j?f#kbOcGY=kWfOML^s#zZyV1Ng5|NY6Z)qMYc-SZFzHO2Q|0IJH$ zG71Re9vnYwkC5evSz9XGhrvEaaWTLLfQcaQM3O2d01KR9f)v=iW&x14g|>Ua59^FjG6;)4BolAx(d7_SbxBa>BTLBq(n1}Mql zb0FD)ErvAc#+-^igAv>I{=Lz7K6^qUQc@yP1_m0v!6wr`ZDHVtbMrB;#!n$(w8k@x zeMs}Fv^|d%NB~)i>1c74rq&tYQ^Y}IOOXb_P_2ns=J5V=Bk1h zPa9l5NK8Zk#+^~`?tcrC7G^>Gu};BK;fD#=5hNhv#nvm(r$N;Nf1q0$EiO{6(#-*3 zzgTZKSSO8@I#mI+bh1kZEEb0wKSJ~Z)Ugq^uyVj;e`_v7L9Z*W%ywG9kltIC%E-|0 zMLdhv>W~GW=h9+-HBZ%HUK9^Q^v986u0tSXLc+ z_d472)jOslmx#_x7I-9m_y%DCqhNga{EeubxWR5Di!G(0zW(CUa%x)I=&RA7yra#! zD~QF%!b60Yr~W=wTkMxf{2uq-q0g=*xomU@(B~q)nC5P$r^Lkl?*nI`rDTdLeT|HK zB_}tL8gMU;(X(s8=3lLs4@Mts)sF}n*eL6ZrAg@oGY4J~ZHQFz_XPA^C~zcvd6udW&1z0 z)h)RJ4!n~^qBc;M(#XX9rvGIRSP?i;fj2Zr0ik9E!Ko6Udb*ZF#;~{eZWRN+)IIo1 z%*k72<>u~gAD`EsC*2w~0j7&~(C)59f!sI<%kCc-5UeM>9FG(=nmp%1pfTgPSZ=(U zt(G9h@q(fuPWgGNcyEs^Gmfl&<)mkHd3jQ3C{ho~;oth0&%H#FV3rAg?C&&a3H*Ga z9n7E;!*@?SW=nB4KQtiP3X@1F+h2X1r4B9PpFYW9?7W`op zv%PHzyJGyg&i7_>=vW!7RPJ`K_l=WG6L$O6Xg1uJ*kQ()0884)<)jo$>NdNj=P~oZqpyhj&yaJfCI|&TUpFl(7iS(c8b1GyK7E>sqEeB3 zF$Ory*whUkEhzYxhjaO#$UbYoBjr0r{~2Ps@<%U?G@K?)^`y!%{AVUf(kZjyT{=8w zoJ#kljr%_ziYb&N9CW2NTBDax%I8P#f4)j&P)d*u&P($a{~Z{>{XJKB0bfV22DXh= z!l|y$Cx_2wds3y!pzd-zY)7&*C{+2^!cN$2TE;Khs_FE!ZGE&9S)6kZ{Qfz{VW|t( zge_VEI5KJx+J-}c>`yc;OthM5y1IJW5rjm_<{!Cv(OKI@%FEZXaXM9tWP?)94fQ@eMI@IwJh%8Lj7=HUd1;WqJZO-h{rhHp{!HS{7b?`x z&KaJ04yT&LtnX82Vk4=<|2YpX^O|e{>`!70u+MS*=6l}1^`*fhB#aR9@Wis^rokxB zjpGdyNkU8&bC@u+DR5>_1?9IqeoicVzM!2+>yPZa$Y~70Lx7K1CLk?v{?Ae2z%DSZ z;P@IzqsX{~eF<~M!bWxv7MYBGmPpt_gV!MVQEF)kGP7eV%TkmW1e1cgp4Uy+5d%$6 zpNVG2W#D6&Zc5OrHsw5|~?W=|Q33{)Pka z_TwTW@Z>NsSOWSMoGXB`oSOoH43)bO#^-!qY52zv(5e|R6+=UCNCO8RzS$Eorhsq^9!h3c1g+vX1mZRV{H`nA5(_EEFRb5bJCyr= z_Nz3ub_DMK+F03a>ufVOS)c0tzPo^k@&4j#~pMqhx?Y8IJ38+3{d@m#POiXdg2ESjk>A| zo2=tvNADXR%+d+62p5Ux`)oDjOc4w;GzaQstK{~R^U5uJJHtKwD9*3$7N*LSkADGF zBieMgV=cD-$ESWpwp)}Rg>`rX0m5~WK>6f`ViDCu6Qx+3KT zoWpmHQc}y8EeD8~a#a_OPQfv%RS6$l)xal`Ibm~QOd?^*dAoEBC|tk*0XM*0Q=%H1 z4anfUv84yz?GUkgp`)V*W#%$?p)fG`=b6KH4ffacoko;YRCD#-2jmkq3zi>c;&{&_ zNAyR2EYW9XQeZ&5hfmkU1dV{8V7c|0*YS=S6_RRhE+h0i>-Xl$%Jb9q-!1T+rW2(- zCF+x4(|J)~9TyyIu{c!+you_MC+lV_X4iZ9^_N}vB-y0vXr*&vOmD)f01GQ^oTyg) z!VbW^Zj}z~cJ}lE^bNq3#_|vtD|Z)2&?FW~kuyNjBjAi+9z`a)IWRa@ttNL=YMQeD z+=afqB?RdyVQu-Im4!V)4p{mI07#&r9XWse!}synBPiJ$*G9G6_)dTb@J1fz-nxx> z6=035Lgss5v+Tg}WAJJbe zEg0a-gUl+RCG<#b+h2MbVBPp&VR0n$rdZ_kEDmgNcGm_EWOA);#GI|ZM(pJw`3-~6 z1r#t&Uq>&sUC1JFg~mM$Zah<}{lh>@H~Zn<(BmwU2M8EyxVi|W$kQp(d&$wdKn^@Z zPj7o0141#=0T&GeKOUAi8Hmx+(z-?^xCQ|h5W#)c8AeV-D(awCsvitR2BIU9#hsb? z9n0%OZz#p4g78iYp$`Q2g8(8KN$H2YHmm*ObKoU0GVoeT^zRlH{k0Q`tbf|)G9z^b z0es0_FsK1}A%Z^*)P`hi=2NxkmDT*Tr*Q{w!ztV~2x^&ePJ$nMyNUW9;VYxfUOT*x zAPEymkZ1lRks?2*lc9)R6V2f5?HMfmq!s7_=JY7=FmMyo4Jn*`LL%y^6aFV1ZCqv9DR1(X}OLs^}1fwkH}|frZ7gp`q^lJUfYFYD!8$q!gH09osiZ2(|qLVpGAZ zY+#1M?}9%G##u|)v%vQ-JUlfe!@yMIfn&*+IOqXZSB)Vwa@qaV;o=;e^oqWH>&}O~ zW-qUzXcj{yz|}%HK762nUSbQ#X8^E4UK8NdSy8TiU}5opk@nv4SpWU||5@5m%E%~X zg-EhxHIz+>B%|!i%#c||B-w<_kP)&+Mpj0N2wB+)nPp~t57+w|@9Xyce7?UweqDcD zx0^S0I?vbZ^?Z!uxF4-;A>^Sjo~h^Ey*3sEfkH?~NN=wO&Jq$5KbpsTy|lx^!Wwnj zazbzTLqcri=-qvfX{gti9Flg=ms6+L)jx|4!e3euhT3pn3$S=#N2^)f|7Ti=f$K% ztJeAXeZ(X-soTp*iS~JIyP86v@_Z!xP$`A{9f3>E3)e|@>%5haT3X4YybQ91DRs=d zA@@uSOaWI9w=6yRzC9N#syuvh$3D_4>LD=ooB6g5>VyV{1<2+Q zzTx9a$9wk;Qmdh)!8%?Z($XPjSycCOa<1T^6g!Pu85ry;mIC8j*mdwxO{?~k2h>K^+J&9IEma*BI z*e9;+o0_ngH!rVlKR@k!S2WLP0=)%UaATmK0}BV(4kj0ug^6}Z>(1+c41l?E%W01x1eUANqad|o zxq?p$%pLLFyFNvV*7fy83x56gg0QdKxb3|kvUG~-SUhE-xg8oySANyr{)KlJXOEG8-cHQ=+OxCI00PD4%dIgw6Q2~Y<`jh0#qss^KXz;O(87P_DN|e?_zvpc zK$YiyNI)AJRt7JgfCtW8dvMY*InqGRSW^gHi{o+e1LDLA7t^niy_j2Am_KK5Z^oj# za|zfyfqT90)|r8~eM~|-efAz50Z*fuujN~l6eoM|Uq0S`V}W=^hKpJnravJKg)yjJ z>oZW`={K_Id!Ip;xA0T#{OIt0@5YFK>}~55on`Eo%gY>rSMZUgrlOLI;I;+#ZYbIn zq7*C{Jam7jTzeUQ`nHx|(HYL;a8*MVeN9V?&P4(X@V}THBhw*~ja>+Qzj^cd`Pt$1 zEnePahgovPuFy2&qi7I2zn(|?hxyxE=Rf!9jS~0^-vC2!2{SvB7w5d)H<1tZkm%3) z`kJ>3vO+pfs~5H#YP&B_CO8aOidcLYuTW!=4@=z3sabkgSGVWts*#Yx%8k`s^bhIT zo`w|MFOc98kxBB9Svf>?kcbd>hc}HQ_RPmKtjgEIuB*H@PtJXl_wmywuSPfewpV!a z*v~;!o9@7@QSLO3H84ZGI2u|fXGQ3uzmKH0dB{YDg7q7QiZ&@WR1cAyV1x z?YuhTQ(swA;{o+PRKY&6%{sYES}Yh|LXK7Q(_c3DL-=XoY3Bspx@WD*F0k{iE*&43 znR%SWaHWWZrDi7#KxRFKv{Y22^Xzl%r<=sD+0h<7I#S#b{Bz#>!&%tO_K=W>B5PK& zr{J*b@;uvp}htW1HwIH zWi@~7hYbd}2Og6F1>@N96D;fV8Sa|<1Wa!>OUbl;Mgqb{7Xxstu#@ep=3+N7QK#*h zK?|{Rcw%Pe$ja&Y-r`>)q7NTEdPMOd90G5p6lKJA(9-r`&Nb^jnz|Ot&W-LU|A`GF0LakM<@a4b2PyBynh+chIAb94qzoRPbnI@a zfAh+u8EgdqTbLBV;uZR*b1ma;m7Q<0@ID8hR1k#7GC0@TZ&)gF*E#!Hw~E+UoZ`?UntbTc z;9Rn9F}$MGiV#f*M;QIC?43~sF9S)SlP&R=mgqzw-h*s^i{688!E*SS)jhsvwKlnE z=7n!;^hJ)ND!%bc<;4PCePzuHk^GEZ(Zh=DxxgB$J((sYW2T@6BL!?UMDSNA3;t*}=KFj~-Rk9XxJH zu@#Ni>_f(WBa?h3^ch$nK0YN6dN*4B>G`8CIi{ux<3R2WGL>9iDo)alIjo8_hv z^_DV8H_A^HnDso-e!gtP;_Q@2p1kc&+jk^<5cjGD1SC5Cw&*L@-R%$_+pxygViJ5& zdX0W$Y%EJF|4E9g^?u)uu#EmPSJhiek6hoH{VDhmp5lsXWIWwgl*w~EP8$q=cd7nf! zKI1qA*Iy2|Q+0ew${stbb-c%^!7Lc~cij6b2&MhnM|_r^`;Tx11%Z2^RcuUQ={a0i zh8_jg0+iWZNKHxz#W`*yj^~Em}&5e z+s-G#iN3V=_`F|Lbv2Y(XpbDf|193(98hd5HbqV3$F$#G95WKQhm-AvuxkvLZZ+Q= z9vKJedXFO|dj$sLaFujbtUfLzvDH4I_^@R<`28brgJ{=PR&rh4VL+G(?|!QBfJ4J= zw)*?lPG9dQPlhbMj(xg)7lEhcAt+CNy?lHSvpPp471R0KjW(*_q;*PD%XaF5O!!l* zxWI#tR5>kKuM@cSC%!<&q-YxB1ZDfD7hM<;9y~5)V{J|3h7vCePXQ*u<$l+(tEs%D zAN8icm2KU_`#ibLx@`dL^_B%ClZE-@lY?Z4}7@o%| zoGIN5`nB_;Sm*<0qbY2s*7KD#hSA(y2|}dj7EToM%M{}(6+%j$+h^14d37bk3iFBNtB${ZLeIuu{hnI`nLXa zWxv&_oIgv`vK$n?wN+dnKQ|b4{~j+xJgurKksH)fdeS8(h2}>%wH|B^4o4NvJade- zu(1fSXLE2^IVNmZwmECL6jSCw@aShr@fm(H z4cu&Kx)$AW;flf1w5Z1(Rz}9$e<)KN#9}wc+v;M^eW>$?GZzR&f5+Gpc1-*C`(m;E zr81eHYqsqyuAK2_Xx1W2&iU6>%bRSh+i}vDZg(p$--pE*edg|RtMgr-2{H~ zZV9Q@qMZB4#HgVmOlV8611R!O;)IX!4gw5#F=Y>=t+UR=<5sDv3KB6+Q8sSYDx+aK zXNCDwSVm9%0eVKsXH^2)h>4IB!&BJ>Te$j!cr;$+6|sSV7oNwRFRP+c<+xeoO`t<2X%neb6o{bOqS*u0ycgYJ`5U)!Y zXd;ITI+1J2$;LMRrTEwUD7$yE>eZ{WzcpTgcpDHfySk80Qy0kVUttU=wTWm03mQ28 zSgvcU`)XL;qrf4vS?${{Of3R_^srL4Rb2XN`sgm5cok|oQBuDn?p(qwg>i4{ zV-ytS6^d=AA|EBRZa!EDP`!$^$i;_*PVs9Pl6f+)Gs*@B>RYx34Kwhn=-d7#HvXM* zv(#Ri-Tek6s1iz#anqP}l?pKl>aBZ`ESD~BtP4~6UM|huq!y<;_3}-z&Cee*V~^Qy zp&OlDs)WU78a(^@ry_UWxk(;8eo_BhQJM^&QR@h^YP2H?y{4 zO&?ER_l)lPb=5kqpOx;%O^7HcLMMo9q3F4#rZzaU_eAZTs)HTddcQDn{G4o7udffv zx`Fq1c~0?y!{Q6qxz1NXIaau(_K*zWpg(NgViVU^R;DO*?`T(zgoK3K4#OfPBNt(- zFLw|9Lc$~oCZA9d1Pp)eEp;Fxz-m+@x`tvVI$E4UBF?F<+4Gd;g=s{c5_t@gy+I`nGf2l%(3S!e;zAj z9ae85i(G)p&vOgus+hWXm|Rd9mZD+05trn-bE{d}?|ISaDK7+~hlorFKRU`BflOQN zKej#nY!f}f#N~juz1&`rfSTJqMKZzBNeqo2>F1MBalu~N0cy80P-E;Y@H8wJ$d`1rKOIZC4%y4gHY4PBg z)vy6Zk7zo9d|@i;?c29A5jH4U>9&|Wm z?BhX1@{#6PNy*DiF??-(x-q%A))@&5|AUO|?pGVl&UMiJE10Fg2D zTd@}r0ve?=ERS9=3u45Q;3N+fSSn2hEId?%cf)f<8k}lP*FL0PQrDX}&s)@F!_zKT zeLEupf;c&qDxflSCtTN6v--8l9TYEX%AW98-(7YtFPr;r5^Z?Z@5OWTi%sl?;R2z7L1y2Vz~PgTYPV!v8L_58!2#dPODEX$TMJ~1)X@A|II+RVV05RiV8QF85!OAUQ|hG%0Du`p>X3s=Yk9?IOWTF6m)bhcQ!Xtez4BZ-8wx0 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 z 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%0D~?rAQXFd=|1B*lmHk2Sfl)m_X&l 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 LG?a4{O`iTABC0Z5 literal 0 HcmV?d00001 diff --git a/ui/embed/sharepoint-customizer/images/sidepane-welcome.png b/ui/embed/sharepoint-customizer/images/sidepane-welcome.png new file mode 100644 index 0000000000000000000000000000000000000000..364a644de55fefd3fa64a59a70b255fc6f9f9738 GIT binary patch literal 55363 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%0D;;R&= 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 Vd(_#9edSNehtv