@@ -71,6 +71,8 @@ let persistViewTimer: ReturnType<typeof setTimeout> | null = null;
7171// Track whether tool input has been received (to know if we should restore persisted state)
7272let hasReceivedToolInput = false ;
7373
74+ let widgetUUID : string | undefined = undefined ;
75+
7476/**
7577 * Persisted camera state for localStorage
7678 */
@@ -83,18 +85,6 @@ interface PersistedCameraState {
8385 roll : number ; // radians
8486}
8587
86- /**
87- * Get localStorage key for persisting view state
88- * Uses toolInfo.id (tool invocation ID) - localStorage is scoped per conversation per server,
89- * so each tool call remembers its own view state within the conversation.
90- */
91- function getViewStorageKey ( ) : string | null {
92- const context = app . getHostContext ( ) ;
93- const toolId = context ?. toolInfo ?. id ;
94- if ( ! toolId ) return null ;
95- return `cesium-view:${ toolId } ` ;
96- }
97-
9888/**
9989 * Get current camera state for persistence
10090 */
@@ -132,8 +122,7 @@ function schedulePersistViewState(cesiumViewer: any): void {
132122 * Persist current view state to localStorage
133123 */
134124function persistViewState ( cesiumViewer : any ) : void {
135- const key = getViewStorageKey ( ) ;
136- if ( ! key ) {
125+ if ( ! widgetUUID ) {
137126 log . info ( "No storage key available, skipping view persistence" ) ;
138127 return ;
139128 }
@@ -142,13 +131,9 @@ function persistViewState(cesiumViewer: any): void {
142131 if ( ! state ) return ;
143132
144133 try {
145- localStorage . setItem ( key , JSON . stringify ( state ) ) ;
146- log . info (
147- "Persisted view state:" ,
148- key ,
149- state . latitude . toFixed ( 2 ) ,
150- state . longitude . toFixed ( 2 ) ,
151- ) ;
134+ const value = JSON . stringify ( state ) ;
135+ localStorage . setItem ( widgetUUID , value ) ;
136+ log . info ( "Persisted view state:" , widgetUUID , value ) ;
152137 } catch ( e ) {
153138 log . warn ( "Failed to persist view state:" , e ) ;
154139 }
@@ -158,12 +143,14 @@ function persistViewState(cesiumViewer: any): void {
158143 * Load persisted view state from localStorage
159144 */
160145function loadPersistedViewState ( ) : PersistedCameraState | null {
161- const key = getViewStorageKey ( ) ;
162- if ( ! key ) return null ;
146+ if ( ! widgetUUID ) return null ;
163147
164148 try {
165- const stored = localStorage . getItem ( key ) ;
166- if ( ! stored ) return null ;
149+ const stored = localStorage . getItem ( widgetUUID ) ;
150+ if ( ! stored ) {
151+ console . info ( "No persisted view state found" ) ;
152+ return null ;
153+ }
167154
168155 const state = JSON . parse ( stored ) as PersistedCameraState ;
169156 // Basic validation
@@ -175,6 +162,7 @@ function loadPersistedViewState(): PersistedCameraState | null {
175162 log . warn ( "Invalid persisted view state, ignoring" ) ;
176163 return null ;
177164 }
165+ log . info ( "Loaded persisted view state:" , state ) ;
178166 return state ;
179167 } catch ( e ) {
180168 log . warn ( "Failed to load persisted view state:" , e ) ;
@@ -402,8 +390,10 @@ async function getVisiblePlaces(extent: BoundingBox): Promise<string[]> {
402390}
403391
404392/**
405- * Debounced location update using multi-point reverse geocoding
406- * Samples multiple points in the visible extent to discover places
393+ * Debounced location update using multi-point reverse geocoding.
394+ * Samples multiple points in the visible extent to discover places.
395+ *
396+ * Updates model context with structured YAML frontmatter (similar to pdf-server).
407397 */
408398function scheduleLocationUpdate ( cesiumViewer : any ) : void {
409399 if ( reverseGeocodeTimer ) {
@@ -420,34 +410,35 @@ function scheduleLocationUpdate(cesiumViewer: any): void {
420410 }
421411
422412 const { widthKm, heightKm } = getScaleDimensions ( extent ) ;
423- const extentInfo =
424- `Extent: [${ extent . west . toFixed ( 4 ) } , ${ extent . south . toFixed ( 4 ) } , ` +
425- `${ extent . east . toFixed ( 4 ) } , ${ extent . north . toFixed ( 4 ) } ] ` +
426- `(${ widthKm . toFixed ( 1 ) } km × ${ heightKm . toFixed ( 1 ) } km)` ;
427- log . info ( extentInfo ) ;
413+
414+ log . info ( `Extent: ${ widthKm . toFixed ( 1 ) } km × ${ heightKm . toFixed ( 1 ) } km` ) ;
428415
429416 // Get places visible in the extent (samples multiple points for large areas)
430417 const places = await getVisiblePlaces ( extent ) ;
431- const placesText =
432- places . length > 0 ? `Visible places: ${ places . join ( ", " ) } ` : "" ;
433-
434- if ( places . length > 0 || center ) {
435- const centerText = center
436- ? `Center: ${ center . lat . toFixed ( 4 ) } , ${ center . lon . toFixed ( 4 ) } `
437- : "" ;
438-
439- const contextText = [ placesText , centerText , extentInfo ]
440- . filter ( Boolean )
441- . join ( "\n" ) ;
442418
443- log . info ( "Updating model context:" , contextText ) ;
444-
445- // Update the model's context with the current map location.
446- // If the host doesn't support this, the request will silently fail.
447- app . updateModelContext ( {
448- content : [ { type : "text" , text : contextText } ] ,
449- } ) ;
450- }
419+ // Build structured markdown with YAML frontmatter (like pdf-server)
420+ // Note: tool name isn't in the notification protocol, so we hardcode it
421+ const frontmatter = [
422+ "---" ,
423+ `tool: show-map` ,
424+ center
425+ ? `center: [${ center . lat . toFixed ( 4 ) } , ${ center . lon . toFixed ( 4 ) } ]`
426+ : null ,
427+ `extent: [${ extent . west . toFixed ( 4 ) } , ${ extent . south . toFixed ( 4 ) } , ${ extent . east . toFixed ( 4 ) } , ${ extent . north . toFixed ( 4 ) } ]` ,
428+ `extent-size: ${ widthKm . toFixed ( 1 ) } km × ${ heightKm . toFixed ( 1 ) } km` ,
429+ places . length > 0 ? `visible-places: [${ places . join ( ", " ) } ]` : null ,
430+ "---" ,
431+ ]
432+ . filter ( Boolean )
433+ . join ( "\n" ) ;
434+
435+ log . info ( "Updating model context:" , frontmatter ) ;
436+
437+ // Update the model's context with the current map location.
438+ // If the host doesn't support this, the request will silently fail.
439+ app . updateModelContext ( {
440+ content : [ { type : "text" , text : frontmatter } ] ,
441+ } ) ;
451442 } , 1500 ) ;
452443}
453444
@@ -947,40 +938,36 @@ app.ontoolinput = async (params) => {
947938// },
948939// );
949940
941+ // Handle tool result - extract widgetUUID and restore persisted view if available
942+ app . ontoolresult = async ( result ) => {
943+ widgetUUID = result . _meta ?. widgetUUID
944+ ? String ( result . _meta . widgetUUID )
945+ : undefined ;
946+ log . info ( "Tool result received, widgetUUID:" , widgetUUID ) ;
947+
948+ // Now that we have widgetUUID, try to restore persisted view
949+ // This overrides the tool input position if a saved state exists
950+ if ( viewer && widgetUUID ) {
951+ const restored = restorePersistedView ( viewer ) ;
952+ if ( restored ) {
953+ log . info ( "Restored persisted view from tool result handler" ) ;
954+ await waitForTilesLoaded ( viewer ) ;
955+ hideLoading ( ) ;
956+ }
957+ }
958+ } ;
959+
950960// Initialize Cesium and connect to host
951- async function init ( ) {
961+ async function initialize ( ) {
952962 try {
953963 log . info ( "Loading CesiumJS from CDN..." ) ;
954964 await loadCesium ( ) ;
955965 log . info ( "CesiumJS loaded successfully" ) ;
956966
957967 viewer = await initCesium ( ) ;
958- // Don't hide loading here - we wait for tool input to position camera
959- // and for tiles to load before hiding the loading indicator
960- log . info ( "CesiumJS initialized, waiting for tool input..." ) ;
968+ log . info ( "CesiumJS initialized" ) ;
961969
962- // Fallback: if no tool input received within 2 seconds, try restoring
963- // persisted view or show default view
964- setTimeout ( async ( ) => {
965- const loadingEl = document . getElementById ( "loading" ) ;
966- if (
967- loadingEl &&
968- loadingEl . style . display !== "none" &&
969- ! hasReceivedToolInput
970- ) {
971- // No explicit tool input - try to restore persisted view
972- const restored = restorePersistedView ( viewer ! ) ;
973- if ( restored ) {
974- log . info ( "Restored persisted view, waiting for tiles..." ) ;
975- } else {
976- log . info ( "No persisted view, using default view..." ) ;
977- }
978- await waitForTilesLoaded ( viewer ! ) ;
979- hideLoading ( ) ;
980- }
981- } , 2000 ) ;
982-
983- // Connect to host (auto-creates PostMessageTransport)
970+ // Connect to host (must happen before we can receive notifications)
984971 await app . connect ( ) ;
985972 log . info ( "Connected to host" ) ;
986973
@@ -1009,6 +996,26 @@ async function init() {
1009996
1010997 // Set up keyboard shortcuts for fullscreen (Escape to exit, Ctrl/Cmd+Enter to toggle)
1011998 document . addEventListener ( "keydown" , handleFullscreenKeyboard ) ;
999+
1000+ // Wait a bit for tool input, then try restoring persisted view or show default
1001+ setTimeout ( async ( ) => {
1002+ const loadingEl = document . getElementById ( "loading" ) ;
1003+ if (
1004+ loadingEl &&
1005+ loadingEl . style . display !== "none" &&
1006+ ! hasReceivedToolInput
1007+ ) {
1008+ // No explicit tool input - try to restore persisted view
1009+ const restored = restorePersistedView ( viewer ! ) ;
1010+ if ( restored ) {
1011+ log . info ( "Restored persisted view, waiting for tiles..." ) ;
1012+ } else {
1013+ log . info ( "No persisted view, using default view..." ) ;
1014+ }
1015+ await waitForTilesLoaded ( viewer ! ) ;
1016+ hideLoading ( ) ;
1017+ }
1018+ } , 500 ) ;
10121019 } catch ( error ) {
10131020 log . error ( "Failed to initialize:" , error ) ;
10141021 const loadingEl = document . getElementById ( "loading" ) ;
@@ -1019,4 +1026,5 @@ async function init() {
10191026 }
10201027}
10211028
1022- init ( ) ;
1029+ // Start initialization
1030+ initialize ( ) ;
0 commit comments