Skip to content
1 change: 1 addition & 0 deletions src/common/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const UTIL_CHANNELS = {
CHECK_INTERNET_CONNECTIVITY: "check-internet-connectivity",
FETCH_FAVICON: "fetch-favicon",
OPEN_IN_CONNECTION_BROWSER: "open-in-connection-browser",
RESTART_APP: "restart-app",
} as const;

// Filesystem-related IPC channels
Expand Down
1 change: 1 addition & 0 deletions src/common/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface UtilsAPI {
showModalWindow: (options: ModalWindowOptions) => Promise<void>;
closeModalWindow: () => Promise<void>;
sendModalMessage: (payload: ModalWindowMessagePayload) => Promise<void>;
restartApp: () => Promise<void>;
}

/**
Expand Down
7 changes: 7 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ class ToolBoxApp {
ipcMain.removeHandler(UTIL_CHANNELS.FETCH_FAVICON);
ipcMain.removeHandler(UTIL_CHANNELS.OPEN_EXTERNAL);
ipcMain.removeHandler(UTIL_CHANNELS.OPEN_IN_CONNECTION_BROWSER);
ipcMain.removeHandler(UTIL_CHANNELS.RESTART_APP);

// Filesystem handlers
ipcMain.removeHandler(FILESYSTEM_CHANNELS.READ_TEXT);
Expand Down Expand Up @@ -1210,6 +1211,12 @@ class ToolBoxApp {
this.modalWindowManager?.sendMessageToModal(payload);
});

ipcMain.handle(UTIL_CHANNELS.RESTART_APP, () => {
this.isQuitting = true;
app.relaunch();
app.quit();
});

// Clipboard handler
ipcMain.handle(UTIL_CHANNELS.COPY_TO_CLIPBOARD, (_, text) => {
this.api.copyToClipboard(text);
Expand Down
3 changes: 3 additions & 0 deletions src/main/managers/toolRegistryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,9 @@ export class ToolRegistryManager extends EventEmitter {
minAPI: manifestEntry.minAPI,
maxAPI: manifestEntry.maxAPI,
mcpHeadlessEnabled: manifestEntry.mcpHeadlessEnabled,
marketplaceSourceId: manifestEntry.marketplaceSourceId,
marketplaceSourceLabel: manifestEntry.marketplaceSourceLabel,
marketplaceSourceType: manifestEntry.marketplaceSourceType,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ contextBridge.exposeInMainWorld("toolboxAPI", {
showModalWindow: (options: unknown) => ipcRenderer.invoke(UTIL_CHANNELS.SHOW_MODAL_WINDOW, options),
closeModalWindow: () => ipcRenderer.invoke(UTIL_CHANNELS.CLOSE_MODAL_WINDOW),
sendModalMessage: (payload: unknown) => ipcRenderer.invoke(UTIL_CHANNELS.SEND_MODAL_MESSAGE, payload),
restartApp: () => ipcRenderer.invoke(UTIL_CHANNELS.RESTART_APP),
},

// Troubleshooting namespace - organized like other features
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ <h2 class="sidebar-title">INSTALLED</h2>
<input type="checkbox" id="tools-update-required-filter" class="filter-checkbox-input" />
<span>Update Available</span>
</label>
<label class="filter-checkbox-label" for="tools-private-marketplace-filter" id="tools-private-marketplace-filter-row" style="display: none">
<input type="checkbox" id="tools-private-marketplace-filter" class="filter-checkbox-input" />
<span>Private Marketplace</span>
</label>
<label class="filter-checkbox-label" for="tools-mcp-enabled-filter" id="tools-mcp-enabled-filter-row">
<input type="checkbox" id="tools-mcp-enabled-filter" class="filter-checkbox-input" />
<span>MCP Enabled</span>
Expand Down Expand Up @@ -263,6 +267,10 @@ <h2 class="sidebar-title">MARKETPLACE</h2>
<input type="checkbox" id="marketplace-new-filter" class="filter-checkbox-input" />
<span>New (last 7 days)</span>
</label>
<label class="filter-checkbox-label" id="marketplace-private-marketplace-filter-row" style="display: none">
<input type="checkbox" id="marketplace-private-marketplace-filter" class="filter-checkbox-input" />
<span>Private Marketplace</span>
</label>
<label class="filter-checkbox-label" id="marketplace-mcp-enabled-filter-row">
<input type="checkbox" id="marketplace-mcp-enabled-filter" class="filter-checkbox-input" />
<span>MCP Enabled</span>
Expand Down
39 changes: 37 additions & 2 deletions src/renderer/modules/marketplaceManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export async function loadToolsLibrary(): Promise<void> {
isSupported: tool.isSupported, // Include compatibility status
npmPackageName: tool.npmPackageName, // Include npm package name for pre-release detection
mcpHeadlessEnabled: tool.mcpHeadlessEnabled,
marketplaceSourceId: tool.marketplaceSourceId,
marketplaceSourceLabel: tool.marketplaceSourceLabel,
marketplaceSourceType: tool.marketplaceSourceType,
}) as ToolDetail,
);

Expand Down Expand Up @@ -106,25 +109,36 @@ export async function loadMarketplace(): Promise<void> {
const newFilter = document.getElementById("marketplace-new-filter") as HTMLInputElement | null;
const mcpEnabledFilter = document.getElementById("marketplace-mcp-enabled-filter") as HTMLInputElement | null;
const mcpEnabledFilterRow = document.getElementById("marketplace-mcp-enabled-filter-row") as HTMLElement | null;
const privateMarketplaceFilter = document.getElementById("marketplace-private-marketplace-filter") as HTMLInputElement | null;
const privateMarketplaceFilterRow = document.getElementById("marketplace-private-marketplace-filter-row") as HTMLElement | null;
const sortSelect = document.getElementById("marketplace-sort-select") as HTMLSelectElement | null;
const mcpPreviewUiEnabled = isMcpPreviewUiEnabled();
const userSettings = await window.toolboxAPI.getUserSettings();
const hasConfiguredPrivateMarketplace = (userSettings.marketplaceSources || []).some((source) => source.type === "private");

if (mcpEnabledFilterRow) {
mcpEnabledFilterRow.style.display = mcpPreviewUiEnabled ? "" : "none";
}
if (!mcpPreviewUiEnabled && mcpEnabledFilter) {
mcpEnabledFilter.checked = false;
}
if (privateMarketplaceFilterRow) {
privateMarketplaceFilterRow.style.display = hasConfiguredPrivateMarketplace ? "" : "none";
}
if (!hasConfiguredPrivateMarketplace && privateMarketplaceFilter) {
privateMarketplaceFilter.checked = false;
}

const searchTerm = searchInput?.value ? searchInput.value.toLowerCase() : "";
const selectedCategory = categoryFilter?.value || "";
const selectedAuthor = authorFilter?.value || "";
const showNewOnly = newFilter?.checked || false;
const showMcpEnabledOnly = mcpPreviewUiEnabled && (mcpEnabledFilter?.checked || false);
const showPrivateMarketplaceOnly = hasConfiguredPrivateMarketplace && (privateMarketplaceFilter?.checked || false);
const deprecatedToolsVisibility = (await window.toolboxAPI.getSetting("deprecatedToolsVisibility")) || "hide-all";

// Update filter button indicator and one-click clear button visibility
const hasDropdownFilters = !!(selectedCategory || selectedAuthor || showNewOnly || showMcpEnabledOnly);
const hasDropdownFilters = !!(selectedCategory || selectedAuthor || showNewOnly || showMcpEnabledOnly || showPrivateMarketplaceOnly);
const marketplaceFilterBtn = document.getElementById("marketplace-filter-btn");
if (marketplaceFilterBtn) {
marketplaceFilterBtn.classList.toggle("has-active-filters", hasDropdownFilters);
Expand Down Expand Up @@ -180,6 +194,10 @@ export async function loadMarketplace(): Promise<void> {
return false;
}

if (showPrivateMarketplaceOnly && t.marketplaceSourceType !== "private") {
return false;
}

// Deprecated filter
if (t.status === "deprecated") {
if (deprecatedToolsVisibility === "hide-all" || deprecatedToolsVisibility === "show-installed") {
Expand Down Expand Up @@ -214,7 +232,7 @@ export async function loadMarketplace(): Promise<void> {
// Show empty state if no tools match the search
if (filteredTools.length === 0) {
const hasSearchTerm = searchTerm.length > 0;
const hasActiveFilters = hasSearchTerm || selectedCategory || selectedAuthor || showNewOnly || showMcpEnabledOnly;
const hasActiveFilters = hasSearchTerm || selectedCategory || selectedAuthor || showNewOnly || showMcpEnabledOnly || showPrivateMarketplaceOnly;
const emptyMessage = hasSearchTerm ? "Try a different search term." : hasActiveFilters ? "No tools match the current filters." : "Check back later for new tools.";
marketplaceList.innerHTML = `
<div class="empty-state">
Expand Down Expand Up @@ -437,6 +455,13 @@ export async function loadMarketplace(): Promise<void> {
});
}

if (privateMarketplaceFilter && !(privateMarketplaceFilter as any)._pptbBound) {
(privateMarketplaceFilter as any)._pptbBound = true;
privateMarketplaceFilter.addEventListener("change", () => {
loadMarketplace();
});
}

// Setup sort event listener
if (sortSelect && !(sortSelect as any)._pptbBound) {
(sortSelect as any)._pptbBound = true;
Expand Down Expand Up @@ -764,6 +789,11 @@ function clearMarketplaceFilters(): void {
mcpEnabledFilter.checked = false;
}

const privateMarketplaceFilter = document.getElementById("marketplace-private-marketplace-filter") as HTMLInputElement | null;
if (privateMarketplaceFilter) {
privateMarketplaceFilter.checked = false;
}

// Reload the marketplace to reflect the cleared filters
loadMarketplace();
}
Expand Down Expand Up @@ -797,6 +827,11 @@ export function clearMarketplaceDropdownFilters(): void {
mcpEnabledFilter.checked = false;
}

const privateMarketplaceFilter = document.getElementById("marketplace-private-marketplace-filter") as HTMLInputElement | null;
if (privateMarketplaceFilter) {
privateMarketplaceFilter.checked = false;
}

// Reload the marketplace to reflect the cleared filters
loadMarketplace();
}
Expand Down
169 changes: 159 additions & 10 deletions src/renderer/modules/settingsManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
MAX_COLOR_BORDER_THICKNESS,
MIN_COLOR_BORDER_THICKNESS,
} from "../constants";
import { getModalStyles } from "../modals/sharedStyles";
import type { SettingsState } from "../types/index";
import { offBrowserWindowModalClosed, offBrowserWindowModalMessage, onBrowserWindowModalClosed, onBrowserWindowModalMessage, showBrowserWindowModal } from "./browserWindowModals";
import { loadMarketplace } from "./marketplaceManagement";
import { setDefaultNotificationDuration } from "./notifications";
import {
Expand Down Expand Up @@ -144,6 +146,140 @@ function collectMarketplaceSourcesFromSettingsPanel(): MarketplaceSource[] {
return sources;
}

function shouldPromptRestartForMarketplaceSourceChanges(previousSources: MarketplaceSource[], nextSources: MarketplaceSource[]): boolean {
const previousBuiltInEnabled = previousSources.find((source) => source.id === "builtin-pptb")?.enabled ?? true;
const nextBuiltInEnabled = nextSources.find((source) => source.id === "builtin-pptb")?.enabled ?? true;

const builtInChanged = previousBuiltInEnabled !== nextBuiltInEnabled;

const normalizePrivateSources = (sources: MarketplaceSource[]) =>
sources
.filter((source) => source.type === "private")
.map((source) => ({
id: source.id,
label: source.label,
url: source.url,
enabled: source.enabled,
}))
.sort((left, right) => left.id.localeCompare(right.id));

const privateSourcesChanged = JSON.stringify(normalizePrivateSources(previousSources)) !== JSON.stringify(normalizePrivateSources(nextSources));

return builtInChanged || privateSourcesChanged;
}

async function promptRestartForMarketplaceSourceChanges(): Promise<boolean> {
const modalId = "marketplace-source-restart-required";
const restartChannel = "settings:marketplace-restart-now";
const laterChannel = "settings:marketplace-restart-later";
const isDarkTheme = document.body.classList.contains("dark-theme");
const styles = `${getModalStyles(isDarkTheme)}
<style>
.restart-required-hero {
border: 1px solid ${isDarkTheme ? "rgba(0, 180, 216, 0.35)" : "rgba(14, 99, 156, 0.25)"};
background: ${isDarkTheme ? "linear-gradient(135deg, rgba(0, 180, 216, 0.15) 0%, rgba(106, 0, 255, 0.18) 100%)" : "linear-gradient(135deg, rgba(14, 99, 156, 0.08) 0%, rgba(106, 0, 255, 0.1) 100%)"};
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 12px;
}

.restart-required-hero-title {
margin: 0 0 4px;
font-size: 14px;
font-weight: 600;
}

.restart-required-hero-text {
margin: 0;
font-size: 13px;
line-height: 1.4;
color: ${isDarkTheme ? "rgba(255,255,255,0.88)" : "rgba(31,31,31,0.88)"};
}

.restart-required-note {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: ${isDarkTheme ? "rgba(255,255,255,0.75)" : "rgba(31,31,31,0.72)"};
}
</style>`;
const body = `
<div class="modal-panel">
<div class="modal-header">
<div>
<p class="modal-eyebrow">Marketplace</p>
<h3>Restart Required</h3>
</div>
<button class="icon-button" id="marketplace-restart-close-btn" aria-label="Close">×</button>
</div>
<div class="modal-body">
<div class="restart-required-hero">
<p class="restart-required-hero-title">Marketplace source changes were saved</p>
<p class="restart-required-hero-text">Restart Power Platform ToolBox to apply marketplace source updates.</p>
</div>
<p class="restart-required-note">Marketplace source was changed. These changes take effect after restarting the app.</p>
</div>
<div class="modal-footer">
<button type="button" id="marketplace-restart-later-btn" class="fluent-button fluent-button-secondary">Later</button>
<button type="button" id="marketplace-restart-now-btn" class="fluent-button fluent-button-primary">Restart Now</button>
</div>
</div>`;
const script = `
<script>
(() => {
const bridge = window.modalBridge;
if (!bridge) return;

const restartNow = () => bridge.send(${JSON.stringify(restartChannel)});
const restartLater = () => bridge.send(${JSON.stringify(laterChannel)});

document.getElementById("marketplace-restart-now-btn")?.addEventListener("click", restartNow);
document.getElementById("marketplace-restart-later-btn")?.addEventListener("click", restartLater);
document.getElementById("marketplace-restart-close-btn")?.addEventListener("click", restartLater);
})();
</script>`;

const html = `${styles}\n${body}\n${script}`;

return await new Promise<boolean>((resolve) => {
let shouldRestart = false;

const onMessage = (payload: { channel: string }) => {
if (payload?.channel === restartChannel) {
shouldRestart = true;
void window.toolboxAPI.utils.closeModalWindow();
return;
}

if (payload?.channel === laterChannel) {
shouldRestart = false;
void window.toolboxAPI.utils.closeModalWindow();
}
};

const onClosed = () => {
offBrowserWindowModalMessage(onMessage);
offBrowserWindowModalClosed(onClosed);
resolve(shouldRestart);
};

onBrowserWindowModalMessage(onMessage);
onBrowserWindowModalClosed(onClosed);

showBrowserWindowModal({
id: modalId,
html,
width: 520,
height: 320,
}).catch((error) => {
offBrowserWindowModalMessage(onMessage);
offBrowserWindowModalClosed(onClosed);
logError(error instanceof Error ? error : new Error(String(error)));
resolve(false);
});
});
}

/**
* Load settings into the settings UI panel
*/
Expand Down Expand Up @@ -312,6 +448,8 @@ export async function saveSettings(): Promise<void> {
marketplaceSources,
};

const requiresRestartForMarketplaceSources = shouldPromptRestartForMarketplaceSourceChanges(originalSettings.marketplaceSources ?? [], currentSettings.marketplaceSources);

// Only include changed settings in the update
const changedSettings: any = {};

Expand Down Expand Up @@ -393,6 +531,13 @@ export async function saveSettings(): Promise<void> {
body: "Your settings have been saved.",
type: "success",
});

if (changedSettings.marketplaceSources !== undefined && requiresRestartForMarketplaceSources) {
const shouldRestart = await promptRestartForMarketplaceSourceChanges();
if (shouldRestart) {
await window.toolboxAPI.utils.restartApp();
}
}
}
// If no changes, do nothing (no notification shown)
}
Expand Down Expand Up @@ -762,18 +907,22 @@ export function renderSettingsContent(panel: HTMLElement): void {
</div>
`,
);
const addedRow = marketplaceSourcesList.lastElementChild as HTMLElement | null;
addedRow?.querySelector("[data-action='remove-marketplace-source']")?.addEventListener("click", () => {
addedRow.remove();
});
});
}

marketplaceSourcesList?.querySelectorAll<HTMLElement>("[data-action='remove-marketplace-source']").forEach((button) => {
button.addEventListener("click", () => {
button.closest(".settings-vscode-marketplace-source-row")?.remove();
});
});
const list = marketplaceSourcesList as HTMLElement & { _pptbBound?: boolean };
if (!list._pptbBound) {
list._pptbBound = true;
list.addEventListener("click", (event) => {
const target = event.target as HTMLElement | null;
const removeButton = target?.closest("[data-action='remove-marketplace-source']") as HTMLButtonElement | null;
if (!removeButton) {
return;
}

removeButton.closest(".settings-vscode-marketplace-source-row")?.remove();
});
}
}

// Wire up font help link
const fontHelpLink = panel.querySelector("#font-help-link") as HTMLAnchorElement | null;
Expand Down
Loading
Loading