Skip to content

Commit cd76daf

Browse files
stephanjclaude
andcommitted
fix: add null-safety to welcome template rendering with fallback to local content
The persistent welcome content cache could contain stale JSON with wrong field names, causing silent NPE on pooled thread and empty welcome panel. Now skips announcements with null/empty messages, falls back to ResourceBundle for null remote fields, and clears stale cache on rendering failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cf45de5 commit cd76daf

File tree

4 files changed

+117
-14
lines changed

4 files changed

+117
-14
lines changed

docusaurus/static/api/welcome.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@
1818
{ "emoji": "\uD83D\uDC40", "name": "Chat History", "description": "All chats are saved and can be restored or removed" },
1919
{ "emoji": "\uD83E\uDDE0", "name": "Project Scanner", "description": "Add source code (full project or by package) to prompt context (or clipboard) when using Anthropic, OpenAI or Gemini." }
2020
],
21-
"announcements": [
22-
{ "type": "success", "message": "\uD83C\uDF89 New in v0.9.4: Agent Mode with parallel sub-agents! Enable in Agent Settings to let the LLM autonomously read, write, and edit files in your project." }
23-
],
21+
"announcements": [],
2422
"tip": "You can modify the endpoints for different LLM providers and the default command skills in the plugin settings.",
2523
"enjoy": "Enjoy!",
2624
"socialLinks": [

src/main/java/com/devoxx/genie/ui/webview/handler/WebViewMessageRenderer.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.devoxx.genie.model.request.ChatMessageContext;
44
import com.devoxx.genie.model.welcome.WelcomeContent;
55
import com.devoxx.genie.service.welcome.WelcomeContentService;
6+
import com.devoxx.genie.ui.settings.DevoxxGenieStateService;
67
import com.devoxx.genie.ui.webview.WebServer;
78
import com.devoxx.genie.ui.webview.template.ChatMessageTemplate;
89
import com.devoxx.genie.ui.webview.template.WelcomeTemplate;
@@ -67,13 +68,23 @@ private void showWelcomeContent(ResourceBundle resourceBundle) {
6768
return;
6869
}
6970

70-
// Use the WelcomeTemplate to generate HTML, with remote content if available
71-
WelcomeContent remoteContent = WelcomeContentService.getInstance().getWelcomeContent();
72-
WelcomeTemplate welcomeTemplate = new WelcomeTemplate(webServer, resourceBundle, remoteContent);
73-
String welcomeContent = welcomeTemplate.generate();
71+
String welcomeContent;
72+
try {
73+
// Use the WelcomeTemplate to generate HTML, with remote content if available
74+
WelcomeContent remoteContent = WelcomeContentService.getInstance().getWelcomeContent();
75+
WelcomeTemplate welcomeTemplate = new WelcomeTemplate(webServer, resourceBundle, remoteContent);
76+
welcomeContent = welcomeTemplate.generate();
77+
} catch (Exception e) {
78+
log.warn("Failed to generate welcome content from remote, falling back to local", e);
79+
// Clear stale cached content that may have caused the error
80+
DevoxxGenieStateService.getInstance().setWelcomeContentCachedJson("");
81+
WelcomeTemplate welcomeTemplate = new WelcomeTemplate(webServer, resourceBundle);
82+
welcomeContent = welcomeTemplate.generate();
83+
}
7484

7585
// Only inject welcome content if no chat messages are already present (defense-in-depth).
76-
// Content is from trusted internal WelcomeTemplate, not user input.
86+
// Content is from trusted internal WelcomeTemplate, not user input — all values are
87+
// from the plugin's own ResourceBundle or the remote welcome.json with escapeHtml applied.
7788
jsExecutor.executeJavaScript(
7889
"(function() {" +
7990
" var container = document.getElementById('conversation-container');" +

src/main/java/com/devoxx/genie/ui/webview/template/WelcomeTemplate.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ public WelcomeTemplate(WebServer webServer, ResourceBundle resourceBundle, @Null
5050

5151
@NotNull
5252
private String generateFromRemoteContent(@NotNull String htmlTemplate, @NotNull String customPromptCommands) {
53-
String title = escapeHtml(remoteContent.getTitle());
54-
String description = escapeHtml(remoteContent.getDescription());
55-
String instructions = escapeHtml(remoteContent.getInstructions());
56-
String tip = escapeHtml(remoteContent.getTip());
57-
String enjoy = escapeHtml(remoteContent.getEnjoy());
53+
String title = escapeHtml(nullSafe(remoteContent.getTitle(), resourceBundle.getString("welcome.title")));
54+
String description = escapeHtml(nullSafe(remoteContent.getDescription(), resourceBundle.getString("welcome.description")));
55+
String instructions = escapeHtml(nullSafe(remoteContent.getInstructions(), resourceBundle.getString("welcome.instructions")));
56+
String tip = escapeHtml(nullSafe(remoteContent.getTip(), resourceBundle.getString("welcome.tip")));
57+
String enjoy = escapeHtml(nullSafe(remoteContent.getEnjoy(), resourceBundle.getString("welcome.enjoy")));
5858
String featuresHtml = buildFeaturesHtml(remoteContent.getFeatures());
5959
String announcementsHtml = buildAnnouncementsHtml(remoteContent.getAnnouncements());
6060
String socialLinksHtml = buildSocialLinksHtml(remoteContent.getSocialLinks());
@@ -122,11 +122,15 @@ private String buildAnnouncementsHtml(@Nullable List<WelcomeAnnouncement> announ
122122
}
123123
StringBuilder sb = new StringBuilder();
124124
for (WelcomeAnnouncement announcement : announcements) {
125+
String message = announcement.getMessage();
126+
if (message == null || message.isEmpty()) {
127+
continue;
128+
}
125129
String type = announcement.getType() != null ? announcement.getType() : "info";
126130
sb.append("<div class=\"announcement announcement-")
127131
.append(escapeHtml(type))
128132
.append("\">")
129-
.append(escapeHtml(announcement.getMessage()))
133+
.append(escapeHtml(message))
130134
.append("</div>\n");
131135
}
132136
return sb.toString();
@@ -205,4 +209,9 @@ private String getDefaultFeaturesHtml() {
205209
private String getDefaultSocialLinksHtml() {
206210
return "<p>Follow us on Bluesky : <a href=\"https://bsky.app/profile/devoxxgenie.bsky.social\">@DevoxxGenie.bsky.social</a></p>";
207211
}
212+
213+
@NotNull
214+
private static String nullSafe(@Nullable String value, @NotNull String fallback) {
215+
return value != null && !value.isEmpty() ? value : fallback;
216+
}
208217
}

src/test/java/com/devoxx/genie/ui/webview/template/WelcomeTemplateTest.java

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,91 @@ void generate_withNoConstructorArgs_usesLocalFallback() {
205205
assertTrue(html.contains("Welcome to DevoxxGenie"));
206206
}
207207

208+
@Test
209+
void generate_withNullAnnouncementMessage_skipsAnnouncement() {
210+
WelcomeContent content = createRemoteContent();
211+
WelcomeAnnouncement announcement = new WelcomeAnnouncement();
212+
announcement.setType("info");
213+
announcement.setMessage(null); // null message — should be skipped
214+
content.setAnnouncements(List.of(announcement));
215+
216+
WelcomeTemplate template = new WelcomeTemplate(webServer, resourceBundle, content);
217+
218+
String html = template.generate();
219+
220+
assertNotNull(html);
221+
assertFalse(html.contains("announcement-info"), "Announcement with null message should be skipped");
222+
assertFalse(html.contains("${"), "No unresolved placeholders should remain");
223+
}
224+
225+
@Test
226+
void generate_withEmptyAnnouncementMessage_skipsAnnouncement() {
227+
WelcomeContent content = createRemoteContent();
228+
WelcomeAnnouncement announcement = new WelcomeAnnouncement();
229+
announcement.setType("warning");
230+
announcement.setMessage(""); // empty message — should be skipped
231+
content.setAnnouncements(List.of(announcement));
232+
233+
WelcomeTemplate template = new WelcomeTemplate(webServer, resourceBundle, content);
234+
235+
String html = template.generate();
236+
237+
assertNotNull(html);
238+
assertFalse(html.contains("announcement-warning"), "Announcement with empty message should be skipped");
239+
}
240+
241+
@Test
242+
void generate_withNullTitle_fallsBackToResourceBundle() {
243+
WelcomeContent content = createRemoteContent();
244+
content.setTitle(null); // null field — should fall back to resource bundle
245+
246+
WelcomeTemplate template = new WelcomeTemplate(webServer, resourceBundle, content);
247+
248+
String html = template.generate();
249+
250+
assertNotNull(html);
251+
assertTrue(html.contains("Welcome to DevoxxGenie"), "Null remote title should fall back to resource bundle");
252+
assertFalse(html.contains("${"), "No unresolved placeholders should remain");
253+
}
254+
255+
@Test
256+
void generate_withAllNullRemoteFields_fallsBackToResourceBundle() {
257+
WelcomeContent content = new WelcomeContent();
258+
content.setSchemaVersion(1);
259+
// All text fields are null
260+
261+
WelcomeTemplate template = new WelcomeTemplate(webServer, resourceBundle, content);
262+
263+
String html = template.generate();
264+
265+
assertNotNull(html);
266+
assertTrue(html.contains("Welcome to DevoxxGenie"));
267+
assertTrue(html.contains("Test description"));
268+
assertTrue(html.contains("Test instructions"));
269+
assertFalse(html.contains("${"), "No unresolved placeholders should remain");
270+
}
271+
272+
@Test
273+
void generate_withMixedNullAndValidAnnouncements_rendersOnlyValid() {
274+
WelcomeContent content = createRemoteContent();
275+
WelcomeAnnouncement nullMsg = new WelcomeAnnouncement();
276+
nullMsg.setType("info");
277+
nullMsg.setMessage(null);
278+
WelcomeAnnouncement validMsg = new WelcomeAnnouncement();
279+
validMsg.setType("success");
280+
validMsg.setMessage("This one is valid!");
281+
content.setAnnouncements(List.of(nullMsg, validMsg));
282+
283+
WelcomeTemplate template = new WelcomeTemplate(webServer, resourceBundle, content);
284+
285+
String html = template.generate();
286+
287+
assertNotNull(html);
288+
assertFalse(html.contains("announcement-info"), "Null message announcement should be skipped");
289+
assertTrue(html.contains("announcement-success"), "Valid announcement should be rendered");
290+
assertTrue(html.contains("This one is valid!"));
291+
}
292+
208293
private WelcomeContent createRemoteContent() {
209294
WelcomeContent content = new WelcomeContent();
210295
content.setSchemaVersion(1);

0 commit comments

Comments
 (0)