{
+ const moduleNamespace: PluginExports = await import(/* webpackIgnore: true */ url);
+ const pluginClass = resolvePluginClass(moduleNamespace);
+ if (typeof pluginClass !== 'function') {
+ throw new Error(`Conductor web plugin at "${url}" did not export a plugin class`);
+ }
+ hostPlugin.registerPlugin(pluginClass as PluginClass<[ITabService]>, tabService);
+}
diff --git a/src/index.tsx b/src/index.tsx
index b2e5e74967..c47bae8dbc 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,6 +1,8 @@
import 'src/i18n/i18n';
import 'src/styles/global.css';
import 'src/styles/index.scss';
+// Expose shared libs for dynamically-loaded Conductor web plugins (must run before any plugin loads).
+import 'src/bootstrap/conductorSharedDeps';
import { Button, OverlaysProvider } from '@blueprintjs/core';
import { setModulesStaticURL } from 'js-slang/dist/modules/loader';
@@ -10,7 +12,10 @@ import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import javascript from 'react-syntax-highlighter/dist/esm/languages/prism/javascript';
import Constants, { Links } from 'src/commons/utils/Constants';
import { showWarningMessage } from 'src/commons/utils/notifications/NotificationsHelper';
-import { register as registerServiceWorker } from 'src/commons/utils/RegisterServiceWorker';
+import {
+ register as registerServiceWorker,
+ unregister as unregisterServiceWorker,
+} from 'src/commons/utils/RegisterServiceWorker';
import { triggerSyncLogs } from 'src/features/eventLogging/client';
import { store } from 'src/pages/createStore';
@@ -49,26 +54,34 @@ createInBrowserFileSystem(store)
);
});
-registerServiceWorker({
- onUpdate: registration => {
- showWarningMessage(
-
- A new version of Source Academy is available.
-
-
,
- 0,
- );
- },
-});
+if (process.env.NODE_ENV === 'production') {
+ registerServiceWorker({
+ onUpdate: registration => {
+ showWarningMessage(
+
+ A new version of Source Academy is available.
+
+
,
+ 0,
+ );
+ },
+ });
+} else {
+ // In development we never want a service worker: a stale one (from a production build or earlier
+ // visit) serves the cached app shell for every request, so fetched evaluator/worker scripts come
+ // back as `index.html` and Conductor runs hang. Proactively unregister any SW and clear its
+ // caches on every dev load so this self-heals and never recurs.
+ unregisterServiceWorker();
+}
if (Constants.cadetLoggerUrl) {
// Seriously: registerServiceWorker onSuccess and onUpdate are separate paths.
diff --git a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx
index 964aef234d..801337564c 100644
--- a/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx
+++ b/src/pages/academy/grading/subcomponents/GradingWorkspace.tsx
@@ -38,6 +38,7 @@ import type { SideContentProps } from '../../../../commons/sideContent/SideConte
import { useSideContent } from '../../../../commons/sideContent/SideContentHelper';
import {
type SideContentTab,
+ type SideContentTabId,
SideContentType,
} from '../../../../commons/sideContent/SideContentTypes';
import Workspace, { type WorkspaceProps } from '../../../../commons/workspace/Workspace';
@@ -425,8 +426,8 @@ function GradingWorkspace(props: Props) {
const sideContentProps: SideContentProps = {
onChange: (
- newTabId: SideContentType,
- prevTabId: SideContentType,
+ newTabId: SideContentTabId,
+ prevTabId: SideContentTabId,
event: React.MouseEvent,
) => {
if (newTabId === prevTabId) {
diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx
index 1abe0f2c3a..a450151ee7 100644
--- a/src/pages/playground/Playground.tsx
+++ b/src/pages/playground/Playground.tsx
@@ -34,6 +34,7 @@ import {
} from 'src/commons/utils/WarningDialogHelper';
import WorkspaceActions from 'src/commons/workspace/WorkspaceActions';
import type { WorkspaceLocation } from 'src/commons/workspace/WorkspaceTypes';
+import { selectConductorEnable } from 'src/features/conductor/flagConductorEnable';
import CseMachine from 'src/features/cseMachine/CseMachine';
import GithubActions from 'src/features/github/GitHubActions';
import PersistenceActions from 'src/features/persistence/PersistenceActions';
@@ -82,7 +83,6 @@ import { generateLanguageIntroduction } from '../../commons/utils/IntroductionHe
import { convertParamToBoolean, convertParamToInt } from '../../commons/utils/ParamParseHelper';
import { type IParsedQuery, parseQuery } from '../../commons/utils/QueryHelper';
import Workspace, { type WorkspaceProps } from '../../commons/workspace/Workspace';
-import { selectConductorEnable } from '../../features/conductor/flagConductorEnable';
import { initSession, log } from '../../features/eventLogging';
import type {
CodeDelta,
@@ -194,6 +194,10 @@ export async function handleHash(
}
}
+// Tab id exposed by the conductor stepper web plugin. The frontend mirrors this contract to hide
+// the legacy REPL/resizing when that plugin's tab is active (see usages below).
+const CONDUCTOR_STEPPER_TAB_ID = 'stepper';
+
function Playground(props: PlaygroundProps) {
const { isSicpEditor } = props;
const workspaceLocation: WorkspaceLocation = isSicpEditor ? 'sicp' : 'playground';
@@ -745,6 +749,9 @@ function Playground(props: PlaygroundProps) {
? conductorEvaluatorSupportsCse || hasCseSnapshots
: languageConfig.supports.cseMachine || hasCseSnapshots;
const shouldShowSubstVisualizer = languageConfig.supports.substVisualizer;
+ // When the Conductor framework is enabled, the stepper (and other tools) are provided by web
+ // plugins loaded dynamically, so the legacy in-frontend tabs are hidden in favour of plugin tabs.
+ const conductorEnabled = useTypedSelector(selectConductorEnable);
const conductorWelcomeText = useTypedSelector(state => {
if (!selectConductorEnable(state)) return null;
@@ -797,9 +804,12 @@ function Playground(props: PlaygroundProps) {
if (shouldShowCseMachine) {
tabs.push(makeCseMachineTabFrom(workspaceLocation));
}
- if (shouldShowSubstVisualizer) {
+ // The legacy stepper tab is only shown with the old (non-conductor) pipeline.
+ if (shouldShowSubstVisualizer && !conductorEnabled) {
tabs.push(makeSubstVisualizerTabFrom(workspaceLocation, output));
}
+ // Under the conductor, tools are contributed by dynamically-loaded web plugins; their tabs are
+ // injected automatically by SideContentProvider (via the shared tab service), not here.
}
if (!isSicpEditor && !Constants.playgroundOnly) {
@@ -821,6 +831,7 @@ function Playground(props: PlaygroundProps) {
shouldShowDataVisualizer,
shouldShowCseMachine,
shouldShowSubstVisualizer,
+ conductorEnabled,
remoteExecutionTab,
editorSessionId,
sessionManagementTab,
@@ -989,7 +1000,10 @@ function Playground(props: PlaygroundProps) {
sourceVariant: languageConfig.variant,
externalLibrary: ExternalLibraryName.NONE, // temporary placeholder as we phase out libraries
hidden:
- selectedTab === SideContentType.substVisualizer || selectedTab === SideContentType.cseMachine,
+ selectedTab === SideContentType.substVisualizer ||
+ selectedTab === SideContentType.cseMachine ||
+ // When the conductor stepper plugin tab is active, also hide the REPL (matches legacy behaviour)
+ (conductorEnabled && (selectedTab as string) === CONDUCTOR_STEPPER_TAB_ID),
inputHidden: replDisabled,
replButtons: [replDisabled ? null : evalButton, clearButton],
disableScrolling: isSicpEditor,
@@ -1056,7 +1070,10 @@ function Playground(props: PlaygroundProps) {
workspaceLocation,
},
sideContentIsResizeable:
- selectedTab !== SideContentType.substVisualizer && selectedTab !== SideContentType.cseMachine,
+ selectedTab !== SideContentType.substVisualizer &&
+ selectedTab !== SideContentType.cseMachine &&
+ // When the conductor stepper plugin tab is active, also disable resizing (matches legacy behaviour)
+ !(conductorEnabled && (selectedTab as string) === CONDUCTOR_STEPPER_TAB_ID),
};
const mobileWorkspaceProps: MobileWorkspaceProps = {
diff --git a/src/pages/playground/PlaygroundTabs.tsx b/src/pages/playground/PlaygroundTabs.tsx
index 7496ea8539..1d2ae958a9 100644
--- a/src/pages/playground/PlaygroundTabs.tsx
+++ b/src/pages/playground/PlaygroundTabs.tsx
@@ -8,14 +8,15 @@ import SideContentSubstVisualizer from 'src/commons/sideContent/content/SideCont
import {
type SideContentLocation,
type SideContentTab,
+ type SideContentTabId,
SideContentType,
} from 'src/commons/sideContent/SideContentTypes';
-export const mobileOnlyTabIds: readonly SideContentType[] = [
+export const mobileOnlyTabIds: readonly SideContentTabId[] = [
SideContentType.mobileEditor,
SideContentType.mobileEditorRun,
];
-export const desktopOnlyTabIds: readonly SideContentType[] = [SideContentType.introduction];
+export const desktopOnlyTabIds: readonly SideContentTabId[] = [SideContentType.introduction];
export const makeIntroductionTabFrom = (content: string): SideContentTab => ({
label: 'Introduction',
diff --git a/src/styles/StepperPopover.scss b/src/styles/StepperPopover.scss
index 553009be57..687ffac09c 100644
--- a/src/styles/StepperPopover.scss
+++ b/src/styles/StepperPopover.scss
@@ -38,5 +38,6 @@
.stepper-display {
font: $stepper-display-font;
color: $stepper-conditional-color;
+ margin-bottom: 16px;
}
}
diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss
index f55c293a88..dabe5e409b 100644
--- a/src/styles/_workspace.scss
+++ b/src/styles/_workspace.scss
@@ -268,7 +268,7 @@ $code-color-notification: #f9f0d7;
word-break: break-word;
color: $code-color-result;
text-align: justify;
- overflow-x: auto;
+ overflow-x: hidden;
/* Respect padding of containing bp3 Card when scrollable */
margin-bottom: 0.4rem;
@@ -406,7 +406,8 @@ $code-color-notification: #f9f0d7;
// Specific CSS for the Stepper and CSE Machine tab, since REPL is hidden
##{$ns}-tab-panel_side-content-tabs_subst_visualiser,
- ##{$ns}-tab-panel_side-content-tabs_cse_machine {
+ ##{$ns}-tab-panel_side-content-tabs_cse_machine,
+ ##{$ns}-tab-panel_side-content-tabs_stepper {
height: calc(100% - 60px);
margin-top: -45px;
diff --git a/yarn.lock b/yarn.lock
index fbe4ffa5ab..153b7ff572 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3169,10 +3169,10 @@ __metadata:
languageName: node
linkType: hard
-"@sourceacademy/plugin-directory@https://github.com/source-academy/plugin-directory.git#0.0.2":
- version: 0.0.2
- resolution: "@sourceacademy/plugin-directory@https://github.com/source-academy/plugin-directory.git#commit=8b31c1a2abea6b42d52267c8ea343ee49ec5d857"
- checksum: 10c0/29dbaddeda6efbacaacbeaf8d82816cba4f0ff080a1cdc451f6ae5c71de4c4519890d8a17a10161af0903f894f8b67ee2ae9044e6d1ca003522a05a15af42882
+"@sourceacademy/plugin-directory@https://github.com/source-academy/plugin-directory.git#0.0.3":
+ version: 0.0.3
+ resolution: "@sourceacademy/plugin-directory@https://github.com/source-academy/plugin-directory.git#commit=bdc6343a30009fb91077410c2ea4652fea945f64"
+ checksum: 10c0/5534d545a900722ae5ddcb6ea822d1d20de1d464e54cf6c6774bc1ba52ff2832d6a6f1ce9815917a61b5239fb4f8a5a72d8ce02eb42c6922c39bf5bb614fa0f3
languageName: node
linkType: hard
@@ -7245,7 +7245,7 @@ __metadata:
"@sourceacademy/c-slang": "npm:^1.0.21"
"@sourceacademy/conductor": "npm:^0.5.0"
"@sourceacademy/language-directory": "https://github.com/source-academy/language-directory.git#0.0.10"
- "@sourceacademy/plugin-directory": "https://github.com/source-academy/plugin-directory.git#0.0.2"
+ "@sourceacademy/plugin-directory": "https://github.com/source-academy/plugin-directory.git#0.0.3"
"@sourceacademy/sharedb-ace": "npm:2.1.1"
"@sourceacademy/sling-client": "npm:^0.1.0"
"@sourceacademy/web-cse-machine": "npm:^1.0.0"