From 5136206f74db971080fef11946edc96fc3e367b5 Mon Sep 17 00:00:00 2001
From: tsammeow <97447335+tsammeow@users.noreply.github.com>
Date: Wed, 12 Mar 2025 20:07:19 +0800
Subject: [PATCH 1/2] implement dynamic plugin loading

---
 package.json                                        |  1 +
 src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts |  3 +--
 src/features/conductor/BrowserHostPlugin.ts         | 11 ++++++-----
 src/features/conductor/createConductor.ts           |  5 ++---
 yarn.lock                                           |  8 ++++++++
 5 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/package.json b/package.json
index c3684f33f0..a164d5514f 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
     "mdast-util-to-hast": "^13.0.0",
     "normalize.css": "^8.0.1",
     "phaser": "~3.87.0",
+    "plugin-directory": "https://github.com/source-academy/plugin-directory.git",
     "query-string": "^9.0.0",
     "re-resizable": "^6.9.9",
     "react": "^18.3.1",
diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
index 28217b184e..b5dc56e113 100644
--- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
+++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
@@ -499,8 +499,7 @@ export function* evalCodeConductorSaga(
   const { hostPlugin, conduit }: { hostPlugin: BrowserHostPlugin; conduit: IConduit } = yield call(
     createConductor,
     url,
-    async (fileName: string) => files[fileName],
-    (pluginName: string) => {} // TODO: implement dynamic plugin loading
+    async (fileName: string) => files[fileName]
   );
   const stdoutTask = yield fork(handleStdout, hostPlugin, workspaceLocation);
   yield call([hostPlugin, 'startEvaluator'], entrypointFilePath);
diff --git a/src/features/conductor/BrowserHostPlugin.ts b/src/features/conductor/BrowserHostPlugin.ts
index 80726a865e..3dd79a3783 100644
--- a/src/features/conductor/BrowserHostPlugin.ts
+++ b/src/features/conductor/BrowserHostPlugin.ts
@@ -1,26 +1,27 @@
 import { BasicHostPlugin } from 'conductor/dist/conductor/host';
 import { IChannel, IConduit } from 'conductor/dist/conduit';
+import { plugins, PluginType } from 'plugin-directory';
 
 export class BrowserHostPlugin extends BasicHostPlugin {
   requestFile(fileName: string): Promise<string | undefined> {
     return this.__onRequestFile(fileName);
   }
   requestLoadPlugin(pluginName: string): void {
-    return this.__onRequestLoadPlugin(pluginName);
+    if (pluginName in plugins) {
+      const location = plugins[pluginName as keyof typeof plugins].resolutions[PluginType.WEB];
+      if (location) this.importAndRegisterExternalPlugin(location);
+    }
   }
 
   private __onRequestFile: (fileName: string) => Promise<string | undefined>;
-  private __onRequestLoadPlugin: (pluginName: string) => void;
 
   static readonly channelAttach = super.channelAttach;
   constructor(
     conduit: IConduit,
     channels: IChannel<any>[],
-    onRequestFile: (fileName: string) => Promise<string | undefined>,
-    onRequestLoadPlugin: (pluginName: string) => void
+    onRequestFile: (fileName: string) => Promise<string | undefined>
   ) {
     super(conduit, channels);
     this.__onRequestFile = onRequestFile;
-    this.__onRequestLoadPlugin = onRequestLoadPlugin;
   }
 }
diff --git a/src/features/conductor/createConductor.ts b/src/features/conductor/createConductor.ts
index 0708aa56c8..8afde133b8 100644
--- a/src/features/conductor/createConductor.ts
+++ b/src/features/conductor/createConductor.ts
@@ -4,11 +4,10 @@ import { BrowserHostPlugin } from './BrowserHostPlugin';
 
 export function createConductor(
   evaluatorPath: string,
-  onRequestFile: (fileName: string) => Promise<string | undefined>,
-  onRequestLoadPlugin: (pluginName: string) => void
+  onRequestFile: (fileName: string) => Promise<string | undefined>
 ): { hostPlugin: BrowserHostPlugin; conduit: IConduit } {
   const worker = new Worker(evaluatorPath);
   const conduit = new Conduit(worker, true);
-  const hostPlugin = conduit.registerPlugin(BrowserHostPlugin, onRequestFile, onRequestLoadPlugin);
+  const hostPlugin = conduit.registerPlugin(BrowserHostPlugin, onRequestFile);
   return { hostPlugin, conduit };
 }
diff --git a/yarn.lock b/yarn.lock
index baa41cc656..21020189cf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9563,6 +9563,7 @@ __metadata:
     path-browserify: "npm:^1.0.1"
     phaser: "npm:~3.87.0"
     phaser3spectorjs: "npm:^0.0.8"
+    plugin-directory: "https://github.com/source-academy/plugin-directory.git"
     prettier: "npm:^3.3.3"
     process: "npm:^0.11.10"
     query-string: "npm:^9.0.0"
@@ -14044,6 +14045,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"plugin-directory@https://github.com/source-academy/plugin-directory.git":
+  version: 0.0.1
+  resolution: "plugin-directory@https://github.com/source-academy/plugin-directory.git#commit=8fadc3a0b8f656234659b457b15a0bef223f5f88"
+  checksum: 10c0/d085195118782e35af687e20e8dc97a541ccf46bd40aae441d8b8a7e49057e7d359362ecb31056a47ab76167f835f8abadff58e555e97fe33de21b6f89fc1450
+  languageName: node
+  linkType: hard
+
 "possible-typed-array-names@npm:^1.0.0":
   version: 1.0.0
   resolution: "possible-typed-array-names@npm:1.0.0"

From 9ca16a206b87c6e222b4ba9a625baaa611259ad5 Mon Sep 17 00:00:00 2001
From: tsammeow <97447335+tsammeow@users.noreply.github.com>
Date: Tue, 25 Mar 2025 12:22:33 +0800
Subject: [PATCH 2/2] implement module tab skeleton

---
 src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts | 3 ++-
 src/features/conductor/BrowserHostPlugin.ts         | 9 +++++++--
 src/features/conductor/TabDefinition.ts             | 5 +++++
 src/features/conductor/createConductor.ts           | 6 ++++--
 4 files changed, 18 insertions(+), 5 deletions(-)
 create mode 100644 src/features/conductor/TabDefinition.ts

diff --git a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
index b5dc56e113..5cd66f9812 100644
--- a/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
+++ b/src/commons/sagas/WorkspaceSaga/helpers/evalCode.ts
@@ -499,7 +499,8 @@ export function* evalCodeConductorSaga(
   const { hostPlugin, conduit }: { hostPlugin: BrowserHostPlugin; conduit: IConduit } = yield call(
     createConductor,
     url,
-    async (fileName: string) => files[fileName]
+    async (fileName: string) => files[fileName],
+    _tab => 0
   );
   const stdoutTask = yield fork(handleStdout, hostPlugin, workspaceLocation);
   yield call([hostPlugin, 'startEvaluator'], entrypointFilePath);
diff --git a/src/features/conductor/BrowserHostPlugin.ts b/src/features/conductor/BrowserHostPlugin.ts
index 3dd79a3783..c0c7e4ad5a 100644
--- a/src/features/conductor/BrowserHostPlugin.ts
+++ b/src/features/conductor/BrowserHostPlugin.ts
@@ -1,6 +1,8 @@
 import { BasicHostPlugin } from 'conductor/dist/conductor/host';
 import { IChannel, IConduit } from 'conductor/dist/conduit';
 import { plugins, PluginType } from 'plugin-directory';
+import type React from 'react';
+import { TabDefinition } from './TabDefinition';
 
 export class BrowserHostPlugin extends BasicHostPlugin {
   requestFile(fileName: string): Promise<string | undefined> {
@@ -9,19 +11,22 @@ export class BrowserHostPlugin extends BasicHostPlugin {
   requestLoadPlugin(pluginName: string): void {
     if (pluginName in plugins) {
       const location = plugins[pluginName as keyof typeof plugins].resolutions[PluginType.WEB];
-      if (location) this.importAndRegisterExternalPlugin(location);
+      if (location) this.importAndRegisterExternalPlugin(location, this.__createTab);
     }
   }
 
   private __onRequestFile: (fileName: string) => Promise<string | undefined>;
+  private __createTab: (tabDefinition: TabDefinition, tab: React.JSX.Element) => void;
 
   static readonly channelAttach = super.channelAttach;
   constructor(
     conduit: IConduit,
     channels: IChannel<any>[],
-    onRequestFile: (fileName: string) => Promise<string | undefined>
+    onRequestFile: (fileName: string) => Promise<string | undefined>,
+    createTab: (tabDefinition: TabDefinition, tab: React.JSX.Element) => void
   ) {
     super(conduit, channels);
     this.__onRequestFile = onRequestFile;
+    this.__createTab = createTab;
   }
 }
diff --git a/src/features/conductor/TabDefinition.ts b/src/features/conductor/TabDefinition.ts
new file mode 100644
index 0000000000..b8e321caff
--- /dev/null
+++ b/src/features/conductor/TabDefinition.ts
@@ -0,0 +1,5 @@
+export interface TabDefinition {
+  readonly label: string;
+  readonly iconName: string;
+  readonly description?: string;
+}
diff --git a/src/features/conductor/createConductor.ts b/src/features/conductor/createConductor.ts
index 8afde133b8..6f34e32d98 100644
--- a/src/features/conductor/createConductor.ts
+++ b/src/features/conductor/createConductor.ts
@@ -1,13 +1,15 @@
 import { Conduit, IConduit } from 'conductor/dist/conduit';
 
 import { BrowserHostPlugin } from './BrowserHostPlugin';
+import { TabDefinition } from './TabDefinition';
 
 export function createConductor(
   evaluatorPath: string,
-  onRequestFile: (fileName: string) => Promise<string | undefined>
+  onRequestFile: (fileName: string) => Promise<string | undefined>,
+  createTab: (tabDefinition: TabDefinition, tab: React.JSX.Element) => void
 ): { hostPlugin: BrowserHostPlugin; conduit: IConduit } {
   const worker = new Worker(evaluatorPath);
   const conduit = new Conduit(worker, true);
-  const hostPlugin = conduit.registerPlugin(BrowserHostPlugin, onRequestFile);
+  const hostPlugin = conduit.registerPlugin(BrowserHostPlugin, onRequestFile, createTab);
   return { hostPlugin, conduit };
 }
