@@ -7,10 +7,10 @@ import {
77} from '@jupyterlab/application' ;
88
99import {
10- ISessionContext ,
1110 DOMUtils ,
12- IToolbarWidgetRegistry ,
1311 ICommandPalette ,
12+ ISessionContext ,
13+ IToolbarWidgetRegistry ,
1414} from '@jupyterlab/apputils' ;
1515
1616import { Cell , CodeCell } from '@jupyterlab/cells' ;
@@ -21,14 +21,24 @@ import { IDocumentManager } from '@jupyterlab/docmanager';
2121
2222import { DocumentRegistry } from '@jupyterlab/docregistry' ;
2323
24+ import {
25+ IInspector ,
26+ InspectionHandler ,
27+ KernelConnector ,
28+ } from '@jupyterlab/inspector' ;
29+
2430import { IMainMenu } from '@jupyterlab/mainmenu' ;
2531
2632import {
27- NotebookPanel ,
28- INotebookTracker ,
2933 INotebookTools ,
34+ INotebookTracker ,
35+ NotebookPanel ,
3036} from '@jupyterlab/notebook' ;
3137
38+ import { Kernel , KernelMessage } from '@jupyterlab/services' ;
39+
40+ import { MimeModel } from '@jupyterlab/rendermime' ;
41+
3242import { ISettingRegistry } from '@jupyterlab/settingregistry' ;
3343
3444import { ITranslator , nullTranslator } from '@jupyterlab/translation' ;
@@ -37,6 +47,8 @@ import { INotebookShell } from '@jupyter-notebook/application';
3747
3848import { Poll } from '@lumino/polling' ;
3949
50+ import { Signal } from '@lumino/signaling' ;
51+
4052import { Widget } from '@lumino/widgets' ;
4153
4254import { TrustedComponent } from './trusted' ;
@@ -749,6 +761,239 @@ const editNotebookMetadata: JupyterFrontEndPlugin<void> = {
749761 } ,
750762} ;
751763
764+ /**
765+ * A plugin providing a pager widget to display help and documentation
766+ * in the down panel, similar to classic notebook behavior.
767+ */
768+ const pager : JupyterFrontEndPlugin < void > = {
769+ id : '@jupyter-notebook/notebook-extension:pager' ,
770+ description :
771+ 'A plugin to toggle the inspector when a pager payload is received.' ,
772+ autoStart : true ,
773+ requires : [ INotebookTracker , IInspector ] ,
774+ optional : [ ISettingRegistry , ITranslator ] ,
775+ activate : (
776+ app : JupyterFrontEnd ,
777+ notebookTracker : INotebookTracker ,
778+ inspector : IInspector ,
779+ settingRegistry : ISettingRegistry | null ,
780+ translator : ITranslator | null
781+ ) => {
782+ translator = translator ?? nullTranslator ;
783+
784+ let openHelpInDownArea = true ;
785+
786+ const kernelMessageHandlers : {
787+ [ sessionId : string ] : {
788+ kernel : Kernel . IKernelConnection ;
789+ handler : (
790+ sender : Kernel . IKernelConnection ,
791+ args : Kernel . IAnyMessageArgs
792+ ) => void ;
793+ } ;
794+ } = { } ;
795+
796+ if ( settingRegistry ) {
797+ const loadSettings = settingRegistry . load ( pager . id ) ;
798+ const updateSettings = ( settings : ISettingRegistry . ISettings ) : void => {
799+ openHelpInDownArea = settings . get ( 'openHelpInDownArea' )
800+ . composite as boolean ;
801+ } ;
802+
803+ Promise . all ( [ loadSettings , app . restored ] )
804+ . then ( ( [ settings ] ) => {
805+ updateSettings ( settings ) ;
806+ settings . changed . connect ( updateSettings ) ;
807+ } )
808+ . catch ( ( reason : Error ) => {
809+ console . error (
810+ `Failed to load settings for ${ pager . id } : ${ reason . message } `
811+ ) ;
812+ } ) ;
813+ }
814+
815+ const setupPagerListener = ( sessionContext : ISessionContext ) => {
816+ const sessionId = sessionContext . session ?. id ;
817+ if ( ! sessionId ) {
818+ return ;
819+ }
820+
821+ if ( kernelMessageHandlers [ sessionId ] ) {
822+ const { kernel, handler } = kernelMessageHandlers [ sessionId ] ;
823+ kernel . anyMessage . disconnect ( handler ) ;
824+ delete kernelMessageHandlers [ sessionId ] ;
825+ }
826+
827+ // Listen for kernel messages that may contain pager payloads
828+ const kernelMessageHandler = async (
829+ sender : Kernel . IKernelConnection ,
830+ args : Kernel . IAnyMessageArgs
831+ ) => {
832+ if ( ! openHelpInDownArea ) {
833+ // If false, do nothing - let the pager payload pass through
834+ // so it displays inline in the cell output area like in JupyterLab
835+ return ;
836+ }
837+ const { msg, direction } = args ;
838+
839+ // only check 'execute_reply' from the shell channel for pager data
840+ if (
841+ direction === 'recv' &&
842+ msg . channel === 'shell' &&
843+ msg . header . msg_type === 'execute_reply'
844+ ) {
845+ const content = msg . content as KernelMessage . IExecuteReply ;
846+ if (
847+ content . status === 'ok' &&
848+ content . payload &&
849+ content . payload . length > 0
850+ ) {
851+ const pagePayload = content . payload . find (
852+ ( item ) => item . source === 'page'
853+ ) ;
854+
855+ if ( pagePayload && pagePayload . data ) {
856+ const text = ( pagePayload . data as any ) [ 'text/plain' ] ;
857+
858+ // Remove the 'page' payload from the message to prevent it from also appearing in the cell's output area
859+ content . payload = content . payload . filter (
860+ ( item ) => item . source !== 'page'
861+ ) ;
862+ if ( content . payload . length === 0 ) {
863+ // If no other payloads remain, delete the payload array from the content.
864+ delete content . payload ;
865+ }
866+
867+ await app . commands . execute ( 'inspector:open' , {
868+ text,
869+ refresh : true ,
870+ } ) ;
871+ }
872+ }
873+ }
874+ } ;
875+
876+ // Connect to the kernel's anyMessage signal to catch
877+ // pager payloads before the output area by cleaning up and reconnecting
878+ if ( sessionContext . session ?. kernel ) {
879+ if ( kernelMessageHandlers [ sessionId ] ) {
880+ const { kernel, handler : oldHandler } =
881+ kernelMessageHandlers [ sessionId ] ;
882+ kernel . anyMessage . disconnect ( oldHandler ) ;
883+ }
884+
885+ sessionContext . session . kernel . anyMessage . connect ( kernelMessageHandler ) ;
886+ kernelMessageHandlers [ sessionId ] = {
887+ kernel : sessionContext . session . kernel ,
888+ handler : kernelMessageHandler ,
889+ } ;
890+ }
891+ } ;
892+
893+ let inspectionHandler : InspectionHandler ;
894+
895+ notebookTracker . widgetAdded . connect ( ( _sender , panel ) => {
896+ if ( panel . sessionContext ) {
897+ setupPagerListener ( panel . sessionContext ) ;
898+ }
899+
900+ panel . sessionContext . sessionChanged . connect ( ( ) => {
901+ if ( panel . sessionContext ) {
902+ setupPagerListener ( panel . sessionContext ) ;
903+ }
904+ } ) ;
905+
906+ panel . sessionContext . kernelChanged . connect ( ( ) => {
907+ if ( panel . sessionContext ) {
908+ setupPagerListener ( panel . sessionContext ) ;
909+ }
910+ } ) ;
911+
912+ const sessionContext = panel . sessionContext ;
913+ const rendermime = panel . content . rendermime ;
914+ const connector = new ( class extends KernelConnector {
915+ async fetch ( request : InspectionHandler . IRequest ) : Promise < any > {
916+ console . log ( 'Custom fetch called with:' , request ) ;
917+ }
918+ } ) ( { sessionContext } ) ;
919+
920+ // Define a custom inspection handler so we can persist the pager data even after
921+ // switching cells or moving the cursor position
922+ inspectionHandler = new ( class extends InspectionHandler {
923+ get inspected ( ) {
924+ return this . _notebookInspected ;
925+ }
926+
927+ onEditorChange ( text : string ) : void {
928+ if ( text && text . trim ( ) ) {
929+ this . _previousInspectData = text ;
930+ }
931+
932+ // Use the current text or fall back to previous data
933+ const dataToShow =
934+ text && text . trim ( ) ? text : this . _previousInspectData ;
935+
936+ const update : IInspector . IInspectorUpdate = { content : null } ;
937+
938+ if ( dataToShow ) {
939+ const data = {
940+ 'text/plain' : dataToShow ,
941+ } ;
942+
943+ const mimeType = rendermime . preferredMimeType ( data ) ;
944+ if ( mimeType ) {
945+ const widget = rendermime . createRenderer ( mimeType ) ;
946+ const model = new MimeModel ( { data } ) ;
947+ void widget . renderModel ( model ) ;
948+ update . content = widget ;
949+ }
950+ }
951+
952+ // Emit the inspection update signal
953+ this . _notebookInspected . emit ( update ) ;
954+ }
955+
956+ private _previousInspectData = '' ;
957+ private _notebookInspected : Signal <
958+ InspectionHandler ,
959+ IInspector . IInspectorUpdate
960+ > = new Signal < InspectionHandler , IInspector . IInspectorUpdate > ( this ) ;
961+ } ) ( { connector, rendermime } ) ;
962+
963+ // Listen for parent disposal.
964+ panel . disposed . connect ( ( ) => {
965+ inspectionHandler . dispose ( ) ;
966+ } ) ;
967+ } ) ;
968+
969+ // Handle current notebook if already open
970+ if ( notebookTracker . currentWidget ) {
971+ const panel = notebookTracker . currentWidget ;
972+ if ( panel . sessionContext ) {
973+ setupPagerListener ( panel . sessionContext ) ;
974+
975+ panel . sessionContext . sessionChanged . connect ( ( ) => {
976+ if ( panel . sessionContext ) {
977+ setupPagerListener ( panel . sessionContext ) ;
978+ }
979+ } ) ;
980+
981+ panel . sessionContext . kernelChanged . connect ( ( ) => {
982+ if ( panel . sessionContext ) {
983+ setupPagerListener ( panel . sessionContext ) ;
984+ }
985+ } ) ;
986+ }
987+ }
988+
989+ // Keep track of notebook instances and set inspector source.
990+ const setSource = ( _widget : Widget | null ) : void => {
991+ inspector . source = inspectionHandler ;
992+ } ;
993+ void app . restored . then ( ( ) => setSource ( app . shell . currentWidget ) ) ;
994+ } ,
995+ } ;
996+
752997/**
753998 * Export the plugins as default.
754999 */
@@ -761,6 +1006,7 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
7611006 kernelLogo ,
7621007 kernelStatus ,
7631008 notebookToolsWidget ,
1009+ pager ,
7641010 scrollOutput ,
7651011 tabIcon ,
7661012 trusted ,
0 commit comments