Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2554523
Add automatic sidebar sync with canvas updates
rabea-al Oct 4, 2025
8e76f8b
add core backup function
MFA-X-AI Oct 6, 2025
8df5a9d
add update all feature
MFA-X-AI Oct 8, 2025
4a70e72
throw message error if running update without lib name
MFA-X-AI Oct 8, 2025
fb43ea3
allow no-overwrite update flag, skip diff for .xircuits files
MFA-X-AI Oct 8, 2025
f7257d2
fix syntax errors
MFA-X-AI Oct 8, 2025
a687c90
reuse existing functions
MFA-X-AI Oct 8, 2025
819a859
Add "View details" dialog for long notifications
rabea-al Oct 8, 2025
c207cb7
clean up library ID returned by resolveLibraryForNode
rabea-al Oct 9, 2025
7087a46
Merge branch 'master' into notification-view-details
MFA-X-AI Oct 9, 2025
9a353a4
Improve inline copy button with temporary "Copied" hint in details di…
rabea-al Oct 12, 2025
9d15615
Fix copy button overlap
rabea-al Oct 13, 2025
41bc12f
Revert "Improve inline copy button with temporary "Copied" hint in de…
rabea-al Oct 13, 2025
136a9d4
Make canvas updates fire only after real link changes, not every smal…
rabea-al Oct 13, 2025
94671ad
Add CLI update tests (PR #450)
rabea-al Oct 14, 2025
543aa68
over core libs no-overwrite + preserve locals w/o prune
rabea-al Oct 14, 2025
62569ee
Merge pull request #459 from XpressAI/fahreza/backup-core
MFA-X-AI Feb 17, 2026
78230fe
Merge pull request #460 from rabea-al/notification-view-details
MFA-X-AI Feb 17, 2026
97a0b3e
Merge pull request #457 from rabea-al/feature/sidebar-auto-sync
MFA-X-AI Feb 17, 2026
a891bb9
Add CLI update tests (PR #450)
rabea-al Oct 14, 2025
cdaad95
over core libs no-overwrite + preserve locals w/o prune
rabea-al Oct 14, 2025
be4a2e9
Merge branch 'cli-update-pr450' of https://github.com/rabea-al/xircui…
MFA-X-AI Feb 18, 2026
35e358c
fix broken tests, update with new ones based on new cli
MFA-X-AI Feb 18, 2026
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
44 changes: 42 additions & 2 deletions src/component_info_sidebar/ComponentPreviewWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { collectParamIO } from './portPreview';
import { IONodeTree } from './IONodeTree';
import type { IONode } from './portPreview';
import { commandIDs } from "../commands/CommandIDs";
import { canvasUpdatedSignal } from '../components/XircuitsBodyWidget';

export interface IComponentInfo {
name: string;
Expand Down Expand Up @@ -156,7 +157,7 @@ class OverviewSection extends ReactWidget {
}
setModel(m: IComponentInfo | null) {
this._model = m;
this.update();
this.update();
}
render(): JSX.Element {
if (!this._model) {
Expand Down Expand Up @@ -321,6 +322,7 @@ export class ComponentPreviewWidget extends SidePanel {
const shell = this._app.shell as ILabShell;
shell.expandRight();
shell.activateById(this.id);
this._bindCanvasListener();
}

private _computeToolbarState(): ToolbarState {
Expand Down Expand Up @@ -357,9 +359,47 @@ export class ComponentPreviewWidget extends SidePanel {
canOpenScript: !!m.node && !isStartFinish,
canCenter: !!(m.node && m.engine),
canOpenWorkflow: nodeType === 'xircuits_workflow',
canCollapse: !isStartFinish
canCollapse: !isStartFinish
};
}

private _isListening = false;

private _bindCanvasListener(): void {
if (this._isListening || this.isDisposed) return;

const onCanvasUpdate = () => {
const engine = this._model?.engine;
const currentNode = this._model?.node;
if (!engine || !currentNode) return;

// Refresh node reference in case the model recreated it after a change
const id = currentNode.getID?.();
const latestNode = engine.getModel?.().getNodes?.().find(n => n.getID?.() === id);
if (latestNode && latestNode !== currentNode) {
this._model!.node = latestNode;
}

try {
const { inputs = [], outputs = [] } = collectParamIO(this._model!.node as any);
this._inputs.setData(inputs);
this._outputs.setData(outputs);
} catch (err) {
console.warn('[Sidebar] Failed to collect I/O, keeping previous state:', err);
}


this._topbar?.update();
};

canvasUpdatedSignal.connect(onCanvasUpdate, this);
this._isListening = true;

this.disposed.connect(() => {
canvasUpdatedSignal.disconnect(onCanvasUpdate, this);
this._isListening = false;
});
}

private _navigate(step: -1 | 1) {
const node = this._model?.node;
Expand Down
31 changes: 31 additions & 0 deletions src/components/XircuitsBodyWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ const ZoomControls = styled.div<{visible: boolean}>`
}
`;

export type CanvasUpdatedPayload = { reason: 'content'; };

export const canvasUpdatedSignal = new Signal<Window, CanvasUpdatedPayload>(window);

export const BodyWidget: FC<BodyWidgetProps> = ({
context,
xircuitsApp,
Expand Down Expand Up @@ -450,12 +454,37 @@ export const BodyWidget: FC<BodyWidgetProps> = ({
setSaved(false);
}
}, []);

// Schedule a single canvas update per frame and ignore incomplete link drags
const scheduleCanvasEmit = React.useMemo(() => {
let scheduled = false;
return () => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;

const model = xircuitsApp.getDiagramEngine().getModel();

// skip if a link is still being dragged (no target port yet)
const draggingUnfinished =
!!model &&
Object.values(model.getLinks?.() ?? {}).some(
(l: any) => !(l?.getTargetPort?.())
);
if (!draggingUnfinished) {
canvasUpdatedSignal.emit({ reason: 'content' });
}
});
};
}, [xircuitsApp]);

const onChange = useCallback((): void => {
if (skipSerializationRef.current) {
return;
}
serializeModel();
scheduleCanvasEmit();
}, [serializeModel]);


Expand Down Expand Up @@ -485,6 +514,7 @@ export const BodyWidget: FC<BodyWidgetProps> = ({
return () => clearTimeout(timeout);
},
linksUpdated: (event) => {
scheduleCanvasEmit();
const timeout = setTimeout(() => {
event.link.registerListener({
sourcePortChanged: () => {
Expand All @@ -508,6 +538,7 @@ export const BodyWidget: FC<BodyWidgetProps> = ({
xircuitsApp.getDiagramEngine().setModel(deserializedModel);
clearSearchFlags();


// On the first load, clear undo history and register global engine listeners
if (initialRender.current) {
currentContext.model.sharedModel.clearUndoHistory();
Expand Down
74 changes: 74 additions & 0 deletions src/helpers/notificationAugmentor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Notification, showDialog, Dialog } from '@jupyterlab/apputils';
import { Widget } from '@lumino/widgets';

const MAX_VISIBLE_CHARS = 140;
const VIEW_DETAILS_LABEL = 'View details';

function toPlainText(value: unknown): string {
try {
return typeof value === 'string' ? value : JSON.stringify(value, null, 2);
} catch {
return String(value ?? '');
}
}

function createViewDetailsAction(fullMessage: string, dialogTitle = 'Details'): Notification.IAction {
return {
label: VIEW_DETAILS_LABEL,
caption: 'Show full message',
callback: async () => {
const dialogBody = new Widget();
dialogBody.addClass('xircuits-notification-details');

const pre = document.createElement('pre');
pre.textContent = fullMessage;
dialogBody.node.appendChild(pre);

const copyButton = Dialog.createButton({ label: 'Copy' });
const closeButton = Dialog.okButton({ label: 'Close' });

const result = await showDialog({
title: dialogTitle,
body: dialogBody,
buttons: [copyButton, closeButton]
});

if (result.button.label === 'Copy') {
await navigator.clipboard.writeText(fullMessage);
}
}
};
}

function ensureViewDetailsAction(
messageText: string,
options: any = {},
title?: string
): any {
if (messageText.length <= MAX_VISIBLE_CHARS) return options;
const actions = [...(options.actions ?? [])];
if (!actions.some((a: any) => a?.label === VIEW_DETAILS_LABEL)) {
actions.push(createViewDetailsAction(messageText, title));
}
return { ...options, actions };
}

export function augmentNotifications(): void {
const NotificationObj: any = Notification as any;
if (NotificationObj.__xircuitsAugmented) return;

const wrap = (method: 'error' | 'warning' | 'info' | 'success') => {
const original = NotificationObj[method]?.bind(Notification);
if (!original) return;

NotificationObj[method] = (...args: any[]) => {
const [rawMessage, rawOptions] = args;
const text = toPlainText(rawMessage);
const options = ensureViewDetailsAction(text, rawOptions);
return original(text, options);
};
};

['error', 'warning', 'info', 'success'].forEach(wrap);
NotificationObj.__xircuitsAugmented = true;
}
4 changes: 3 additions & 1 deletion src/helpers/notificationEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ export async function resolveLibraryForNode(
const candidateId = pathToLibraryId(extras.path);
if (!candidateId) return { libId: null, status: 'unknown' };

const cleanLibId = candidateId.replace(/^xai_components[\/\\]/i, '');

const idx = await loadLibraryIndex();
const entry = idx.get(candidateId);
return computeStatusFromEntry(entry, candidateId);
return computeStatusFromEntry(entry, cleanLibId);
}

export async function showInstallForRemoteLibrary(args: {
Expand Down
8 changes: 6 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ import type { Signal } from "@lumino/signaling";
import { commandIDs } from "./commands/CommandIDs";
import { IEditorTracker } from '@jupyterlab/fileeditor';
import { IMainMenu } from '@jupyterlab/mainmenu';
import { installLibrarySilently } from './context-menu/TrayContextMenu';
import { normalizeLibraryName } from './tray_library/ComponentLibraryConfig';
import { handleInstall, installLibrarySilently } from './context-menu/TrayContextMenu';
import { augmentNotifications } from './helpers/notificationAugmentor';
import { loadLibraryIndex } from './helpers/notificationEffects';
import { normalizeLibraryName } from './tray_library/ComponentLibraryConfig';
import { installComponentPreview } from './component_info_sidebar/previewHelper';
const FACTORY = 'Xircuits editor';

Expand Down Expand Up @@ -81,6 +82,9 @@ const xircuits: JupyterFrontEndPlugin<void> = {

console.log('Xircuits is activated!');

// Add "View details" to long notifications
augmentNotifications();

// Creating the widget factory to register it so the document manager knows about
// our new DocumentWidget
const widgetFactory = new XircuitsFactory({
Expand Down
16 changes: 13 additions & 3 deletions style/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ body.light-mode jp-button[title="Toggle Light/Dark Mode"] .moon { visibility:

body.light-mode jp-button[title="Toggle Light/Dark Mode"] .sun { visibility: visible; }




.xircuits-notification-details {
max-height: 60vh;
overflow: auto;
padding: 0.25rem;
}

.xircuits-notification-details pre {
white-space: pre-wrap;
word-break: break-word;
margin: 0;
font-family: var(--jp-code-font-family);
font-size: var(--jp-code-font-size);
}
Loading