Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
- Added minimum tested Claude Code CLI version (2.1.78) to installation requirements (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/29)
- Fixed anchor link clicks (`#section`) in markdown preview not enabling the Back button; Back now returns to the position before the anchor jump
- Fixed markdown preview not rendering images (`![alt](path)` syntax)
- Fixed markdown preview not rendering images with relative paths (`![alt](path)` syntax)
- Fixed session tab blocking resize below ~600px when docked as a side panel (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/19)
- Added alphabetical sorting to the "Select Project" dropdown (https://github.com/nbclaudecodegui/NetbeansClaudeCodeGui/issues/21)
- Improved error panel on process start failure: shows Working Directory and CLAUDE_CONFIG_DIR fields (before Command and Error) for easier diagnosis; all fields have a right-click Copy menu; fixed "Restart Advanced…" not dismissing the error panel
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.21.53-SNAPSHOT</version>
<version>0.21.55-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
13 changes: 10 additions & 3 deletions src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public class MarkdownPreviewTab extends TopComponent {
/** Absolute path key used to identify this tab in {@link #OPEN_TABS}. */
String filePath;

/** Parent directory of {@link #filePath}; used to resolve relative image paths. */
String baseDirPath;

// Navigation history
final List<String> historyPaths = new ArrayList<>();
final List<FileObject> historyFos = new ArrayList<>();
Expand Down Expand Up @@ -133,9 +136,10 @@ public static void openLive(String filePath, String content, FileObject fo) {
MarkdownPreviewTab tab = new MarkdownPreviewTab();
tab.filePath = filePath;
tab.fileObject = fo;
tab.baseDirPath = new java.io.File(filePath).getParent();

String html = MarkdownRenderer.toHtml(content != null ? content : "");
tab.pane = MarkdownRenderer.createOutputPane(html, new java.io.File(filePath).getParent());
tab.pane = MarkdownRenderer.createOutputPane(html, tab.baseDirPath);

String name = new java.io.File(filePath).getName();
tab.setDisplayName("Preview: " + name);
Expand Down Expand Up @@ -171,7 +175,8 @@ public static void openLive(String filePath, String content, FileObject fo) {
* @param markdown markdown text to render
*/
public void updateContent(String markdown) {
String html = MarkdownRenderer.toHtml(markdown != null ? markdown : "");
String html = MarkdownRenderer.resolveImagePaths(
MarkdownRenderer.toHtml(markdown != null ? markdown : ""), baseDirPath);
SwingUtilities.invokeLater(() -> {
if (pane == null) return;
if (scrollPane == null) {
Expand Down Expand Up @@ -298,6 +303,7 @@ void navigateTo(String newPath, FileObject newFo, boolean addToHistory) {
}
this.filePath = newPath;
this.fileObject = newFo;
this.baseDirPath = new File(newPath).getParent();
OPEN_TABS.put(newPath, this);

setDisplayName("Preview: " + new File(newPath).getName());
Expand Down Expand Up @@ -583,8 +589,9 @@ protected void componentOpened() {
FileObject fo = FileUtil.toFileObject(f);
if (pane == null) {
try {
baseDirPath = f.getParent();
String content = fo != null ? fo.asText() : "";
pane = MarkdownRenderer.createOutputPane(MarkdownRenderer.toHtml(content));
pane = MarkdownRenderer.createOutputPane(MarkdownRenderer.toHtml(content), baseDirPath);
} catch (IOException e) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,28 @@ public static JEditorPane createOutputPane(String html) {
*/
public static JEditorPane createOutputPane(String html, String baseDirPath) {
JEditorPane pane = createOutputPane();
if (baseDirPath != null) {
try {
((HTMLDocument) pane.getDocument()).setBase(
new java.net.URL("file://" + baseDirPath + "/"));
} catch (java.net.MalformedURLException ignored) {
// fall through — relative images won't resolve but nothing breaks
}
}
pane.setText(html);
pane.setText(resolveImagePaths(html, baseDirPath));
return pane;
}

/**
* Replaces relative {@code src} attributes in {@code <img>} tags with absolute
* {@code file://} URIs so Swing's HTMLEditorKit can load them regardless of the
* document base URL setting.
*
* @param html HTML fragment (may contain {@code <img src="...">} tags)
* @param baseDirPath absolute directory path of the file being rendered, or null
* @return the HTML with relative image paths resolved, or the original HTML if
* {@code baseDirPath} is null
*/
public static String resolveImagePaths(String html, String baseDirPath) {
if (baseDirPath == null) return html;
String base = baseDirPath.replace("\\", "/");
return html.replaceAll(
"src=\"(?!https?://|file://|data:)([^\"]+)\"",
"src=\"file://" + base + "/$1\"");
}

/**
* Creates and configures a {@link JEditorPane} for rendering chat output with no initial content.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,50 @@ void testHeadingSlugStripNonAlphanumeric() {
String html = MarkdownRenderer.toHtml("# Hello, World! (2024)");
assertTrue(html.contains("<a name=\"hello-world-2024\">"), "slug strips punctuation");
}

// -------------------------------------------------------------------------
// resolveImagePaths()
// -------------------------------------------------------------------------

@Test
void testResolveImagePathsRelative() {
String html = "<img src=\"screenshots/overview.png\" alt=\"\">";
String result = MarkdownRenderer.resolveImagePaths(html, "/docs");
assertEquals("<img src=\"file:///docs/screenshots/overview.png\" alt=\"\">", result);
}

@Test
void testResolveImagePathsAbsoluteHttpUnchanged() {
String html = "<img src=\"https://example.com/img.png\" alt=\"\">";
String result = MarkdownRenderer.resolveImagePaths(html, "/docs");
assertEquals(html, result);
}

@Test
void testResolveImagePathsFileUriUnchanged() {
String html = "<img src=\"file:///abs/path/img.png\" alt=\"\">";
String result = MarkdownRenderer.resolveImagePaths(html, "/docs");
assertEquals(html, result);
}

@Test
void testResolveImagePathsDataUriUnchanged() {
String html = "<img src=\"data:image/png;base64,abc\" alt=\"\">";
String result = MarkdownRenderer.resolveImagePaths(html, "/docs");
assertEquals(html, result);
}

@Test
void testResolveImagePathsNullBaseDirReturnsUnchanged() {
String html = "<img src=\"img.png\" alt=\"\">";
String result = MarkdownRenderer.resolveImagePaths(html, null);
assertEquals(html, result);
}

@Test
void testResolveImagePathsWindowsBackslashesNormalized() {
String html = "<img src=\"img.png\" alt=\"\">";
String result = MarkdownRenderer.resolveImagePaths(html, "C:\\docs\\project");
assertEquals("<img src=\"file://C:/docs/project/img.png\" alt=\"\">", result);
}
}
Loading