Skip to content
Merged
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
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ and this project adheres to

### Fixed

- Support Home and End keys in the Access tokens base URL segmented control,
skipping disabled options (#129).

- Stop dashboard Public URL polling when leaving Settings and offer Refresh when
the background check outlasts polling (#129).

- Cancel Public URL probes during plugin cleanup and ignore late settings writes
after disposal, including when the plugin is re-registered (#129).

- Return immediately after saving a Public URL while serialized background
checks keep only the latest matching result. Refresh dashboard check results
and OAuth warnings when checks finish, and match persistent warning text in
the editor sidebar (#129).

- Skip the Public URL probe when clearing the URL and clear persisted check
metadata so stale failures are removed (#129).
- Persist Public URL probe results only for the current validated settings
origin, preserving saved failures and OAuth warnings when other URLs are
checked (#129).

- Use thin theme-accent focus rings only for keyboard focus on dashboard
navigation, details summaries, and buttons.
- Recover dashboard editor jumps when the last-focused editor tab has closed or
stopped responding. Track editor presence, clear owned focus and presence on
pagehide, and avoid reloading a live editor when acknowledgement times out.
Expand All @@ -27,6 +49,22 @@ and this project adheres to
prereleases in `release.yml` (#128).
- Include a generic install URL and install command in prerelease notes (#128).

### Added

- Require a configured Public URL for the MCP OAuth server URL, add a shortcut
to its Settings field, and show OAuth discovery warnings for missing URLs or
matching persisted failed checks in the dashboard and editor sidebar.
- Automatically check changed Public URLs using the existing guarded probe and
persist check metadata separately from editable settings.
- Add an Access tokens header segmented control for link and setup base URLs,
defaulting to this page's URL. Store the preference only in browser
localStorage and disable Public URL selection until configured.

### Changed

- Make the dashboard and editor sidebar MCP OAuth controls accessible toggle
switches.

## [0.2.0-beta.13] - 2026-11-10

- Label the "Opened from dashboard" card action Go to flow for flow, tab and
Expand Down
35 changes: 34 additions & 1 deletion docs/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,12 @@ paths:
post:
tags: [Status]
summary: Check the configured public URL
description: Probes the MCP endpoint at the supplied HTTPS origin without following redirects.
description: >-
Probes the MCP endpoint at the supplied HTTPS origin without following redirects.
Persists the latest result, including failures, as internal publicUrlCheck metadata.
PUT /settings also runs this check when publicBaseUrl changes. Stored results
apply only when their exact publicBaseUrl matches the current setting.
Clients cannot overwrite this metadata through PUT /settings.
operationId: checkPublicUrl
security:
- AdminSession: []
Expand All @@ -463,6 +468,7 @@ paths:
ok: { type: boolean }
status: { type: [integer, 'null'] }
url: { type: string }
publicUrlCheck: { $ref: '#/components/schemas/PublicUrlCheck' }
error: { type: string }
'400':
$ref: '#/components/responses/BadRequest'
Expand Down Expand Up @@ -1945,6 +1951,22 @@ components:
format: uri
additionalProperties: false

PublicUrlCheck:
type: object
description: Internal persisted check metadata, not an editable or settings.js-lockable setting.
required: [publicBaseUrl, status, ok, checkedAt]
properties:
publicBaseUrl:
type: string
description: Exact Public URL value checked. Ignore the result for other URL values.
status:
type: [integer, 'null']
description: HTTP status, or null for network, timeout, or validation failures.
ok:
type: boolean
checkedAt:
type: string
format: date-time
AiAccessSettingsResponse:
type: object
required:
Expand All @@ -1971,6 +1993,17 @@ components:
type: string
apiBase:
type: string
publicUrlCheck:
oneOf:
- $ref: '#/components/schemas/PublicUrlCheck'
- type: object
required: [publicBaseUrl, pending]
properties:
publicBaseUrl: { type: string }
pending: { type: boolean, const: true }
- type: 'null'
description: >-
Latest persisted check, null before any check or when cleared, or {publicBaseUrl, pending: true} while an automatic check is queued or running. PUT persists and returns without waiting for the probe. Poll GET until a non-pending check matches the current Public URL. Checks run serially; superseded automatic checks are skipped and superseded results are ignored.
settings:
type: object
required:
Expand Down
51 changes: 49 additions & 2 deletions src/plugin/editor-sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/** Derived from package.json via build-time `__AI_ACCESS_VERSION__` (not hand-typed). */
export const EDITOR_SIDEBAR_ASSET_VERSION = packageVersion();

function editorSidebarStyles(): string {

Check warning on line 14 in src/plugin/editor-sidebar.ts

View workflow job for this annotation

GitHub Actions / Code Quality

Function 'editorSidebarStyles' has too many lines (189). Maximum allowed is 150
return `#red-ui-sidebar-ai-access {
position: relative;
height: 100%;
Expand All @@ -19,6 +19,36 @@
flex-direction: column;
box-sizing: border-box;
}

#red-ui-sidebar-ai-access #ai-access-oauth-enabled {
appearance: none;
box-sizing: border-box;
display: inline-block;
position: relative;
width: 2.4rem;
height: 1.35rem;
flex: 0 0 auto;
border: 1px solid var(--red-ui-form-input-border-color, #888);
border-radius: 1rem;
background: var(--red-ui-form-input-border-color, #888);
cursor: pointer;
vertical-align: middle;
}
#red-ui-sidebar-ai-access #ai-access-oauth-enabled::before {
content: '';
position: absolute;
top: 0.15rem;
left: 0.15rem;
width: 0.9rem;
height: 0.9rem;
border-radius: 50%;
background: var(--red-ui-primary-background, #fff);
}
#red-ui-sidebar-ai-access #ai-access-oauth-enabled:checked { background: var(--red-ui-text-color-link, #4682b4); border-color: var(--red-ui-text-color-link, #4682b4); }
#red-ui-sidebar-ai-access #ai-access-oauth-enabled:checked::before { left: calc(100% - 1.05rem); }
#red-ui-sidebar-ai-access #ai-access-oauth-enabled:focus-visible { outline: 2px solid var(--red-ui-form-input-border-selected-color, #4682b4); outline-offset: 3px; }
#red-ui-sidebar-ai-access #ai-access-oauth-enabled:disabled { opacity: 0.5; cursor: not-allowed; }

#red-ui-sidebar-ai-access .red-ui-sidebar-header {
text-align: left;
font-weight: 600;
Expand Down Expand Up @@ -171,7 +201,7 @@
}`;
}

function editorSidebarScriptPartA(): string {

Check warning on line 204 in src/plugin/editor-sidebar.ts

View workflow job for this annotation

GitHub Actions / Code Quality

Function 'editorSidebarScriptPartA' has too many lines (308). Maximum allowed is 150
return `
/**
* Editor right-hand sidebar tab: AI Access.
Expand Down Expand Up @@ -326,7 +356,13 @@
$('#ai-access-local-row').toggle(!ADMIN_AUTH);

var oauth = s.oauth || {};
var check = data.publicUrlCheck;
var publicUrlFailed = check && check.publicBaseUrl === s.publicBaseUrl && check.ok === false;
$('#ai-access-oauth-public-url-warning')
.text(!s.publicBaseUrl ? 'Set a Public URL so OAuth clients can discover this server.' : 'The latest Public URL check failed. OAuth clients may not discover this server. Check the Public URL in Settings.')
.toggle(!!oauth.enabled && (!s.publicBaseUrl || publicUrlFailed));
$('#ai-access-oauth-enabled').prop('checked', !!oauth.enabled)
.attr('aria-checked', String(!!oauth.enabled))
.prop('disabled', readonly || sources.oauth === 'settings.js');
$('#ai-access-readonly-note').toggle(readonly);
$('#ai-access-lock-enabled').toggle(sources.enabled === 'settings.js');
Expand Down Expand Up @@ -396,6 +432,7 @@
}

function oauthPublicUrlWarning(value) {
if (LAST && LAST.publicUrlCheck && LAST.publicUrlCheck.publicBaseUrl === value && LAST.publicUrlCheck.ok === false) return 'The latest Public URL check failed. OAuth clients may not discover this server.';
if (!value) return 'No Public URL is configured. OAuth clients may not discover this server.';
try {
var url = new URL(value);
Expand Down Expand Up @@ -446,7 +483,7 @@
if (warning) {
openConfirmDialog(warning + '\\n\\nEnable OAuth anyway? (Set the Public URL on the AI Access dashboard.)').then(function (accepted) {
if (accepted) persistSettings(payload);
else $('#ai-access-oauth-enabled').prop('checked', false);
else $('#ai-access-oauth-enabled').prop('checked', false).attr('aria-checked', 'false');
});
return;
}
Expand Down Expand Up @@ -530,13 +567,14 @@

// (2) Enable MCP OAuth.
row = $('<div class="red-ui-settings-row"></div>').appendTo(body);
$('<input id="ai-access-oauth-enabled" type="checkbox">').appendTo(row);
$('<input id="ai-access-oauth-enabled" type="checkbox" role="switch" aria-checked="false">').appendTo(row);
$('<label for="ai-access-oauth-enabled"></label>')
.html('<p>Enable MCP OAuth</p>MCP clients (ChatGPT, Claude, …) sign in at &lt;Public URL&gt;/mcp.')
.appendTo(row);
$('<span id="ai-access-oauth-lock" class="form-tips" style="display:none"></span>')
.text('🔒 settings.js').appendTo(row);

$('<div id="ai-access-oauth-public-url-warning" class="form-tips ai-access-oauth-warning" role="alert" style="display:none"></div>').appendTo(body);
// (3) OAuth route conflict warning — only rendered when conflicts exist.
$('<div id="ai-access-oauth-route-warning" class="form-tips ai-access-oauth-warning" role="status" style="display:none"></div>').appendTo(body);

Expand All @@ -545,6 +583,15 @@
saveSettings
);

$('#ai-access-oauth-enabled').on('change', function () {
$('#ai-access-oauth-enabled').attr('aria-checked', String($('#ai-access-oauth-enabled').is(':checked')));
}).on('keydown', function (ev) {
if (ev.key !== 'Enter') return;
ev.preventDefault();
var control = $('#ai-access-oauth-enabled');
if (!control.prop('disabled') && !ev.repeat) control[0].click();
});

// (4) Open the dashboard.
row = $('<div class="ai-access-actions"></div>').appendTo(body);
$('<button type="button" id="ai-access-open-mcp" class="red-ui-button"></button>')
Expand Down Expand Up @@ -591,7 +638,7 @@
* flow/subflow tab (RED.workspaces.show) and acks so the dashboard does not
* open another editor tab. Ids are looked up in RED.nodes; nothing else runs.
*/
export function editorRevealListenerScript(): string {

Check warning on line 641 in src/plugin/editor-sidebar.ts

View workflow job for this annotation

GitHub Actions / Code Quality

Function 'editorRevealListenerScript' has too many lines (187). Maximum allowed is 150
return `(function () {
if (typeof RED === 'undefined') return;
if (window.__com55AiAccessRevealListener) return;
Expand Down
4 changes: 3 additions & 1 deletion src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import { aiRedactingAdapter, rawFlowAdapter } from './secret-redaction.js';
import { createPluginServices, type CreatePluginServicesOptions } from './services.js';
import {
checkPublicUrlHandler,
disposePublicUrlChecks,
getSettingsHandler,
putSettingsHandler,
} from './settings-routes.js';
Expand Down Expand Up @@ -203,6 +204,7 @@ export function registerRoutes(
recordLiveSnapshot(rawFlowAdapter(services.flowAdapter), event.config);
};
const shutdown = () => {
disposePublicUrlChecks(services.settingsStore);
try {
oauthStore.dispose();
} catch (error) {
Expand Down Expand Up @@ -499,7 +501,7 @@ export function registerRoutes(
requireAdminSession(RED, 'api'),
requireStarPermission(RED),
requireJsonContentType('rest'),
asyncRoute(checkPublicUrlHandler(RED))
asyncRoute(checkPublicUrlHandler(RED, services.settingsStore))
);

const linkGuards = [
Expand Down
Loading
Loading