diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2709898..ace28740 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Fixed markdown preview not rendering nested list structure (multi-level indented lists now display with proper nesting)
- Fixed "Continue last" and "Resume specific" staying enabled (and selected) when a directory or profile with no sessions is chosen in the session selector
- Fixed model list showing garbled names (e.g. "Mtoket 4.6 with 1M context") when the /model menu includes 1M-context variants and cost annotations
diff --git a/pom.xml b/pom.xml
index 3ce8db4b..3bec908f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbclaudecodegui
netbeans-claude-code-gui
- 0.22.48-SNAPSHOT
+ 0.22.49-SNAPSHOT
nbm
Netbeans Plugin Claude Code GUI
NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java b/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java
index bb4d829e..ecfc8957 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/common/MarkdownRenderer.java
@@ -299,31 +299,9 @@ && isSeparatorRow(lines[i + 1])) {
continue;
}
- // --- Unordered list ---
- if (isUnordered(line)) {
- html.append("
");
- while (i < lines.length && isUnordered(lines[i])) {
- html.append("- ")
- .append(inlineToHtml(lines[i]
- .replaceFirst("^\\s*[-*]\\s+", "")))
- .append("
");
- i++;
- }
- html.append("
");
- continue;
- }
-
- // --- Ordered list ---
- if (isOrdered(line)) {
- html.append("");
- while (i < lines.length && isOrdered(lines[i])) {
- html.append("- ")
- .append(inlineToHtml(lines[i]
- .replaceFirst("^\\s*\\d+\\.\\s+", "")))
- .append("
");
- i++;
- }
- html.append("
");
+ // --- List (nested ordered/unordered) ---
+ if (isUnordered(line) || isOrdered(line)) {
+ i = renderNestedList(lines, i, html);
continue;
}
@@ -531,5 +509,69 @@ private static void appendHtml(JEditorPane pane, String html) {
}
}
+ private static int renderNestedList(String[] lines, int start, StringBuilder html) {
+ List indentStack = new ArrayList<>();
+ List typeStack = new ArrayList<>();
+
+ int i = start;
+ while (i < lines.length) {
+ String line = lines[i];
+
+ if (line.isBlank()) {
+ int j = i + 1;
+ while (j < lines.length && lines[j].isBlank()) j++;
+ if (j < lines.length && (isOrdered(lines[j]) || isUnordered(lines[j]))) {
+ i = j;
+ continue;
+ }
+ break;
+ }
+
+ if (!isOrdered(line) && !isUnordered(line)) break;
+
+ int indent = leadingSpaces(line);
+ boolean ordered = isOrdered(line);
+
+ if (indentStack.isEmpty()) {
+ html.append(ordered ? "" : "");
+ indentStack.add(indent);
+ typeStack.add(ordered);
+ } else {
+ int topIndent = indentStack.get(indentStack.size() - 1);
+ if (indent > topIndent) {
+ html.append(ordered ? "" : "");
+ indentStack.add(indent);
+ typeStack.add(ordered);
+ } else if (indent < topIndent) {
+ while (indentStack.size() > 1 && indentStack.get(indentStack.size() - 1) > indent) {
+ html.append(typeStack.get(typeStack.size() - 1) ? "
" : "
");
+ indentStack.remove(indentStack.size() - 1);
+ typeStack.remove(typeStack.size() - 1);
+ }
+ }
+ if (!typeStack.isEmpty() && typeStack.get(typeStack.size() - 1) != ordered) {
+ html.append(typeStack.get(typeStack.size() - 1) ? "
" : "");
+ typeStack.set(typeStack.size() - 1, ordered);
+ html.append(ordered ? "" : "");
+ }
+ }
+
+ String content = line.replaceFirst("^\\s*(?:\\d+\\.|[-*])\\s+", "");
+ html.append("- ").append(inlineToHtml(content)).append("
");
+ i++;
+ }
+
+ for (int k = typeStack.size() - 1; k >= 0; k--) {
+ html.append(typeStack.get(k) ? "
" : "");
+ }
+ return i;
+ }
+
+ private static int leadingSpaces(String line) {
+ int n = 0;
+ while (n < line.length() && line.charAt(n) == ' ') n++;
+ return n;
+ }
+
private MarkdownRenderer() {}
}
diff --git a/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java b/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java
index 443ad51c..0ea16901 100644
--- a/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java
+++ b/src/test/java/io/github/nbclaudecodegui/ui/common/MarkdownRendererTest.java
@@ -266,6 +266,49 @@ void testHeadingSlugStripNonAlphanumeric() {
assertTrue(html.contains(""), "slug strips punctuation");
}
+ // -------------------------------------------------------------------------
+ // toHtml() — nested lists
+ // -------------------------------------------------------------------------
+
+ @Test
+ void testNestedOrderedList() {
+ String md = "1. first\n 1. sub-first\n 2. sub-second\n2. second";
+ String html = MarkdownRenderer.toHtml(md);
+ // outer ol must contain an inner ol
+ int outerOl = html.indexOf("");
+ int innerOl = html.indexOf("", outerOl + 1);
+ assertTrue(innerOl > outerOl, "inner must exist inside outer ");
+ assertTrue(html.contains("- first
") || html.contains("- first"),
+ "outer first item");
+ assertTrue(html.contains("sub-first"), "sub-first item");
+ assertTrue(html.contains("sub-second"), "sub-second item");
+ assertTrue(html.contains("second"), "outer second item");
+ // must have at least 2
closing tags
+ long closeCount = html.chars().filter(c -> c == '<').count();
+ assertTrue(html.indexOf("
", html.indexOf("
") + 1) > 0,
+ "at least 2
tags expected");
+ }
+
+ @Test
+ void testNestedUnorderedList() {
+ String md = "- alpha\n - sub-alpha\n - sub-beta\n- beta";
+ String html = MarkdownRenderer.toHtml(md);
+ int outerUl = html.indexOf("");
+ int innerUl = html.indexOf("", outerUl + 1);
+ assertTrue(innerUl > outerUl, "inner must exist inside outer ");
+ assertTrue(html.contains("alpha"), "outer alpha");
+ assertTrue(html.contains("sub-alpha"), "sub-alpha");
+ }
+
+ @Test
+ void testMixedNestedList() {
+ String md = "1. item\n - nested-ul\n2. item2";
+ String html = MarkdownRenderer.toHtml(md);
+ assertTrue(html.contains(""), "outer ol");
+ assertTrue(html.contains(""), "inner ul");
+ assertTrue(html.contains("nested-ul"), "nested item text");
+ }
+
// -------------------------------------------------------------------------
// resolveImagePaths()
// -------------------------------------------------------------------------