Skip to content

Add Dynamic Tab and Plugin Loading#3977

Draft
AaravMalani wants to merge 3 commits into
masterfrom
feat/add-tab-manager
Draft

Add Dynamic Tab and Plugin Loading#3977
AaravMalani wants to merge 3 commits into
masterfrom
feat/add-tab-manager

Conversation

@AaravMalani

Copy link
Copy Markdown
Member

Description

This PR helps add dynamic loading of tabs from external plugins. It provides an implementation of the ITabService passed to web plugins when plugins are initially loaded, creating a sideContentManager which emits tab changes to the required workspaces.

It also adds a plugin registry which stores installable plugins implemented in the frontend (autocomplete, etc.). It utilises the plugin directory to load external plugins if an installable plugin is not currently present with a given ID.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Code quality improvements

How to test

Checklist

  • I have tested this code
  • I have updated the documentation

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 55012dde-d7d2-4847-98a9-2a63b87af85c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces dynamic plugin loading and tab management by integrating @sourceacademy/common-tabs and introducing a SideContentManager (TabService) to handle dynamic tab registration, visibility, and rendering. It also updates the conductor evaluation flow to support loading external plugins dynamically. The code review feedback highlights several key improvements: first, tabs should not be cleared immediately upon evaluation or conductor termination (as this causes them to disappear prematurely), but rather at the start of a new evaluation; second, newly registered tabs should default to being visible; and finally, the subscribe method in TabService should be defined as an arrow function to avoid inefficient re-binding and re-subscription cycles in SideContentProvider.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 577 to 579
yield call([conduit, 'terminate']);
yield call([sideContentManager, sideContentManager.clearTabs]);
yield cancel(stdoutTask);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Clearing the tabs immediately when the evaluation finishes or is terminated will cause any dynamically loaded tabs (such as visualization or interactive game tabs) to disappear as soon as the program stops running.

To allow users to interact with and view the tabs after the program execution completes, the tabs should persist. Instead of clearing them here at the end of the evaluation, you should clear them at the beginning of evalCodeConductorSaga when a new execution is initiated.

Suggested change
yield call([conduit, 'terminate']);
yield call([sideContentManager, sideContentManager.clearTabs]);
yield cancel(stdoutTask);
yield call([conduit, 'terminate']);
yield cancel(stdoutTask);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That isn't true, it doesn't immediately vanish. It does reload when the run button is pressed, so maybe finding a way to persistently get run the Conductor across evaluations is something to consider

Comment thread src/commons/sagas/helpers/conductorEvaluatorCache.ts
Comment thread src/commons/sideContent/SideContentManager.ts
Comment on lines +66 to +69
subscribe(listener: Listener): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defining subscribe as a standard method and then binding it on every render in SideContentProvider (via sideContentManager.subscribe.bind(sideContentManager)) creates a new function reference on every render. This causes useSyncExternalStore to unsubscribe and re-subscribe on every single render, which is highly inefficient.

To fix this, define subscribe as an arrow function property in SideContentManager. This automatically binds the method to the instance and ensures a stable function reference, allowing you to pass sideContentManager.subscribe directly without .bind.

Suggested change
subscribe(listener: Listener): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
subscribe = (listener: Listener): () => void => {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
};

Comment on lines +60 to +63
const serviceTabs = useSyncExternalStore(
sideContentManager.subscribe.bind(sideContentManager),
() => sideContentManager.getTabs(workspaceLocation),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Now that subscribe is defined as a stable arrow function on sideContentManager, you can pass it directly to useSyncExternalStore without calling .bind(). This avoids creating a new function reference on every render and prevents unnecessary unsubscriptions/resubscriptions.

Suggested change
const serviceTabs = useSyncExternalStore(
sideContentManager.subscribe.bind(sideContentManager),
() => sideContentManager.getTabs(workspaceLocation),
);
const serviceTabs = useSyncExternalStore(
sideContentManager.subscribe,
() => sideContentManager.getTabs(workspaceLocation),
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant