diff --git a/.github/workflows/build-win-mac-universal.yml b/.github/workflows/build-win-mac-universal.yml new file mode 100644 index 00000000..dd40b47a --- /dev/null +++ b/.github/workflows/build-win-mac-universal.yml @@ -0,0 +1,142 @@ +name: Build and Package (p2 ZIP) + +on: + push: + branches: [ main ] + workflow_dispatch: + +env: + MAVEN_OPTS: -Xmx2g + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract plugin version + run: | + VERSION=$(grep -oP '(?<=)[^<]+' pom.xml) + TIMESTAMP=$(date -u +%Y%m%d%H%M%S) + DISPLAY_VERSION="${VERSION/-SNAPSHOT/-$TIMESTAMP}" + echo "COPILOT_PLUGIN_VERSION=$VERSION" >> $GITHUB_ENV + echo "COPILOT_DISPLAY_VERSION=$DISPLAY_VERSION" >> $GITHUB_ENV + + - name: Set up Java 21 (Temurin) + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + cache: 'maven' + + - name: Cache Maven local repo + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-m2- + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install Copilot agent + working-directory: com.microsoft.copilot.eclipse.core/copilot-agent + run: | + npm install --force + cp -r node_modules/@github/copilot-language-server/dist ./dist + + - name: Make mvnw executable + run: chmod +x ./mvnw + + - name: Build (Tycho/Maven) + run: | + ./mvnw -DskipTests -Dcheckstyle.skip=true clean verify + + - name: Prepare build artifacts + run: | + mkdir -p build + SRC=com.microsoft.copilot.eclipse.repository/target/com.microsoft.copilot.eclipse.repository-${COPILOT_PLUGIN_VERSION}.zip + if [ ! -f "$SRC" ]; then + echo "ERROR: Expected ZIP not found at $SRC" + ls -la com.microsoft.copilot.eclipse.repository/target || true + exit 1 + fi + if ! unzip -l "$SRC" | grep -qE 'content\.(jar|xml|xml\.xz)|p2\.index'; then + echo "ERROR: p2 repository ZIP is missing p2 metadata (content.jar / content.xml.xz / p2.index)" + unzip -l "$SRC" + exit 1 + fi + find build/ -maxdepth 1 -name "github-copilot-for-anypoint-*.zip" -delete 2>/dev/null || true + + # Create universal p2 repository (all platforms) + OUT_UNIVERSAL=build/github-copilot-for-anypoint-${COPILOT_DISPLAY_VERSION}.zip + cp -f "$SRC" "$OUT_UNIVERSAL" + sha256sum "$OUT_UNIVERSAL" | awk '{print $1 " " $2}' > "${OUT_UNIVERSAL}.sha256" + + # Create Windows-specific package (win32 only) + OUT_WIN=build/github-copilot-for-anypoint-${COPILOT_DISPLAY_VERSION}-win32.zip + unzip -q "$SRC" -d temp_win + rm -rf temp_win/plugins/*linux* temp_win/plugins/*macosx* temp_win/features/*linux* temp_win/features/*macosx* 2>/dev/null || true + cd temp_win && zip -q -r "../${OUT_WIN}" . && cd .. + rm -rf temp_win + sha256sum "$OUT_WIN" | awk '{print $1 " " $2}' > "${OUT_WIN}.sha256" + + # Create macOS-specific package (macosx only) + OUT_MAC=build/github-copilot-for-anypoint-${COPILOT_DISPLAY_VERSION}-macos.zip + unzip -q "$SRC" -d temp_mac + rm -rf temp_mac/plugins/*linux* temp_mac/plugins/*win32* temp_mac/features/*linux* temp_mac/features/*win32* 2>/dev/null || true + cd temp_mac && zip -q -r "../${OUT_MAC}" . && cd .. + rm -rf temp_mac + sha256sum "$OUT_MAC" | awk '{print $1 " " $2}' > "${OUT_MAC}.sha256" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }} + path: | + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}.zip + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}.zip.sha256 + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-win32.zip + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-win32.zip.sha256 + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-macos.zip + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-macos.zip.sha256 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: "v${{ env.COPILOT_DISPLAY_VERSION }}" + name: "GitHub Copilot for Anypoint Studio v${{ env.COPILOT_DISPLAY_VERSION }}" + prerelease: true + body: | + Pre-release build of GitHub Copilot for Anypoint Studio. + + **Installation in Anypoint Studio:** + + Select the package appropriate for your platform: + + - **Windows (64-bit)**: Download `github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-win32.zip` + - **macOS (Intel & Apple Silicon)**: Download `github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-macos.zip` + - **All Platforms**: Download `github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}.zip` (universal p2 repository with all platform support) + + **Installation Steps:** + 1. In Anypoint Studio, go to **Help → Install New Software...** + 2. Click **Add...** and select the downloaded ZIP file as a local update site. + 3. Follow the installation wizard and restart Anypoint Studio. + + **Verification:** + Each package includes a `.sha256` file for integrity verification. Use `sha256sum -c file.zip.sha256` to verify the download. + files: | + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}.zip + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}.zip.sha256 + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-win32.zip + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-win32.zip.sha256 + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-macos.zip + build/github-copilot-for-anypoint-${{ env.COPILOT_DISPLAY_VERSION }}-macos.zip.sha256 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a77cc802..f11fdcd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,43 +1,43 @@ -name: CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -permissions: - contents: read - -jobs: - build: - - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js environment - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install Copilot agent - working-directory: com.microsoft.copilot.eclipse.core/copilot-agent - run: npm i -f - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - - name: Build - uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 # v1.0.1 - with: - run: >- - ./mvnw clean verify --batch-mode +# name: CI + +# on: +# push: +# branches: [ "main" ] +# pull_request: +# branches: [ "main" ] + +# permissions: +# contents: read + +# jobs: +# build: + +# runs-on: ${{ matrix.os }} +# strategy: +# matrix: +# os: [ubuntu-latest, macos-latest, windows-latest] + +# steps: +# - uses: actions/checkout@v4 + +# - name: Setup Node.js environment +# uses: actions/setup-node@v4 +# with: +# node-version: 22 + +# - name: Install Copilot agent +# working-directory: com.microsoft.copilot.eclipse.core/copilot-agent +# run: npm i -f + +# - name: Set up JDK 17 +# uses: actions/setup-java@v4 +# with: +# java-version: '17' +# distribution: 'temurin' +# cache: maven + +# - name: Build +# uses: coactions/setup-xvfb@b6b4fcfb9f5a895edadc3bc76318fae0ac17c8b3 # v1.0.1 +# with: +# run: >- +# ./mvnw clean verify --batch-mode diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java index d8a5221a..8890a95b 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/BaseTurnWidget.java @@ -650,6 +650,16 @@ public CompletableFuture requestToolExecuti reset(); this.confirmDialog = new InvokeToolConfirmationDialog(this, content, input); + this.confirmDialog.addDisposeListener(e -> { + Composite ancestor = this.getParent(); + while (ancestor != null && !ancestor.isDisposed()) { + if (ancestor instanceof ChatContentViewer) { + ((ChatContentViewer) ancestor).requestRefreshScrollerLayout(); + break; + } + ancestor = ancestor.getParent(); + } + }); CompletableFuture toolConfirmationFuture = this.confirmDialog .getConfirmationFuture(); diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java index 58ee3f5d..7ef32077 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatContentViewer.java @@ -404,6 +404,14 @@ public void renderErrorMessage(String errorMessage) { scrollToLatestUserTurn(); } + /** + * Schedules a single async {@link #refreshScrollerLayout()} call so that multiple dispose/layout + * events that arrive in the same event-loop tick are coalesced into one pass. + */ + public void requestRefreshScrollerLayout() { + SwtUtils.invokeOnDisplayThreadAsync(() -> refreshScrollerLayout(), this); + } + /** * Update the size of scrolled composite when there are content updates. */ diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/InvokeToolConfirmationDialog.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/InvokeToolConfirmationDialog.java index ebc307a0..1f65aead 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/InvokeToolConfirmationDialog.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/InvokeToolConfirmationDialog.java @@ -119,10 +119,7 @@ public void cancelConfirmation() { && StringUtils.isNotEmpty(this.cancelMessage)) { new AgentToolCancelLabel(parent, SWT.NONE, this.cancelMessage); } - this.dispose(); - if (parent != null && !parent.isDisposed()) { - parent.requestLayout(); - } + disposeAndRequestParentLayout(); }, this); } } @@ -318,6 +315,10 @@ private void acceptAndDispose(ConfirmationAction action) { new LanguageModelToolConfirmationResult( ToolConfirmationResult.ACCEPT)); + disposeAndRequestParentLayout(); + } + + private void disposeAndRequestParentLayout() { Composite parent = this.getParent(); this.dispose(); if (parent != null && !parent.isDisposed()) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/WarnWidget.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/WarnWidget.java index 2582320c..b6029b3c 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/WarnWidget.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/WarnWidget.java @@ -4,17 +4,21 @@ package com.microsoft.copilot.eclipse.ui.chat; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.eclipse.swt.SWT; -import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.PlatformUI; @@ -30,7 +34,12 @@ * and whether to pass a plan. */ public class WarnWidget extends Composite { + private static final Pattern MARKDOWN_LINK_PATTERN = Pattern.compile("\\[([^\\]]+)]\\(([^)]+)\\)"); + private static final Pattern RAW_URL_PATTERN = Pattern.compile("https?://\\S+"); + private int buttonLeftMargin; + private Color darkBackground; + private Color darkForeground; /** * Create the composite. @@ -119,15 +128,71 @@ private void buildWarnLabelWithIcon(String message) { buttonLeftMargin = warnLayout.marginWidth + warnLayout.marginLeft + warnImage.getBounds().width + warnLayout.horizontalSpacing; - ChatMarkupViewer textLabel = new ChatMarkupViewer(composite, SWT.LEFT | SWT.WRAP); - StyledText styledText = textLabel.getTextWidget(); - styledText.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, true)); - styledText.setEditable(false); - textLabel.setMarkup(message); + Link messageLink = new Link(composite, SWT.LEFT | SWT.WRAP); + messageLink.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + messageLink.setText(toLinkMarkup(message)); + messageLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(org.eclipse.swt.events.SelectionEvent event) { + UiUtils.openLink(event.text); + } + }); requestLayout(); } + private static String toLinkMarkup(String message) { + String text = stripMarkdownEmphasis(message == null ? "" : message); + Matcher markdownLinkMatcher = MARKDOWN_LINK_PATTERN.matcher(text); + StringBuilder result = new StringBuilder(); + int offset = 0; + while (markdownLinkMatcher.find()) { + appendTextWithRawLinks(result, text.substring(offset, markdownLinkMatcher.start())); + appendLink(result, markdownLinkMatcher.group(1), markdownLinkMatcher.group(2)); + offset = markdownLinkMatcher.end(); + } + appendTextWithRawLinks(result, text.substring(offset)); + return result.toString(); + } + + private static void appendTextWithRawLinks(StringBuilder result, String text) { + Matcher rawUrlMatcher = RAW_URL_PATTERN.matcher(text); + int offset = 0; + while (rawUrlMatcher.find()) { + result.append(escapeLinkText(text.substring(offset, rawUrlMatcher.start()))); + String url = rawUrlMatcher.group(); + String trailingPunctuation = ""; + while (!url.isEmpty() && ".,;:".indexOf(url.charAt(url.length() - 1)) >= 0) { + trailingPunctuation = url.charAt(url.length() - 1) + trailingPunctuation; + url = url.substring(0, url.length() - 1); + } + appendLink(result, url, url); + result.append(escapeLinkText(trailingPunctuation)); + offset = rawUrlMatcher.end(); + } + result.append(escapeLinkText(text.substring(offset))); + } + + private static void appendLink(StringBuilder result, String label, String url) { + result.append("") + .append(escapeLinkText(stripMarkdownEmphasis(label == null ? "" : label))) + .append(""); + } + + private static String stripMarkdownEmphasis(String text) { + return text.replace("**", ""); + } + + private static String escapeLinkText(String text) { + return text.replace("&", "&").replace("<", "<").replace(">", ">"); + } + + private static String escapeLinkAttribute(String text) { + return escapeLinkText(text).replace("\"", """).replace("'", "'"); + } + /** * Render plan-driven action buttons for a quota-exceeded warning, kept in sync with the quota {@link StaticBanner}. */