Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/skills/ui-action/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,29 @@ The module-only shortcut can fail dependency resolution or reuse stale bundles,
especially after source edits. If widget markers or other code changes appear to
be ignored, run the root `clean verify` command.

### Selecting the chat renderer

The plugin ships two chat renderers behind the `useBrowserBasedChatRenderer`
preference, and probes can target either one via the `-Dprobe.renderer` launch
property (read once by `ProbeRunner` before the ChatView opens):

- omit it, or pass `-Dprobe.renderer=styledtext`, to run against the seasoned
StyledText renderer (the production default). These probes assert the SWT
widget tree (`user-turn`, `copilot-turn`, `model-info-label`).
- pass `-Dprobe.renderer=browser` to run against the browser-based (HTML)
renderer. Its conversation content lives in an SWT `Browser` DOM that is opaque
to SWTBot widget locators, so those probes assert the `Browser` widget exists
and fall back to `sleep` + `screenshot` for visual verification.

Because the renderer is chosen per launch, cover both by running the two probes:

```bash
# StyledText renderer (default)
./mvnw clean verify -Dprobe.script=probe-scripts/chat-send-receive-001.json
# Browser renderer
./mvnw clean verify -Dprobe.script=probe-scripts/chat-send-receive-browser-001.json -Dprobe.renderer=browser
```

Platform notes:

- Linux headless: prefix the command with `xvfb-run -a`.
Expand Down
13 changes: 13 additions & 0 deletions base.target
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@
<unit id="org.eclipse.egit.feature.group" version="0.0.0"/>
<unit id="org.eclipse.jgit" version="0.0.0"/>
</location>
<location type="InstallableUnit" includeConfigurePhase="true" includeMode="planner" includeSource="true">
<repository location="https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/2025-03/"/>
<unit id="org.commonmark" version="0.0.0"/>
<unit id="org.commonmark-gfm-tables" version="0.0.0"/>
<unit id="org.commonmark-task-list-items" version="0.0.0"/>
<unit id="org.commonmark-gfm-strikethrough" version="0.0.0"/>
<unit id="org.commonmark.source" version="0.0.0"/>
<unit id="org.commonmark-gfm-tables.source" version="0.0.0"/>
<unit id="org.commonmark-task-list-items.source" version="0.0.0"/>
<unit id="org.commonmark-gfm-strikethrough.source" version="0.0.0"/>
<unit id="com.google.guava" version="0.0.0"/>
<unit id="com.google.guava.failureaccess" version="0.0.0"/>
</location>
<location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="planner" includeSource="true" type="InstallableUnit">
<repository location="https://download.eclipse.org/technology/swtbot/releases/latest/"/>
<unit id="org.eclipse.swtbot.eclipse.feature.group" version="0.0.0"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.utils;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.osgi.framework.Bundle;

class BundleUtilsTests {

private static final String PATH = "resources/sample.dat";

private static URL writeTempFile(Path dir, String name, byte[] content) throws Exception {
Path file = dir.resolve(name);
Files.write(file, content);
return file.toUri().toURL();
}

private static Bundle mockBundle(String path, URL entry) {
Bundle bundle = mock(Bundle.class);
when(bundle.getEntry(path)).thenReturn(entry);
return bundle;
}

@Test
void readResourceAsString_returnsUtf8Content(@TempDir Path dir) throws Exception {
String content = "Hello, \u00e4\u00f6\u00fc \u2013 SVG <svg/>";
URL url = writeTempFile(dir, "sample.dat", content.getBytes(StandardCharsets.UTF_8));
Bundle bundle = mockBundle(PATH, url);

assertEquals(content, BundleUtils.readResourceAsString(bundle, PATH));
}

@Test
void readResourceAsBytes_returnsRawBytes(@TempDir Path dir) throws Exception {
byte[] content = {0, 1, 2, 3, (byte) 0xFF, 10, 20};
URL url = writeTempFile(dir, "sample.dat", content);
Bundle bundle = mockBundle(PATH, url);

assertArrayEquals(content, BundleUtils.readResourceAsBytes(bundle, PATH));
}

@Test
void readResourceAsDataUri_encodesBase64WithMimeType(@TempDir Path dir) throws Exception {
byte[] content = {10, 20, 30, 40};
URL url = writeTempFile(dir, "sample.dat", content);
Bundle bundle = mockBundle(PATH, url);

String dataUri = BundleUtils.readResourceAsDataUri(bundle, PATH, "image/png");

String prefix = "data:image/png;base64,";
assertTrue(dataUri.startsWith(prefix), "unexpected data URI: " + dataUri);
byte[] decoded = Base64.getDecoder().decode(dataUri.substring(prefix.length()));
assertArrayEquals(content, decoded);
}

@Test
void readResourceAsString_missingEntry_returnsNull() {
Bundle bundle = mockBundle(PATH, null);
assertNull(BundleUtils.readResourceAsString(bundle, PATH));
}

@Test
void readResourceAsBytes_missingEntry_returnsNull() {
Bundle bundle = mockBundle(PATH, null);
assertNull(BundleUtils.readResourceAsBytes(bundle, PATH));
}

@Test
void readResourceAsDataUri_missingEntry_returnsEmptyString() {
Bundle bundle = mockBundle(PATH, null);
assertEquals("", BundleUtils.readResourceAsDataUri(bundle, PATH, "image/png"));
}

@Test
void readResourceAsBytes_nullBundle_returnsNull() {
assertNull(BundleUtils.readResourceAsBytes(null, PATH));
}

@Test
void readResourceAsBytes_nullPath_returnsNull() {
assertNull(BundleUtils.readResourceAsBytes(mock(Bundle.class), null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private Constants() {
public static final String AUTO_BREAKPOINT_RESPONSE = "autoBreakpointResponse";
public static final String GITHUB_JOBS_VIEW_ID = "com.microsoft.copilot.eclipse.ui.jobs.JobsView";
public static final String SUPPRESS_TERMINAL_DEPENDENCY_DIALOG = "suppressTerminalDependencyDialog";
public static final String USE_BROWSER_BASED_CHAT_RENDERER = "useBrowserBasedChatRenderer";

// Auto-Approve settings
public static final String AUTO_APPROVE_TERMINAL_RULES = "autoApproveTerminalRules";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

package com.microsoft.copilot.eclipse.core.utils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.osgi.framework.Bundle;

import com.microsoft.copilot.eclipse.core.CopilotCore;

/**
* Utility methods for loading resource files (text, binary, images such as PNG or SVG)
* bundled inside an OSGi {@link Bundle}.
*
* <p>All methods degrade gracefully: if the resource cannot be located or read they log the
* failure and return {@code null} (or an empty string for the data-URI helper) instead of
* throwing, so that a missing icon never breaks the surrounding feature.
*/
public final class BundleUtils {

private BundleUtils() {
}

/**
* Reads a bundle resource as a UTF-8 string. Intended for text resources such as SVG icons.
*
* @param bundle the bundle containing the resource
* @param path the bundle-relative resource path (e.g. {@code "resources/html/icons/x.svg"})
* @return the file content as a UTF-8 string, or {@code null} if it cannot be read
*/
public static String readResourceAsString(Bundle bundle, String path) {
byte[] bytes = readResourceAsBytes(bundle, path);
return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8);
}

/**
* Reads a bundle resource as a byte array. Intended for binary resources such as PNG icons.
*
* @param bundle the bundle containing the resource
* @param path the bundle-relative resource path
* @return the file content as bytes, or {@code null} if it cannot be read
*/
public static byte[] readResourceAsBytes(Bundle bundle, String path) {
if (bundle == null || path == null) {
return null;
}
URL entry = bundle.getEntry(path);
if (entry == null) {
CopilotCore.LOGGER.error("Bundle resource not found: " + path,
new FileNotFoundException(path));
return null;
}
try (InputStream is = entry.openStream()) {
return is.readAllBytes();
} catch (IOException e) {
CopilotCore.LOGGER.error("Failed to read bundle resource: " + path, e);
return null;
}
}

/**
* Reads a bundle resource and encodes it as a {@code data:} URI with the given MIME type,
* for embedding a binary resource (e.g. a PNG icon) directly into HTML.
*
* @param bundle the bundle containing the resource
* @param path the bundle-relative resource path
* @param mimeType the MIME type to use in the data URI (e.g. {@code "image/png"})
* @return the {@code data:} URI string, or an empty string if the resource cannot be read
*/
public static String readResourceAsDataUri(Bundle bundle, String path, String mimeType) {
byte[] bytes = readResourceAsBytes(bundle, path);
if (bytes == null) {
return "";
}
return "data:" + mimeType + ";base64," + Base64.getEncoder().encodeToString(bytes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[
{ "action": "waitForIdle" },
{ "action": "screenshot", "id": "01-startup" },

{ "action": "showView", "idRef": "com.microsoft.copilot.eclipse.ui.chat.ChatView" },
{ "action": "waitForIdle" },
{ "action": "screenshot", "id": "02-chat-open" },

{ "action": "assertExists",
"locator": { "by": "viewId", "id": "com.microsoft.copilot.eclipse.ui.chat.ChatView" } },

{ "action": "waitFor",
"locator": { "by": "widgetClass", "value": "Browser" },
"timeoutSec": 30 },

{ "action": "waitFor",
"locator": { "by": "styledText" },
"timeoutSec": 30 },

{ "action": "click", "locator": { "by": "styledText" } },
{ "action": "clearElement", "locator": { "by": "styledText" } },
{ "action": "typeIn", "locator": { "by": "styledText" }, "text": "hello" },
{ "action": "screenshot", "id": "03-typed" },

{ "action": "dumpUi", "id": "03b-pre-model-poll" },
{ "action": "screenshot", "id": "03c-pre-model-poll" },

{ "action": "waitForMethod",
"locator": { "by": "widgetId", "value": "model-picker" },
"method": "getSelectedItemId",
"timeoutSec": 60 },

{ "action": "waitFor",
"locator": { "by": "buttonWithTooltip", "tooltip": "Send" },
"timeoutSec": 30 },
{ "action": "click", "locator": { "by": "buttonWithTooltip", "tooltip": "Send" } },

{ "action": "sleep", "timeoutSec": 5 },
{ "action": "screenshot", "id": "04-user-message-sent" },

{ "action": "sleep", "timeoutSec": 90 },
{ "action": "screenshot", "id": "05-agent-response" },
{ "action": "dumpUi", "id": "06-final" }
]
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
@RunWith(SWTBotJunit4ClassRunner.class)
public class ProbeRunner {
private static final String SYSPROP = "probe.script";
private static final String RENDERER_SYSPROP = "probe.renderer";
private static final Path RESULTS_DIR = Paths.get("target", "probe-results");
private static final String UI_BUNDLE_ID = "com.microsoft.copilot.eclipse.ui";

Expand Down Expand Up @@ -77,6 +78,13 @@ private static void suppressNuisancePreferences() {
prefs.putBoolean(Constants.AUTO_SHOW_WHAT_IS_NEW, false);
prefs.put(Constants.LAST_USED_COPILOT_PLUGIN_VERSION, currentUiBundleMajorMinor());
prefs.putBoolean(Constants.SUPPRESS_TERMINAL_DEPENDENCY_DIALOG, true);
// Select the chat renderer per launch so the same probe flow can be run against both
// renderers: pass -Dprobe.renderer=browser for the browser-based (HTML) renderer, or
// omit it (or -Dprobe.renderer=styledtext) for the seasoned StyledText renderer. The
// default matches production (StyledText), so plain runs exercise the SWT widget tree.
boolean useBrowserRenderer =
"browser".equalsIgnoreCase(System.getProperty(RENDERER_SYSPROP, "styledtext"));
prefs.putBoolean(Constants.USE_BROWSER_BASED_CHAT_RENDERER, useBrowserRenderer);
prefs.flush();
} catch (BackingStoreException | RuntimeException e) {
System.err.println("[ProbeRunner] Failed to preset nuisance-dialog preferences: " + e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
source.. = src/
output.. = bin/
bin.includes = META-INF/,\
.,\
scripts/
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=17
1 change: 0 additions & 1 deletion com.microsoft.copilot.eclipse.ui.jobs/build.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
source.. = src/
output.. = bin/
bin.includes = META-INF/,\
.,\
plugin.xml,\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ Require-Bundle: org.mockito.mockito-core;bundle-version="5.14.2",
org.eclipse.e4.core.services;bundle-version="2.4.200",
org.osgi.service.event,
com.google.gson,
com.microsoft.copilot.eclipse.terminal.api;bundle-version="0.20.0"
com.microsoft.copilot.eclipse.terminal.api;bundle-version="0.20.0",
org.eclipse.e4.ui.css.swt.theme;bundle-version="0.14.500"
25 changes: 25 additions & 0 deletions com.microsoft.copilot.eclipse.ui.test/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,29 @@
name="Editor for Refactor Rename Tests"/>
</extension>

<extension
point="org.eclipse.ui.commands">
<command
id="copilot.test.compareRenderers"
name="Compare Chat View Rendering Alternatives"/>
</extension>

<extension
point="org.eclipse.ui.handlers">
<handler
class="com.microsoft.copilot.eclipse.ui.chat.ConversationRendererVisualDemoSideBySide"
commandId="copilot.test.compareRenderers"/>
</extension>

<extension
point="org.eclipse.ui.menus">
<menuContribution
locationURI="menu:com.microsoft.copilot.eclipse.ui.menuBar?after=additions">
<command
commandId="copilot.test.compareRenderers"
label="Compare Chat View Rendering Alternatives"
style="push"/>
</menuContribution>
</extension>

</plugin>
Loading