From 2083115bb1a64de13f9d943b8ee833bb4e4c001b Mon Sep 17 00:00:00 2001 From: "ciefa.eth" <156933735+ciefa@users.noreply.github.com> Date: Sun, 4 Jan 2026 12:20:51 +0100 Subject: [PATCH 1/7] Add terminal-style GUI with real-time message streaming Implemented a vanilla Minecraft GUI-based terminal interface for OpenCode with autumn color theme and real-time SSE streaming support. Features: - Terminal-style GUI with ASCII borders and autumn color scheme - Load and display full session message history - Real-time streaming of AI responses via SSE events - Scrollable message history with mouse wheel and arrow keys - Word wrapping for long messages - Auto-scroll to bottom for new messages - Session validation before sending messages Changes: - Add OpenCodeGuiScreen: Terminal GUI implementation - Add /oc gui command to open terminal interface - Add getSessionMessages() to fetch message history from server - Add GUI message listeners for real-time delta streaming - Add GUI response complete listener for proper message spacing - Remove AI text output from game chat (keep status messages only) - Update .gitignore to exclude libs/ and *.log files Technical details: - Uses vanilla Minecraft Screen, GuiGraphics, and EditBox APIs - Integrates with existing SSE event system for live updates - Autumn theme colors from i3 config (orange borders, cream text, dark brown background) - Message history loaded from /session/{id}/message endpoint - Listener pattern for GUI updates from background SSE thread --- .gitignore | 8 +- .../minecraft/client/OpenCodeClient.java | 46 +- .../client/http/OpenCodeHttpClient.java | 31 +- .../minecraft/command/OpenCodeCommand.java | 17 + .../minecraft/gui/OpenCodeGuiScreen.java | 451 ++++++++++++++++++ 5 files changed, 545 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java diff --git a/.gitignore b/.gitignore index f80fa88..3bbddbf 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,10 @@ repo .claude/ # Fabric version backup -FabricVersion/ \ No newline at end of file +FabricVersion/ + +# External libs +libs/ + +# Logs +*.log \ No newline at end of file diff --git a/src/main/java/com/opencode/minecraft/client/OpenCodeClient.java b/src/main/java/com/opencode/minecraft/client/OpenCodeClient.java index 4262304..a05b757 100644 --- a/src/main/java/com/opencode/minecraft/client/OpenCodeClient.java +++ b/src/main/java/com/opencode/minecraft/client/OpenCodeClient.java @@ -30,6 +30,8 @@ public class OpenCodeClient { private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private volatile boolean initialized = false; + private volatile java.util.function.Consumer guiMessageListener = null; + private volatile Runnable guiResponseCompleteListener = null; public OpenCodeClient(ModConfig config, PauseController pauseController) { this.config = config; @@ -99,6 +101,10 @@ private void handleEvent(SseEvent event) { if ("idle".equals(statusType)) { sessionManager.onSessionIdle(); messageRenderer.sendSystemMessage("Ready for input"); + // Notify GUI that response is complete + if (guiResponseCompleteListener != null) { + guiResponseCompleteListener.run(); + } } else if ("busy".equals(statusType)) { sessionManager.onSessionBusy(); messageRenderer.sendSystemMessage("Processing..."); @@ -108,7 +114,8 @@ private void handleEvent(SseEvent event) { handlePartUpdated(event); } case "message.created" -> { - messageRenderer.startNewMessage(); + // Don't clutter chat with message creation events + // messageRenderer.startNewMessage(); } case "session.error" -> { messageRenderer.sendErrorMessage("Session error occurred"); @@ -138,7 +145,13 @@ private void handlePartUpdated(SseEvent event) { pauseController.onDeltaReceived(); String delta = event.getDelta(); if (delta != null && !delta.isEmpty()) { - messageRenderer.appendDelta(delta); + // Don't send AI text to chat - only show in GUI + // messageRenderer.appendDelta(delta); + + // Notify GUI listener if present + if (guiMessageListener != null) { + guiMessageListener.accept(delta); + } } } } @@ -267,6 +280,35 @@ public SessionStatus getStatus() { return sessionManager.getStatus(); } + /** + * Gets the message history for a session + */ + public CompletableFuture getSessionMessages(String sessionId) { + return httpClient.getSessionMessages(sessionId); + } + + /** + * Sets a listener for real-time message updates (for GUI) + */ + public void setGuiMessageListener(java.util.function.Consumer listener) { + this.guiMessageListener = listener; + } + + /** + * Sets a listener for when a response completes (for GUI) + */ + public void setGuiResponseCompleteListener(Runnable listener) { + this.guiResponseCompleteListener = listener; + } + + /** + * Removes the GUI message listeners + */ + public void clearGuiMessageListener() { + this.guiMessageListener = null; + this.guiResponseCompleteListener = null; + } + /** * Returns true if connected and initialized */ diff --git a/src/main/java/com/opencode/minecraft/client/http/OpenCodeHttpClient.java b/src/main/java/com/opencode/minecraft/client/http/OpenCodeHttpClient.java index ace63fb..aaefeac 100644 --- a/src/main/java/com/opencode/minecraft/client/http/OpenCodeHttpClient.java +++ b/src/main/java/com/opencode/minecraft/client/http/OpenCodeHttpClient.java @@ -75,10 +75,11 @@ public CompletableFuture checkHealth() { public CompletableFuture createSession() { JsonObject body = new JsonObject(); + // Don't send x-opencode-directory header - let OpenCode use its default directory + // (The mod's config directory contains a config file that OpenCode doesn't understand) HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/session")) .header("Content-Type", "application/json") - .header("x-opencode-directory", directory) .timeout(Duration.ofSeconds(10)) .POST(HttpRequest.BodyPublishers.ofString(body.toString())) .build(); @@ -99,7 +100,6 @@ public CompletableFuture createSession() { public CompletableFuture> listSessions() { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/session")) - .header("x-opencode-directory", directory) .timeout(Duration.ofSeconds(10)) .GET() .build(); @@ -124,7 +124,6 @@ public CompletableFuture> listSessions() { public CompletableFuture getSession(String sessionId) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/session/" + sessionId)) - .header("x-opencode-directory", directory) .timeout(Duration.ofSeconds(10)) .GET() .build(); @@ -188,7 +187,6 @@ public CompletableFuture sendPrompt(String sessionId, String text) { public CompletableFuture abortSession(String sessionId) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/session/" + sessionId + "/abort")) - .header("x-opencode-directory", directory) .timeout(Duration.ofSeconds(10)) .POST(HttpRequest.BodyPublishers.noBody()) .build(); @@ -201,6 +199,30 @@ public CompletableFuture abortSession(String sessionId) { }); } + /** + * Gets the message history for a session + */ + public CompletableFuture getSessionMessages(String sessionId) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/session/" + sessionId + "/message")) + .timeout(Duration.ofSeconds(10)) + .GET() + .build(); + + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> { + if (response.statusCode() != 200) { + OpenCodeMod.LOGGER.warn("Failed to get messages: {}", response.statusCode()); + return new JsonArray(); + } + return JsonParser.parseString(response.body()).getAsJsonArray(); + }) + .exceptionally(e -> { + OpenCodeMod.LOGGER.warn("Failed to get messages: {}", e.getMessage()); + return new JsonArray(); + }); + } + /** * Subscribes to the global event stream (SSE) */ @@ -222,7 +244,6 @@ private void runSseLoop() { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/global/event")) .header("Accept", "text/event-stream") - .header("x-opencode-directory", directory) .GET() .build(); diff --git a/src/main/java/com/opencode/minecraft/command/OpenCodeCommand.java b/src/main/java/com/opencode/minecraft/command/OpenCodeCommand.java index d5945bf..0e6d070 100644 --- a/src/main/java/com/opencode/minecraft/command/OpenCodeCommand.java +++ b/src/main/java/com/opencode/minecraft/command/OpenCodeCommand.java @@ -6,7 +6,9 @@ import com.opencode.minecraft.OpenCodeMod; import com.opencode.minecraft.client.OpenCodeClient; import com.opencode.minecraft.client.session.SessionInfo; +import com.opencode.minecraft.gui.OpenCodeGuiScreen; import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.network.chat.Component; @@ -20,6 +22,7 @@ * Commands: * - /oc - Show help * - /oc help - Show help + * - /oc gui - Open ModernUI GUI interface * - /oc - Send a prompt to OpenCode * - /oc status - Show connection and session status * - /oc session new - Create a new session @@ -42,6 +45,10 @@ public static void register(CommandDispatcher dispatcher) { .then(Commands.literal("help") .executes(OpenCodeCommand::executeHelp)) + // /oc gui + .then(Commands.literal("gui") + .executes(OpenCodeCommand::executeGui)) + // /oc status .then(Commands.literal("status") .executes(OpenCodeCommand::executeStatus)) @@ -93,6 +100,8 @@ private static int executeHelp(CommandContext context) { source.sendSystemMessage(Component.literal("=== OpenCode Commands ===").withStyle(ChatFormatting.AQUA, ChatFormatting.BOLD)); source.sendSystemMessage(Component.literal("/oc ").withStyle(ChatFormatting.GREEN) .append(Component.literal(" - Send a prompt").withStyle(ChatFormatting.GRAY))); + source.sendSystemMessage(Component.literal("/oc gui").withStyle(ChatFormatting.GREEN) + .append(Component.literal(" - Open GUI interface").withStyle(ChatFormatting.GRAY))); source.sendSystemMessage(Component.literal("/oc status").withStyle(ChatFormatting.GREEN) .append(Component.literal(" - Show status").withStyle(ChatFormatting.GRAY))); source.sendSystemMessage(Component.literal("/oc session new").withStyle(ChatFormatting.GREEN) @@ -111,6 +120,14 @@ private static int executeHelp(CommandContext context) { return 1; } + private static int executeGui(CommandContext context) { + // Open terminal GUI on client thread + Minecraft.getInstance().execute(() -> { + Minecraft.getInstance().setScreen(new OpenCodeGuiScreen()); + }); + return 1; + } + private static int executeStatus(CommandContext context) { CommandSourceStack source = context.getSource(); OpenCodeClient client = OpenCodeMod.getClient(); diff --git a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java new file mode 100644 index 0000000..8897d97 --- /dev/null +++ b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java @@ -0,0 +1,451 @@ +package com.opencode.minecraft.gui; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.opencode.minecraft.OpenCodeMod; +import com.opencode.minecraft.client.session.SessionInfo; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.List; + +/** + * Terminal-style GUI screen for OpenCode chat interface + * Inspired by classic command-line terminals + */ +public class OpenCodeGuiScreen extends Screen { + + // Autumn theme colors + private static final int BACKGROUND_COLOR = 0xE01a1614; // Dark brown (semi-transparent) + private static final int BORDER_COLOR = 0xFFe67700; // Orange border + private static final int TEXT_COLOR = 0xFFf4e8d0; // Cream text + private static final int INPUT_COLOR = 0xFFfaf0e6; // Bright cream input + private static final int PROMPT_COLOR = 0xFFe67700; // Orange prompt + + private EditBox inputField; + private List messageHistory; + private int scrollOffset = 0; + private static final int MAX_VISIBLE_LINES = 20; + private StringBuilder currentAssistantMessage = new StringBuilder(); + private boolean receivingResponse = false; + + public OpenCodeGuiScreen() { + super(Component.literal("OpenCode Terminal")); + this.messageHistory = new ArrayList<>(); + } + + @Override + protected void init() { + super.init(); + + // Load message history from current session + if (messageHistory.isEmpty()) { + loadMessageHistory(); + } + + // Register for real-time message updates + OpenCodeMod.getClient().setGuiMessageListener(this::onMessageDelta); + OpenCodeMod.getClient().setGuiResponseCompleteListener(this::onResponseComplete); + + // Calculate dimensions for terminal window + int terminalWidth = this.width - 40; + int terminalHeight = this.height - 40; + int terminalX = 20; + int terminalY = 20; + + // Input field at bottom + int inputY = terminalY + terminalHeight - 30; + int inputX = terminalX + 30; // Leave space for prompt ">" + + this.inputField = new EditBox( + this.font, + inputX, + inputY, + terminalWidth - 40, + 20, + Component.literal("Input") + ); + this.inputField.setMaxLength(1000); + this.inputField.setBordered(false); + this.inputField.setTextColor(INPUT_COLOR); + this.inputField.setValue(""); + this.addRenderableWidget(this.inputField); + this.setInitialFocus(this.inputField); + } + + @Override + public void removed() { + super.removed(); + // Unregister listener when GUI is closed + OpenCodeMod.getClient().clearGuiMessageListener(); + } + + /** + * Called when a message delta arrives via SSE + */ + private void onMessageDelta(String delta) { + // This is called from the main thread via OpenCodeClient + if (!receivingResponse) { + // Start of a new response + receivingResponse = true; + currentAssistantMessage.setLength(0); + addMessage("[OPENCODE] ", 0xFFdaa520); + } + + // Append delta to current message + currentAssistantMessage.append(delta); + + // Update the last message line with the accumulated text + updateLastMessage("[OPENCODE] " + currentAssistantMessage.toString()); + + // Auto-scroll to bottom when receiving messages + scrollOffset = 0; + } + + /** + * Called when a response completes (session goes to idle) + */ + private void onResponseComplete() { + if (receivingResponse) { + // Add spacing after completed response + addMessage("", 0xFF8b6f47); + receivingResponse = false; + } + } + + /** + * Updates the last message in the history (used for streaming updates) + */ + private void updateLastMessage(String newText) { + if (messageHistory.isEmpty()) { + return; + } + + // Remove old wrapped lines from the last message + // Find where the last message starts by looking backwards + int lastMessageStart = messageHistory.size() - 1; + while (lastMessageStart > 0) { + TerminalLine line = messageHistory.get(lastMessageStart - 1); + // If this line starts with a prefix like [YOU] or [OPENCODE], it's a different message + if (line.text.startsWith("[") && !line.text.equals("")) { + break; + } + // Empty lines separate messages + if (line.text.isEmpty()) { + break; + } + lastMessageStart--; + } + + // Remove old wrapped lines + while (messageHistory.size() > lastMessageStart) { + messageHistory.remove(messageHistory.size() - 1); + } + + // Re-wrap and add the new text + int maxWidth = (this.width - 60); + List wrappedLines = wrapText(newText, maxWidth); + for (String line : wrappedLines) { + messageHistory.add(new TerminalLine(line, 0xFFdaa520)); // Goldenrod for responses + } + } + + private void loadMessageHistory() { + // Add header + addMessage("[SYSTEM] OpenCode Terminal v1.0", 0xFFe67700); // Orange + addMessage("", 0xFF8b6f47); + + // Get current session and load messages + SessionInfo session = OpenCodeMod.getClient().getCurrentSession(); + if (session != null) { + addMessage("[SYSTEM] Loading session: " + session.getId(), 0xFF8b6f47); + addMessage("", 0xFF8b6f47); + + // Fetch messages asynchronously + OpenCodeMod.getClient().getCurrentSession(); + // Access the HTTP client through reflection or add a getter + // For now, we'll need to add a method to OpenCodeClient to expose this + loadSessionMessages(session.getId()); + } else { + addMessage("[SYSTEM] No active session", 0xFF8b6f47); + addMessage("[SYSTEM] Run '/oc session new' to create a session", 0xFF8b6f47); + addMessage("", 0xFF8b6f47); + } + } + + private void loadSessionMessages(String sessionId) { + OpenCodeMod.getClient().getSessionMessages(sessionId) + .thenAccept(messages -> { + // Process on main thread + net.minecraft.client.Minecraft.getInstance().execute(() -> { + if (messages.size() == 0) { + addMessage("[SYSTEM] No messages in session yet", 0xFF8b6f47); + addMessage("[SYSTEM] Type your prompt below to start", 0xFF8b6f47); + addMessage("", 0xFF8b6f47); + } else { + addMessage("[SYSTEM] Loaded " + messages.size() + " messages", 0xFF8b6f47); + addMessage("", 0xFF8b6f47); + + // Parse and display each message + for (JsonElement msgElement : messages) { + parseAndDisplayMessage(msgElement.getAsJsonObject()); + } + } + }); + }) + .exceptionally(e -> { + net.minecraft.client.Minecraft.getInstance().execute(() -> { + addMessage("[ERROR] Failed to load messages: " + e.getMessage(), 0xFFff0000); + addMessage("", 0xFF8b6f47); + }); + return null; + }); + } + + private void parseAndDisplayMessage(JsonObject message) { + // Get message info + JsonObject info = message.has("info") ? message.getAsJsonObject("info") : null; + if (info == null) return; + + String role = info.has("role") ? info.get("role").getAsString() : "unknown"; + + // Get message parts + JsonArray parts = message.has("parts") ? message.getAsJsonArray("parts") : new JsonArray(); + + // Display based on role + for (JsonElement partElement : parts) { + JsonObject part = partElement.getAsJsonObject(); + String type = part.has("type") ? part.get("type").getAsString() : ""; + + // Only display text parts + if ("text".equals(type) && part.has("text")) { + String text = part.get("text").getAsString(); + + if ("user".equals(role)) { + addMessage("[YOU] " + text, 0xFFf4a261); // Light orange/peach + } else if ("assistant".equals(role)) { + addMessage("[OPENCODE] " + text, 0xFFdaa520); // Goldenrod + } + } + } + + // Add spacing after each message exchange + addMessage("", 0xFF8b6f47); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + // Render dark background to prevent blur + renderBackground(guiGraphics, mouseX, mouseY, partialTick); + + // Calculate dimensions + int terminalWidth = this.width - 40; + int terminalHeight = this.height - 40; + int terminalX = 20; + int terminalY = 20; + + // Draw terminal background (autumn dark brown) + guiGraphics.fill(0, 0, this.width, this.height, 0xFF1a1614); // Full dark brown background + guiGraphics.fill(terminalX, terminalY, terminalX + terminalWidth, terminalY + terminalHeight, BACKGROUND_COLOR); + + // Draw border (terminal-style double line) + drawTerminalBorder(guiGraphics, terminalX, terminalY, terminalWidth, terminalHeight); + + // Draw title bar (with more spacing from top border) + String title = "┤ OpenCode Terminal ├"; + int titleX = terminalX + (terminalWidth - this.font.width(title)) / 2; + guiGraphics.drawString(this.font, title, titleX, terminalY + 11, BORDER_COLOR, false); + + // Draw message history (adjusted for title spacing) + int messageY = terminalY + 24; + int maxY = terminalY + terminalHeight - 40; + int lineHeight = this.font.lineHeight + 2; + + int startIndex = Math.max(0, messageHistory.size() - MAX_VISIBLE_LINES - scrollOffset); + int endIndex = Math.min(messageHistory.size(), startIndex + MAX_VISIBLE_LINES); + + for (int i = startIndex; i < endIndex; i++) { + if (messageY >= maxY) break; + + TerminalLine line = messageHistory.get(i); + guiGraphics.drawString(this.font, line.text, terminalX + 10, messageY, line.color, false); + messageY += lineHeight; + } + + // Draw input prompt + int inputY = terminalY + terminalHeight - 30; + guiGraphics.drawString(this.font, ">", terminalX + 10, inputY + 6, PROMPT_COLOR, false); + + // Draw input field border (darker brown) + int inputBoxX = terminalX + 25; + int inputBoxY = inputY + 2; + int inputBoxWidth = terminalWidth - 35; + guiGraphics.fill(inputBoxX, inputBoxY, inputBoxX + inputBoxWidth, inputBoxY + 18, 0xFF3e342e); + + // Render widgets (input field) + super.render(guiGraphics, mouseX, mouseY, partialTick); + + // Draw scroll indicator if needed (tan/brown color) + if (messageHistory.size() > MAX_VISIBLE_LINES) { + String scrollInfo = String.format("[↑↓ to scroll | %d/%d]", + Math.max(0, messageHistory.size() - MAX_VISIBLE_LINES - scrollOffset), + messageHistory.size() - MAX_VISIBLE_LINES); + guiGraphics.drawString(this.font, scrollInfo, terminalX + terminalWidth - this.font.width(scrollInfo) - 10, + terminalY + terminalHeight - 10, 0xFF8b6f47, false); + } + } + + private void drawTerminalBorder(GuiGraphics guiGraphics, int x, int y, int width, int height) { + int borderCharWidth = this.font.width("─"); + int sideCharWidth = this.font.width("│"); + + // Top border + guiGraphics.drawString(this.font, "┌" + "─".repeat((width - 20) / borderCharWidth) + "┐", + x, y, BORDER_COLOR, false); + + // Bottom border + guiGraphics.drawString(this.font, "└" + "─".repeat((width - 20) / borderCharWidth) + "┘", + x, y + height - 10, BORDER_COLOR, false); + + // Side borders (adjusted right side for proper alignment) + for (int i = 10; i < height - 10; i += this.font.lineHeight) { + guiGraphics.drawString(this.font, "│", x, y + i, BORDER_COLOR, false); + guiGraphics.drawString(this.font, "│", x + width - 9, y + i, BORDER_COLOR, false); + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + handleSubmit(); + return true; + } + + // Scroll with arrow keys + if (keyCode == GLFW.GLFW_KEY_UP) { + scrollOffset = Math.min(scrollOffset + 1, Math.max(0, messageHistory.size() - MAX_VISIBLE_LINES)); + return true; + } + if (keyCode == GLFW.GLFW_KEY_DOWN) { + scrollOffset = Math.max(0, scrollOffset - 1); + return true; + } + + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + this.onClose(); + return true; + } + + return this.inputField.keyPressed(keyCode, scanCode, modifiers) || super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double deltaX, double deltaY) { + // Scroll with mouse wheel + if (deltaY > 0) { + scrollOffset = Math.min(scrollOffset + 1, Math.max(0, messageHistory.size() - MAX_VISIBLE_LINES)); + } else if (deltaY < 0) { + scrollOffset = Math.max(0, scrollOffset - 1); + } + return true; + } + + private void handleSubmit() { + String text = this.inputField.getValue().trim(); + if (!text.isEmpty()) { + // Check if we have an active session + SessionInfo session = OpenCodeMod.getClient().getCurrentSession(); + if (session == null) { + addMessage("[ERROR] No active session. Run '/oc session new' first", 0xFFff0000); + this.inputField.setValue(""); + return; + } + + addMessage("[YOU] " + text, 0xFFf4a261); // Light orange/peach for user input + addMessage("", 0xFF8b6f47); // Empty line for spacing + this.inputField.setValue(""); + + // Reset response tracking + receivingResponse = false; + currentAssistantMessage.setLength(0); + + // Send to OpenCode server + OpenCodeMod.getClient().sendPrompt(text) + .exceptionally(e -> { + net.minecraft.client.Minecraft.getInstance().execute(() -> { + addMessage("[ERROR] Failed to send message: " + e.getMessage(), 0xFFff0000); + }); + return null; + }); + + // Auto-scroll to bottom + scrollOffset = 0; + } + } + + public void addMessage(String message, int color) { + // Word wrap long messages + int maxWidth = (this.width - 60); + List wrappedLines = wrapText(message, maxWidth); + + for (String line : wrappedLines) { + messageHistory.add(new TerminalLine(line, color)); + } + + // Keep scroll at bottom for new messages + if (scrollOffset == 0) { + // Already at bottom, stay there + } + } + + private List wrapText(String text, int maxWidth) { + List lines = new ArrayList<>(); + String[] words = text.split(" "); + StringBuilder currentLine = new StringBuilder(); + + for (String word : words) { + String testLine = currentLine.length() == 0 ? word : currentLine + " " + word; + if (this.font.width(testLine) <= maxWidth) { + if (currentLine.length() > 0) currentLine.append(" "); + currentLine.append(word); + } else { + if (currentLine.length() > 0) { + lines.add(currentLine.toString()); + currentLine = new StringBuilder(word); + } else { + // Single word is too long, add it anyway + lines.add(word); + } + } + } + + if (currentLine.length() > 0) { + lines.add(currentLine.toString()); + } + + return lines.isEmpty() ? List.of(text) : lines; + } + + @Override + public boolean isPauseScreen() { + return false; // Don't pause the game + } + + /** + * Internal class to store terminal lines with color + */ + private static class TerminalLine { + String text; + int color; + + TerminalLine(String text, int color) { + this.text = text; + this.color = color; + } + } +} From 6308fa9d0380b03e3759020bd129b22529d57438 Mon Sep 17 00:00:00 2001 From: "ciefa.eth" <156933735+ciefa@users.noreply.github.com> Date: Sun, 4 Jan 2026 12:21:22 +0100 Subject: [PATCH 2/7] Fix message streaming logic in GUI Improved updateLastMessage method to more reliably find and update the last assistant message during real-time streaming: - Remove premature addMessage call that caused duplicate prefixes - Search backwards for [OPENCODE] prefix explicitly - Better edge case handling for empty messages - More robust message boundary detection --- .../minecraft/gui/OpenCodeGuiScreen.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java index 8897d97..29b5993 100644 --- a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java +++ b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java @@ -94,7 +94,6 @@ private void onMessageDelta(String delta) { // Start of a new response receivingResponse = true; currentAssistantMessage.setLength(0); - addMessage("[OPENCODE] ", 0xFFdaa520); } // Append delta to current message @@ -122,27 +121,23 @@ private void onResponseComplete() { * Updates the last message in the history (used for streaming updates) */ private void updateLastMessage(String newText) { - if (messageHistory.isEmpty()) { - return; - } - - // Remove old wrapped lines from the last message - // Find where the last message starts by looking backwards - int lastMessageStart = messageHistory.size() - 1; - while (lastMessageStart > 0) { - TerminalLine line = messageHistory.get(lastMessageStart - 1); - // If this line starts with a prefix like [YOU] or [OPENCODE], it's a different message - if (line.text.startsWith("[") && !line.text.equals("")) { + // Find where the last assistant message starts by looking backwards + int lastMessageStart = messageHistory.size(); + + // Look for the last [OPENCODE] message + for (int i = messageHistory.size() - 1; i >= 0; i--) { + TerminalLine line = messageHistory.get(i); + if (line.text.startsWith("[OPENCODE]")) { + lastMessageStart = i; break; } - // Empty lines separate messages - if (line.text.isEmpty()) { + // Stop at empty lines or other message types + if (line.text.isEmpty() || line.text.startsWith("[YOU]") || line.text.startsWith("[SYSTEM]")) { break; } - lastMessageStart--; } - // Remove old wrapped lines + // Remove old assistant message lines while (messageHistory.size() > lastMessageStart) { messageHistory.remove(messageHistory.size() - 1); } From 4795322d5031ba3e77cbea45c0fa63dc64aabcd9 Mon Sep 17 00:00:00 2001 From: "ciefa.eth" <156933735+ciefa@users.noreply.github.com> Date: Sun, 4 Jan 2026 12:35:29 +0100 Subject: [PATCH 3/7] Add advanced markdown formatting to terminal GUI Implemented full markdown parser with rich text rendering for better readability of AI responses, code examples, and technical content. New Features: - Code blocks with borders and syntax labels (triple backticks) - Inline code formatting with distinct color - Bold text rendering with brightened color - Header formatting with enhanced styling - List formatting (bullets and numbered) - Link text parsing and styling - Multi-segment per-line rendering for mixed formatting Implementation: - Created markdown parser package with TextSegment, FormattedLine, MarkdownParser - TextSegment: Stores styled text with color and formatting flags - FormattedLine: Represents a line with multiple styled segments and indentation - MarkdownParser: Parses markdown syntax into formatted structures - Updated OpenCodeGuiScreen to render formatted lines with multiple colors - Extended autumn color palette for different content types Color Scheme: - Regular text: Cream (#f4e8d0) - Bold/Headers: Bright orange (#e67700) - Inline code: Tan (#d4a574) - Code blocks: Light tan (#c9b896) - Links: Goldenrod (#daa520) Technical Details: - Regex-based parsing for inline formatting - Support for bold (**text**), code (`code`), links ([text](url)) - Code block detection and preservation - Automatic color brightening for bold/header text - Indentation support for code blocks and lists --- .../minecraft/gui/OpenCodeGuiScreen.java | 89 +++----- .../minecraft/gui/markdown/FormattedLine.java | 64 ++++++ .../gui/markdown/MarkdownParser.java | 200 ++++++++++++++++++ .../minecraft/gui/markdown/TextSegment.java | 68 ++++++ 4 files changed, 365 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/opencode/minecraft/gui/markdown/FormattedLine.java create mode 100644 src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java create mode 100644 src/main/java/com/opencode/minecraft/gui/markdown/TextSegment.java diff --git a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java index 29b5993..8837b83 100644 --- a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java +++ b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java @@ -5,6 +5,9 @@ import com.google.gson.JsonObject; import com.opencode.minecraft.OpenCodeMod; import com.opencode.minecraft.client.session.SessionInfo; +import com.opencode.minecraft.gui.markdown.FormattedLine; +import com.opencode.minecraft.gui.markdown.MarkdownParser; +import com.opencode.minecraft.gui.markdown.TextSegment; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.Screen; @@ -28,7 +31,7 @@ public class OpenCodeGuiScreen extends Screen { private static final int PROMPT_COLOR = 0xFFe67700; // Orange prompt private EditBox inputField; - private List messageHistory; + private List messageHistory; private int scrollOffset = 0; private static final int MAX_VISIBLE_LINES = 20; private StringBuilder currentAssistantMessage = new StringBuilder(); @@ -123,16 +126,17 @@ private void onResponseComplete() { private void updateLastMessage(String newText) { // Find where the last assistant message starts by looking backwards int lastMessageStart = messageHistory.size(); - + // Look for the last [OPENCODE] message for (int i = messageHistory.size() - 1; i >= 0; i--) { - TerminalLine line = messageHistory.get(i); - if (line.text.startsWith("[OPENCODE]")) { + FormattedLine line = messageHistory.get(i); + String plainText = line.getPlainText(); + if (plainText.trim().startsWith("[OPENCODE]")) { lastMessageStart = i; break; } // Stop at empty lines or other message types - if (line.text.isEmpty() || line.text.startsWith("[YOU]") || line.text.startsWith("[SYSTEM]")) { + if (plainText.trim().isEmpty() || plainText.trim().startsWith("[YOU]") || plainText.trim().startsWith("[SYSTEM]")) { break; } } @@ -142,12 +146,9 @@ private void updateLastMessage(String newText) { messageHistory.remove(messageHistory.size() - 1); } - // Re-wrap and add the new text - int maxWidth = (this.width - 60); - List wrappedLines = wrapText(newText, maxWidth); - for (String line : wrappedLines) { - messageHistory.add(new TerminalLine(line, 0xFFdaa520)); // Goldenrod for responses - } + // Parse markdown and add the new text + List parsedLines = MarkdownParser.parse(newText, 0xFFdaa520); // Goldenrod for responses + messageHistory.addAll(parsedLines); } private void loadMessageHistory() { @@ -267,8 +268,8 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia for (int i = startIndex; i < endIndex; i++) { if (messageY >= maxY) break; - TerminalLine line = messageHistory.get(i); - guiGraphics.drawString(this.font, line.text, terminalX + 10, messageY, line.color, false); + FormattedLine line = messageHistory.get(i); + renderFormattedLine(guiGraphics, line, terminalX + 10, messageY); messageY += lineHeight; } @@ -384,13 +385,9 @@ private void handleSubmit() { } public void addMessage(String message, int color) { - // Word wrap long messages - int maxWidth = (this.width - 60); - List wrappedLines = wrapText(message, maxWidth); - - for (String line : wrappedLines) { - messageHistory.add(new TerminalLine(line, color)); - } + // Parse markdown and add formatted lines + List parsedLines = MarkdownParser.parse(message, color); + messageHistory.addAll(parsedLines); // Keep scroll at bottom for new messages if (scrollOffset == 0) { @@ -398,49 +395,29 @@ public void addMessage(String message, int color) { } } - private List wrapText(String text, int maxWidth) { - List lines = new ArrayList<>(); - String[] words = text.split(" "); - StringBuilder currentLine = new StringBuilder(); - - for (String word : words) { - String testLine = currentLine.length() == 0 ? word : currentLine + " " + word; - if (this.font.width(testLine) <= maxWidth) { - if (currentLine.length() > 0) currentLine.append(" "); - currentLine.append(word); - } else { - if (currentLine.length() > 0) { - lines.add(currentLine.toString()); - currentLine = new StringBuilder(word); - } else { - // Single word is too long, add it anyway - lines.add(word); - } - } - } + /** + * Renders a formatted line with multiple colored segments + */ + private void renderFormattedLine(GuiGraphics guiGraphics, FormattedLine line, int x, int y) { + int currentX = x; - if (currentLine.length() > 0) { - lines.add(currentLine.toString()); + // Add indentation for code blocks and lists + for (int i = 0; i < line.getIndentLevel(); i++) { + currentX += this.font.width(" "); } - return lines.isEmpty() ? List.of(text) : lines; + // Draw each segment + for (TextSegment segment : line.getSegments()) { + String text = segment.getText(); + int color = segment.getEffectiveColor(); + + guiGraphics.drawString(this.font, text, currentX, y, color, false); + currentX += this.font.width(text); + } } @Override public boolean isPauseScreen() { return false; // Don't pause the game } - - /** - * Internal class to store terminal lines with color - */ - private static class TerminalLine { - String text; - int color; - - TerminalLine(String text, int color) { - this.text = text; - this.color = color; - } - } } diff --git a/src/main/java/com/opencode/minecraft/gui/markdown/FormattedLine.java b/src/main/java/com/opencode/minecraft/gui/markdown/FormattedLine.java new file mode 100644 index 0000000..64f62f4 --- /dev/null +++ b/src/main/java/com/opencode/minecraft/gui/markdown/FormattedLine.java @@ -0,0 +1,64 @@ +package com.opencode.minecraft.gui.markdown; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a line with multiple styled text segments + */ +public class FormattedLine { + private final List segments; + private final boolean codeBlock; + private final int indentLevel; + + public FormattedLine() { + this(new ArrayList<>(), false, 0); + } + + public FormattedLine(List segments, boolean codeBlock, int indentLevel) { + this.segments = segments; + this.codeBlock = codeBlock; + this.indentLevel = indentLevel; + } + + public List getSegments() { + return segments; + } + + public void addSegment(TextSegment segment) { + segments.add(segment); + } + + public void addSegment(String text, int color) { + segments.add(new TextSegment(text, color)); + } + + public boolean isCodeBlock() { + return codeBlock; + } + + public int getIndentLevel() { + return indentLevel; + } + + public boolean isEmpty() { + return segments.isEmpty() || + (segments.size() == 1 && segments.get(0).getText().trim().isEmpty()); + } + + /** + * Gets the plain text content (for width calculations) + */ + public String getPlainText() { + StringBuilder sb = new StringBuilder(); + // Add indent + for (int i = 0; i < indentLevel; i++) { + sb.append(" "); + } + // Add text + for (TextSegment segment : segments) { + sb.append(segment.getText()); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java b/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java new file mode 100644 index 0000000..d807f5d --- /dev/null +++ b/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java @@ -0,0 +1,200 @@ +package com.opencode.minecraft.gui.markdown; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses markdown text into formatted lines with styled segments + */ +public class MarkdownParser { + // Autumn theme color palette + private static final int COLOR_TEXT = 0xFFf4e8d0; // Cream text + private static final int COLOR_BOLD = 0xFFe67700; // Orange for bold/headers + private static final int COLOR_CODE = 0xFFd4a574; // Tan for inline code + private static final int COLOR_CODE_BLOCK = 0xFFc9b896; // Light tan for code blocks + private static final int COLOR_LINK = 0xFFdaa520; // Goldenrod for links + + // Regex patterns for inline markdown + private static final Pattern BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*|__(.+?)__"); + private static final Pattern ITALIC_PATTERN = Pattern.compile("\\*(.+?)\\*|_(.+?)_"); + private static final Pattern CODE_PATTERN = Pattern.compile("`([^`]+?)`"); + private static final Pattern LINK_PATTERN = Pattern.compile("\\[([^\\]]+?)\\]\\(([^\\)]+?)\\)"); + + /** + * Parse markdown text into formatted lines + */ + public static List parse(String markdownText, int baseColor) { + List lines = new ArrayList<>(); + String[] rawLines = markdownText.split("\n"); + + boolean inCodeBlock = false; + String codeBlockLang = ""; + + for (String line : rawLines) { + // Check for code block start/end + if (line.trim().startsWith("```")) { + if (!inCodeBlock) { + // Starting code block + inCodeBlock = true; + codeBlockLang = line.trim().substring(3).trim(); + // Add a separator line + FormattedLine separator = new FormattedLine(); + separator.addSegment("┌─[ " + (codeBlockLang.isEmpty() ? "code" : codeBlockLang) + " ]", COLOR_CODE); + lines.add(separator); + } else { + // Ending code block + inCodeBlock = false; + FormattedLine separator = new FormattedLine(); + separator.addSegment("└" + "─".repeat(20), COLOR_CODE); + lines.add(separator); + } + continue; + } + + if (inCodeBlock) { + // Code block line - preserve formatting, indent, use code color + FormattedLine codeLine = new FormattedLine(new ArrayList<>(), true, 1); + codeLine.addSegment(line, COLOR_CODE_BLOCK); + lines.add(codeLine); + } else { + // Regular line - parse inline markdown + FormattedLine formattedLine = parseInlineMarkdown(line, baseColor); + lines.add(formattedLine); + } + } + + return lines; + } + + /** + * Parse inline markdown within a single line + */ + private static FormattedLine parseInlineMarkdown(String line, int baseColor) { + FormattedLine formattedLine = new FormattedLine(); + + // Check for header + int headerLevel = 0; + String trimmed = line.trim(); + while (trimmed.startsWith("#")) { + headerLevel++; + trimmed = trimmed.substring(1); + } + if (headerLevel > 0 && trimmed.startsWith(" ")) { + // This is a header + trimmed = trimmed.trim(); + formattedLine.addSegment(trimmed, COLOR_BOLD); + return formattedLine; + } + + // Check for list items + int indentLevel = 0; + if (line.trim().startsWith("- ") || line.trim().startsWith("* ")) { + formattedLine = new FormattedLine(new ArrayList<>(), false, 1); + line = "• " + line.trim().substring(2); + } else if (line.trim().matches("^\\d+\\.\\s+.*")) { + formattedLine = new FormattedLine(new ArrayList<>(), false, 1); + } + + // Parse inline formatting (bold, italic, code, links) + parseInlineFormatting(line, baseColor, formattedLine); + + return formattedLine; + } + + /** + * Parse inline formatting like **bold**, *italic*, `code`, [links] + */ + private static void parseInlineFormatting(String text, int baseColor, FormattedLine line) { + List spans = new ArrayList<>(); + + // Find all formatting spans + Matcher boldMatcher = BOLD_PATTERN.matcher(text); + while (boldMatcher.find()) { + String content = boldMatcher.group(1) != null ? boldMatcher.group(1) : boldMatcher.group(2); + spans.add(new FormatSpan(boldMatcher.start(), boldMatcher.end(), content, FormatType.BOLD)); + } + + Matcher codeMatcher = CODE_PATTERN.matcher(text); + while (codeMatcher.find()) { + spans.add(new FormatSpan(codeMatcher.start(), codeMatcher.end(), codeMatcher.group(1), FormatType.CODE)); + } + + Matcher linkMatcher = LINK_PATTERN.matcher(text); + while (linkMatcher.find()) { + spans.add(new FormatSpan(linkMatcher.start(), linkMatcher.end(), linkMatcher.group(1), FormatType.LINK)); + } + + // Sort spans by start position + spans.sort((a, b) -> Integer.compare(a.start, b.start)); + + // Build segments + int currentPos = 0; + for (FormatSpan span : spans) { + // Add text before this span + if (span.start > currentPos) { + String before = text.substring(currentPos, span.start); + if (!before.isEmpty()) { + line.addSegment(before, baseColor); + } + } + + // Add formatted span + int color = baseColor; + boolean isBold = false; + boolean isCode = false; + + switch (span.type) { + case BOLD: + color = COLOR_BOLD; + isBold = true; + break; + case CODE: + color = COLOR_CODE; + isCode = true; + break; + case LINK: + color = COLOR_LINK; + break; + } + + line.addSegment(new TextSegment(span.content, color, isBold, false, isCode, false)); + currentPos = span.end; + } + + // Add remaining text + if (currentPos < text.length()) { + String remaining = text.substring(currentPos); + if (!remaining.isEmpty()) { + line.addSegment(remaining, baseColor); + } + } + + // If no formatting found, add the whole line + if (line.getSegments().isEmpty()) { + line.addSegment(text, baseColor); + } + } + + /** + * Helper class to track format spans + */ + private static class FormatSpan { + int start; + int end; + String content; + FormatType type; + + FormatSpan(int start, int end, String content, FormatType type) { + this.start = start; + this.end = end; + this.content = content; + this.type = type; + } + } + + private enum FormatType { + BOLD, ITALIC, CODE, LINK + } +} diff --git a/src/main/java/com/opencode/minecraft/gui/markdown/TextSegment.java b/src/main/java/com/opencode/minecraft/gui/markdown/TextSegment.java new file mode 100644 index 0000000..9a63643 --- /dev/null +++ b/src/main/java/com/opencode/minecraft/gui/markdown/TextSegment.java @@ -0,0 +1,68 @@ +package com.opencode.minecraft.gui.markdown; + +/** + * Represents a styled segment of text with color and formatting + */ +public class TextSegment { + private final String text; + private final int color; + private final boolean bold; + private final boolean italic; + private final boolean code; + private final boolean header; + + public TextSegment(String text, int color) { + this(text, color, false, false, false, false); + } + + public TextSegment(String text, int color, boolean bold, boolean italic, boolean code, boolean header) { + this.text = text; + this.color = color; + this.bold = bold; + this.italic = italic; + this.code = code; + this.header = header; + } + + public String getText() { + return text; + } + + public int getColor() { + return color; + } + + public boolean isBold() { + return bold; + } + + public boolean isItalic() { + return italic; + } + + public boolean isCode() { + return code; + } + + public boolean isHeader() { + return header; + } + + public int getEffectiveColor() { + // Apply brightness adjustments for styling + if (bold || header) { + // Make bold/headers brighter + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + + // Increase brightness by 20% + r = Math.min(255, (int)(r * 1.2)); + g = Math.min(255, (int)(g * 1.2)); + b = Math.min(255, (int)(b * 1.2)); + + return 0xFF000000 | (r << 16) | (g << 8) | b; + } + return color; + } +} From ceea50546be3ced891f2e13a97cd393875f6e3fd Mon Sep 17 00:00:00 2001 From: "ciefa.eth" <156933735+ciefa@users.noreply.github.com> Date: Sun, 4 Jan 2026 12:38:02 +0100 Subject: [PATCH 4/7] Fix terminal using only half the screen Replaced hardcoded MAX_VISIBLE_LINES (20) with dynamic calculation based on actual available screen height. Terminal now uses full available space to display messages instead of being artificially limited to 20 lines. Changes: - Removed MAX_VISIBLE_LINES constant - Calculate maxVisibleLines dynamically in render method - Updated scroll handlers to use dynamic line calculation - Terminal now adapts to any screen size/resolution The terminal will now fill the entire available space between the title bar and input field, significantly improving readability. --- .../minecraft/gui/OpenCodeGuiScreen.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java index 8837b83..b909c8f 100644 --- a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java +++ b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java @@ -33,7 +33,6 @@ public class OpenCodeGuiScreen extends Screen { private EditBox inputField; private List messageHistory; private int scrollOffset = 0; - private static final int MAX_VISIBLE_LINES = 20; private StringBuilder currentAssistantMessage = new StringBuilder(); private boolean receivingResponse = false; @@ -262,8 +261,12 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia int maxY = terminalY + terminalHeight - 40; int lineHeight = this.font.lineHeight + 2; - int startIndex = Math.max(0, messageHistory.size() - MAX_VISIBLE_LINES - scrollOffset); - int endIndex = Math.min(messageHistory.size(), startIndex + MAX_VISIBLE_LINES); + // Calculate how many lines can actually fit on screen + int availableHeight = maxY - (terminalY + 24); + int maxVisibleLines = Math.max(1, availableHeight / lineHeight); + + int startIndex = Math.max(0, messageHistory.size() - maxVisibleLines - scrollOffset); + int endIndex = Math.min(messageHistory.size(), startIndex + maxVisibleLines); for (int i = startIndex; i < endIndex; i++) { if (messageY >= maxY) break; @@ -287,10 +290,10 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia super.render(guiGraphics, mouseX, mouseY, partialTick); // Draw scroll indicator if needed (tan/brown color) - if (messageHistory.size() > MAX_VISIBLE_LINES) { + if (messageHistory.size() > maxVisibleLines) { String scrollInfo = String.format("[↑↓ to scroll | %d/%d]", - Math.max(0, messageHistory.size() - MAX_VISIBLE_LINES - scrollOffset), - messageHistory.size() - MAX_VISIBLE_LINES); + Math.max(0, messageHistory.size() - maxVisibleLines - scrollOffset), + messageHistory.size() - maxVisibleLines); guiGraphics.drawString(this.font, scrollInfo, terminalX + terminalWidth - this.font.width(scrollInfo) - 10, terminalY + terminalHeight - 10, 0xFF8b6f47, false); } @@ -324,7 +327,10 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { // Scroll with arrow keys if (keyCode == GLFW.GLFW_KEY_UP) { - scrollOffset = Math.min(scrollOffset + 1, Math.max(0, messageHistory.size() - MAX_VISIBLE_LINES)); + int availableHeight = (this.height - 40 - 40 - 24); + int lineHeight = this.font.lineHeight + 2; + int maxVisibleLines = Math.max(1, availableHeight / lineHeight); + scrollOffset = Math.min(scrollOffset + 1, Math.max(0, messageHistory.size() - maxVisibleLines)); return true; } if (keyCode == GLFW.GLFW_KEY_DOWN) { @@ -343,8 +349,12 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { @Override public boolean mouseScrolled(double mouseX, double mouseY, double deltaX, double deltaY) { // Scroll with mouse wheel + int availableHeight = (this.height - 40 - 40 - 24); + int lineHeight = this.font.lineHeight + 2; + int maxVisibleLines = Math.max(1, availableHeight / lineHeight); + if (deltaY > 0) { - scrollOffset = Math.min(scrollOffset + 1, Math.max(0, messageHistory.size() - MAX_VISIBLE_LINES)); + scrollOffset = Math.min(scrollOffset + 1, Math.max(0, messageHistory.size() - maxVisibleLines)); } else if (deltaY < 0) { scrollOffset = Math.max(0, scrollOffset - 1); } From 86ac2ee33c04385ad941cf0b790a73568f6559f1 Mon Sep 17 00:00:00 2001 From: "ciefa.eth" <156933735+ciefa@users.noreply.github.com> Date: Sun, 4 Jan 2026 13:17:02 +0100 Subject: [PATCH 5/7] Update to expanded vibrant autumn color palette Replaced muted brown tones with a richer, more varied autumn palette featuring reds, burnt oranges, golden yellows, and copper tones. New Color Scheme (Option 2 - Expanded Autumn): - Background: Warm dark brown (#1a1210) - Borders/Prompt: Burnt orange (#ff8c42) - Regular text: Warm white/cornsilk (#fff8dc) - Bold/Headers: Rust red (#b7410e) - Inline code: Golden yellow (#ffd700) - Code blocks: Copper (#d2691e) - Links: Burgundy (#800020) - User messages: Light salmon (#ffa07a) - System messages: Amber (#ffbf00) - Assistant messages: Dark orange (#ff8c00) - Error messages: Crimson red (#dc143c) - Input box border: Darker copper (#4a2f1e) The new palette provides much better contrast and visual interest while maintaining the cozy autumn aesthetic. Colors are significantly more vibrant and varied compared to the previous muted brown theme. --- .../minecraft/gui/OpenCodeGuiScreen.java | 69 ++++++++++--------- .../gui/markdown/MarkdownParser.java | 12 ++-- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java index b909c8f..af27f3c 100644 --- a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java +++ b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java @@ -23,12 +23,12 @@ */ public class OpenCodeGuiScreen extends Screen { - // Autumn theme colors - private static final int BACKGROUND_COLOR = 0xE01a1614; // Dark brown (semi-transparent) - private static final int BORDER_COLOR = 0xFFe67700; // Orange border - private static final int TEXT_COLOR = 0xFFf4e8d0; // Cream text - private static final int INPUT_COLOR = 0xFFfaf0e6; // Bright cream input - private static final int PROMPT_COLOR = 0xFFe67700; // Orange prompt + // Expanded autumn theme colors - vibrant reds, oranges, golds, and coppers + private static final int BACKGROUND_COLOR = 0xE01a1210; // Dark warm brown (semi-transparent) + private static final int BORDER_COLOR = 0xFFff8c42; // Burnt orange border + private static final int TEXT_COLOR = 0xFFfff8dc; // Warm white (cornsilk) + private static final int INPUT_COLOR = 0xFFfff8dc; // Warm white input + private static final int PROMPT_COLOR = 0xFFff8c42; // Burnt orange prompt private EditBox inputField; private List messageHistory; @@ -114,7 +114,7 @@ private void onMessageDelta(String delta) { private void onResponseComplete() { if (receivingResponse) { // Add spacing after completed response - addMessage("", 0xFF8b6f47); + addMessage("", 0xFFffbf00); // Amber receivingResponse = false; } } @@ -146,20 +146,20 @@ private void updateLastMessage(String newText) { } // Parse markdown and add the new text - List parsedLines = MarkdownParser.parse(newText, 0xFFdaa520); // Goldenrod for responses + List parsedLines = MarkdownParser.parse(newText, 0xFFff8c00); // Dark orange for responses messageHistory.addAll(parsedLines); } private void loadMessageHistory() { // Add header - addMessage("[SYSTEM] OpenCode Terminal v1.0", 0xFFe67700); // Orange - addMessage("", 0xFF8b6f47); + addMessage("[SYSTEM] OpenCode Terminal v1.0", 0xFFff8c42); // Burnt orange + addMessage("", 0xFFffbf00); // Get current session and load messages SessionInfo session = OpenCodeMod.getClient().getCurrentSession(); if (session != null) { - addMessage("[SYSTEM] Loading session: " + session.getId(), 0xFF8b6f47); - addMessage("", 0xFF8b6f47); + addMessage("[SYSTEM] Loading session: " + session.getId(), 0xFFffbf00); // Amber + addMessage("", 0xFFffbf00); // Fetch messages asynchronously OpenCodeMod.getClient().getCurrentSession(); @@ -167,9 +167,9 @@ private void loadMessageHistory() { // For now, we'll need to add a method to OpenCodeClient to expose this loadSessionMessages(session.getId()); } else { - addMessage("[SYSTEM] No active session", 0xFF8b6f47); - addMessage("[SYSTEM] Run '/oc session new' to create a session", 0xFF8b6f47); - addMessage("", 0xFF8b6f47); + addMessage("[SYSTEM] No active session", 0xFFffbf00); // Amber + addMessage("[SYSTEM] Run '/oc session new' to create a session", 0xFFffbf00); + addMessage("", 0xFFffbf00); } } @@ -179,12 +179,12 @@ private void loadSessionMessages(String sessionId) { // Process on main thread net.minecraft.client.Minecraft.getInstance().execute(() -> { if (messages.size() == 0) { - addMessage("[SYSTEM] No messages in session yet", 0xFF8b6f47); - addMessage("[SYSTEM] Type your prompt below to start", 0xFF8b6f47); - addMessage("", 0xFF8b6f47); + addMessage("[SYSTEM] No messages in session yet", 0xFFffbf00); // Amber + addMessage("[SYSTEM] Type your prompt below to start", 0xFFffbf00); + addMessage("", 0xFFffbf00); } else { - addMessage("[SYSTEM] Loaded " + messages.size() + " messages", 0xFF8b6f47); - addMessage("", 0xFF8b6f47); + addMessage("[SYSTEM] Loaded " + messages.size() + " messages", 0xFFffbf00); // Amber + addMessage("", 0xFFffbf00); // Parse and display each message for (JsonElement msgElement : messages) { @@ -195,8 +195,8 @@ private void loadSessionMessages(String sessionId) { }) .exceptionally(e -> { net.minecraft.client.Minecraft.getInstance().execute(() -> { - addMessage("[ERROR] Failed to load messages: " + e.getMessage(), 0xFFff0000); - addMessage("", 0xFF8b6f47); + addMessage("[ERROR] Failed to load messages: " + e.getMessage(), 0xFFdc143c); // Crimson red + addMessage("", 0xFFffbf00); }); return null; }); @@ -222,15 +222,15 @@ private void parseAndDisplayMessage(JsonObject message) { String text = part.get("text").getAsString(); if ("user".equals(role)) { - addMessage("[YOU] " + text, 0xFFf4a261); // Light orange/peach + addMessage("[YOU] " + text, 0xFFffa07a); // Light salmon } else if ("assistant".equals(role)) { - addMessage("[OPENCODE] " + text, 0xFFdaa520); // Goldenrod + addMessage("[OPENCODE] " + text, 0xFFff8c00); // Dark orange } } } // Add spacing after each message exchange - addMessage("", 0xFF8b6f47); + addMessage("", 0xFFffbf00); } @Override @@ -244,8 +244,8 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia int terminalX = 20; int terminalY = 20; - // Draw terminal background (autumn dark brown) - guiGraphics.fill(0, 0, this.width, this.height, 0xFF1a1614); // Full dark brown background + // Draw terminal background (warm dark brown) + guiGraphics.fill(0, 0, this.width, this.height, 0xFF1a1210); // Full warm brown background guiGraphics.fill(terminalX, terminalY, terminalX + terminalWidth, terminalY + terminalHeight, BACKGROUND_COLOR); // Draw border (terminal-style double line) @@ -280,11 +280,11 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia int inputY = terminalY + terminalHeight - 30; guiGraphics.drawString(this.font, ">", terminalX + 10, inputY + 6, PROMPT_COLOR, false); - // Draw input field border (darker brown) + // Draw input field border (darker copper) int inputBoxX = terminalX + 25; int inputBoxY = inputY + 2; int inputBoxWidth = terminalWidth - 35; - guiGraphics.fill(inputBoxX, inputBoxY, inputBoxX + inputBoxWidth, inputBoxY + 18, 0xFF3e342e); + guiGraphics.fill(inputBoxX, inputBoxY, inputBoxX + inputBoxWidth, inputBoxY + 18, 0xFF4a2f1e); // Render widgets (input field) super.render(guiGraphics, mouseX, mouseY, partialTick); @@ -294,8 +294,9 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia String scrollInfo = String.format("[↑↓ to scroll | %d/%d]", Math.max(0, messageHistory.size() - maxVisibleLines - scrollOffset), messageHistory.size() - maxVisibleLines); + // Position below the bottom border (border is at -10, text goes at -2) guiGraphics.drawString(this.font, scrollInfo, terminalX + terminalWidth - this.font.width(scrollInfo) - 10, - terminalY + terminalHeight - 10, 0xFF8b6f47, false); + terminalY + terminalHeight - 2, 0xFFffbf00, false); // Amber } } @@ -367,13 +368,13 @@ private void handleSubmit() { // Check if we have an active session SessionInfo session = OpenCodeMod.getClient().getCurrentSession(); if (session == null) { - addMessage("[ERROR] No active session. Run '/oc session new' first", 0xFFff0000); + addMessage("[ERROR] No active session. Run '/oc session new' first", 0xFFdc143c); // Crimson red this.inputField.setValue(""); return; } - addMessage("[YOU] " + text, 0xFFf4a261); // Light orange/peach for user input - addMessage("", 0xFF8b6f47); // Empty line for spacing + addMessage("[YOU] " + text, 0xFFffa07a); // Light salmon for user input + addMessage("", 0xFFffbf00); // Empty line for spacing this.inputField.setValue(""); // Reset response tracking @@ -384,7 +385,7 @@ private void handleSubmit() { OpenCodeMod.getClient().sendPrompt(text) .exceptionally(e -> { net.minecraft.client.Minecraft.getInstance().execute(() -> { - addMessage("[ERROR] Failed to send message: " + e.getMessage(), 0xFFff0000); + addMessage("[ERROR] Failed to send message: " + e.getMessage(), 0xFFdc143c); // Crimson red }); return null; }); diff --git a/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java b/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java index d807f5d..4cd338b 100644 --- a/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java +++ b/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java @@ -9,12 +9,12 @@ * Parses markdown text into formatted lines with styled segments */ public class MarkdownParser { - // Autumn theme color palette - private static final int COLOR_TEXT = 0xFFf4e8d0; // Cream text - private static final int COLOR_BOLD = 0xFFe67700; // Orange for bold/headers - private static final int COLOR_CODE = 0xFFd4a574; // Tan for inline code - private static final int COLOR_CODE_BLOCK = 0xFFc9b896; // Light tan for code blocks - private static final int COLOR_LINK = 0xFFdaa520; // Goldenrod for links + // Expanded autumn theme color palette - vibrant and varied + private static final int COLOR_TEXT = 0xFFfff8dc; // Warm white (cornsilk) + private static final int COLOR_BOLD = 0xFFb7410e; // Rust red for bold/headers + private static final int COLOR_CODE = 0xFFffd700; // Golden yellow for inline code + private static final int COLOR_CODE_BLOCK = 0xFFd2691e; // Copper for code blocks + private static final int COLOR_LINK = 0xFF800020; // Burgundy for links // Regex patterns for inline markdown private static final Pattern BOLD_PATTERN = Pattern.compile("\\*\\*(.+?)\\*\\*|__(.+?)__"); From ed7aabaf90d964c64e61cfe664ae7c35d2482d48 Mon Sep 17 00:00:00 2001 From: "ciefa.eth" <156933735+ciefa@users.noreply.github.com> Date: Sun, 4 Jan 2026 13:38:25 +0100 Subject: [PATCH 6/7] Replace text borders with graphical borders and add text clipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced Unicode character borders (┌─┐│└┘) with clean graphical rectangular borders using fill() for better appearance and alignment. Added proper text clipping to prevent overflow beyond terminal bounds. Border Improvements: - Replaced text-based borders with solid 2px graphical rectangles - Clean, professional appearance with perfect alignment - No more misalignment issues from font rendering - Better performance (no text rendering for borders) - Simplified title (removed decorative characters) Text Overflow Fixes: - Added scissor/clipping region to message area - Text is truncated with "..." if too long for available width - Code blocks wrap at 120 characters per line - Proper margin calculations inside borders (8px padding) - Boundary checking in renderFormattedLine method Layout Adjustments: - Border thickness: 2px - Message area margins: 8px inside borders - Title positioned at y+8 from top border - Scroll indicator positioned inside bottom-right corner - All text now properly contained within terminal bounds --- .../minecraft/gui/OpenCodeGuiScreen.java | 78 ++++++++++++------- .../gui/markdown/MarkdownParser.java | 31 ++++++-- 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java index af27f3c..3c2be28 100644 --- a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java +++ b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java @@ -248,34 +248,44 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia guiGraphics.fill(0, 0, this.width, this.height, 0xFF1a1210); // Full warm brown background guiGraphics.fill(terminalX, terminalY, terminalX + terminalWidth, terminalY + terminalHeight, BACKGROUND_COLOR); - // Draw border (terminal-style double line) + // Draw border (clean graphical rectangles) drawTerminalBorder(guiGraphics, terminalX, terminalY, terminalWidth, terminalHeight); - // Draw title bar (with more spacing from top border) - String title = "┤ OpenCode Terminal ├"; + // Draw title bar (centered, with spacing from top border) + String title = "OpenCode Terminal"; int titleX = terminalX + (terminalWidth - this.font.width(title)) / 2; - guiGraphics.drawString(this.font, title, titleX, terminalY + 11, BORDER_COLOR, false); + guiGraphics.drawString(this.font, title, titleX, terminalY + 8, BORDER_COLOR, false); - // Draw message history (adjusted for title spacing) - int messageY = terminalY + 24; + // Draw message history with proper margins + int borderThickness = 2; + int messageX = terminalX + borderThickness + 8; // Left margin inside border + int messageY = terminalY + 24; // Below title + int maxX = terminalX + terminalWidth - borderThickness - 8; // Right margin int maxY = terminalY + terminalHeight - 40; + int messageWidth = maxX - messageX; int lineHeight = this.font.lineHeight + 2; // Calculate how many lines can actually fit on screen - int availableHeight = maxY - (terminalY + 24); + int availableHeight = maxY - messageY; int maxVisibleLines = Math.max(1, availableHeight / lineHeight); int startIndex = Math.max(0, messageHistory.size() - maxVisibleLines - scrollOffset); int endIndex = Math.min(messageHistory.size(), startIndex + maxVisibleLines); + // Enable scissor (clipping) to prevent text overflow + guiGraphics.enableScissor(messageX, messageY, maxX, maxY); + for (int i = startIndex; i < endIndex; i++) { if (messageY >= maxY) break; FormattedLine line = messageHistory.get(i); - renderFormattedLine(guiGraphics, line, terminalX + 10, messageY); + renderFormattedLine(guiGraphics, line, messageX, messageY, messageWidth); messageY += lineHeight; } + // Disable scissor + guiGraphics.disableScissor(); + // Draw input prompt int inputY = terminalY + terminalHeight - 30; guiGraphics.drawString(this.font, ">", terminalX + 10, inputY + 6, PROMPT_COLOR, false); @@ -289,34 +299,33 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia // Render widgets (input field) super.render(guiGraphics, mouseX, mouseY, partialTick); - // Draw scroll indicator if needed (tan/brown color) + // Draw scroll indicator if needed (inside terminal, bottom right) if (messageHistory.size() > maxVisibleLines) { - String scrollInfo = String.format("[↑↓ to scroll | %d/%d]", + String scrollInfo = String.format("[↑↓ scroll %d/%d]", Math.max(0, messageHistory.size() - maxVisibleLines - scrollOffset), messageHistory.size() - maxVisibleLines); - // Position below the bottom border (border is at -10, text goes at -2) - guiGraphics.drawString(this.font, scrollInfo, terminalX + terminalWidth - this.font.width(scrollInfo) - 10, - terminalY + terminalHeight - 2, 0xFFffbf00, false); // Amber + // Position inside bottom border + int scrollX = terminalX + terminalWidth - this.font.width(scrollInfo) - 12; + int scrollY = terminalY + terminalHeight - 12; + guiGraphics.drawString(this.font, scrollInfo, scrollX, scrollY, 0xFFffbf00, false); // Amber } } private void drawTerminalBorder(GuiGraphics guiGraphics, int x, int y, int width, int height) { - int borderCharWidth = this.font.width("─"); - int sideCharWidth = this.font.width("│"); + int borderThickness = 2; + // Draw solid border rectangles // Top border - guiGraphics.drawString(this.font, "┌" + "─".repeat((width - 20) / borderCharWidth) + "┐", - x, y, BORDER_COLOR, false); + guiGraphics.fill(x, y, x + width, y + borderThickness, BORDER_COLOR); // Bottom border - guiGraphics.drawString(this.font, "└" + "─".repeat((width - 20) / borderCharWidth) + "┘", - x, y + height - 10, BORDER_COLOR, false); + guiGraphics.fill(x, y + height - borderThickness, x + width, y + height, BORDER_COLOR); - // Side borders (adjusted right side for proper alignment) - for (int i = 10; i < height - 10; i += this.font.lineHeight) { - guiGraphics.drawString(this.font, "│", x, y + i, BORDER_COLOR, false); - guiGraphics.drawString(this.font, "│", x + width - 9, y + i, BORDER_COLOR, false); - } + // Left border + guiGraphics.fill(x, y, x + borderThickness, y + height, BORDER_COLOR); + + // Right border + guiGraphics.fill(x + width - borderThickness, y, x + width, y + height, BORDER_COLOR); } @Override @@ -407,9 +416,9 @@ public void addMessage(String message, int color) { } /** - * Renders a formatted line with multiple colored segments + * Renders a formatted line with multiple colored segments, clipped to max width */ - private void renderFormattedLine(GuiGraphics guiGraphics, FormattedLine line, int x, int y) { + private void renderFormattedLine(GuiGraphics guiGraphics, FormattedLine line, int x, int y, int maxWidth) { int currentX = x; // Add indentation for code blocks and lists @@ -417,11 +426,26 @@ private void renderFormattedLine(GuiGraphics guiGraphics, FormattedLine line, in currentX += this.font.width(" "); } - // Draw each segment + // Draw each segment with boundary checking for (TextSegment segment : line.getSegments()) { String text = segment.getText(); int color = segment.getEffectiveColor(); + // Check if we have space left + int remainingWidth = (x + maxWidth) - currentX; + if (remainingWidth <= 0) break; + + // Truncate text if it's too long + int textWidth = this.font.width(text); + if (textWidth > remainingWidth) { + // Find how much of the text fits + String truncated = text; + while (this.font.width(truncated + "...") > remainingWidth && truncated.length() > 0) { + truncated = truncated.substring(0, truncated.length() - 1); + } + text = truncated + "..."; + } + guiGraphics.drawString(this.font, text, currentX, y, color, false); currentX += this.font.width(text); } diff --git a/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java b/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java index 4cd338b..5751962 100644 --- a/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java +++ b/src/main/java/com/opencode/minecraft/gui/markdown/MarkdownParser.java @@ -23,9 +23,16 @@ public class MarkdownParser { private static final Pattern LINK_PATTERN = Pattern.compile("\\[([^\\]]+?)\\]\\(([^\\)]+?)\\)"); /** - * Parse markdown text into formatted lines + * Parse markdown text into formatted lines with word wrapping */ public static List parse(String markdownText, int baseColor) { + return parse(markdownText, baseColor, 1200); // Default max width + } + + /** + * Parse markdown text into formatted lines with specified max width + */ + public static List parse(String markdownText, int baseColor, int maxWidth) { List lines = new ArrayList<>(); String[] rawLines = markdownText.split("\n"); @@ -41,23 +48,33 @@ public static List parse(String markdownText, int baseColor) { codeBlockLang = line.trim().substring(3).trim(); // Add a separator line FormattedLine separator = new FormattedLine(); - separator.addSegment("┌─[ " + (codeBlockLang.isEmpty() ? "code" : codeBlockLang) + " ]", COLOR_CODE); + separator.addSegment("[ " + (codeBlockLang.isEmpty() ? "code" : codeBlockLang) + " ]", COLOR_CODE); lines.add(separator); } else { // Ending code block inCodeBlock = false; FormattedLine separator = new FormattedLine(); - separator.addSegment("└" + "─".repeat(20), COLOR_CODE); + separator.addSegment("", COLOR_CODE); // Empty separator lines.add(separator); } continue; } if (inCodeBlock) { - // Code block line - preserve formatting, indent, use code color - FormattedLine codeLine = new FormattedLine(new ArrayList<>(), true, 1); - codeLine.addSegment(line, COLOR_CODE_BLOCK); - lines.add(codeLine); + // Code block line - wrap if too long + if (line.length() > 120) { // Wrap very long code lines + int chunkSize = 120; + for (int i = 0; i < line.length(); i += chunkSize) { + String chunk = line.substring(i, Math.min(i + chunkSize, line.length())); + FormattedLine codeLine = new FormattedLine(new ArrayList<>(), true, 1); + codeLine.addSegment(chunk, COLOR_CODE_BLOCK); + lines.add(codeLine); + } + } else { + FormattedLine codeLine = new FormattedLine(new ArrayList<>(), true, 1); + codeLine.addSegment(line, COLOR_CODE_BLOCK); + lines.add(codeLine); + } } else { // Regular line - parse inline markdown FormattedLine formattedLine = parseInlineMarkdown(line, baseColor); From 3c22105723df538ea0d3b115061e3e9b29d924fc Mon Sep 17 00:00:00 2001 From: "ciefa.eth" <156933735+ciefa@users.noreply.github.com> Date: Sun, 4 Jan 2026 13:50:36 +0100 Subject: [PATCH 7/7] Fix scroll indicator position and terminal layout Reverted terminal dimensions to balanced 40px margins after previous adjustment caused scroll indicator to appear outside borders. Changes: - Restored terminal margins to 40px (balanced layout) - Fixed scroll indicator position to Y-40 (above input field) - Scroll text now properly positioned inside terminal borders - Input field has proper spacing from bottom border The scroll indicator now appears above the input field in the bottom-right corner, well within the terminal boundaries. --- .../com/opencode/minecraft/gui/OpenCodeGuiScreen.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java index 3c2be28..696bdfe 100644 --- a/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java +++ b/src/main/java/com/opencode/minecraft/gui/OpenCodeGuiScreen.java @@ -238,9 +238,9 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia // Render dark background to prevent blur renderBackground(guiGraphics, mouseX, mouseY, partialTick); - // Calculate dimensions + // Calculate dimensions with proper margins int terminalWidth = this.width - 40; - int terminalHeight = this.height - 40; + int terminalHeight = this.height - 40; // Balanced margins int terminalX = 20; int terminalY = 20; @@ -299,14 +299,14 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partia // Render widgets (input field) super.render(guiGraphics, mouseX, mouseY, partialTick); - // Draw scroll indicator if needed (inside terminal, bottom right) + // Draw scroll indicator if needed (inside terminal, above input field) if (messageHistory.size() > maxVisibleLines) { String scrollInfo = String.format("[↑↓ scroll %d/%d]", Math.max(0, messageHistory.size() - maxVisibleLines - scrollOffset), messageHistory.size() - maxVisibleLines); - // Position inside bottom border + // Position above input field, bottom right int scrollX = terminalX + terminalWidth - this.font.width(scrollInfo) - 12; - int scrollY = terminalY + terminalHeight - 12; + int scrollY = terminalY + terminalHeight - 40; // Above input, inside border guiGraphics.drawString(this.font, scrollInfo, scrollX, scrollY, 0xFFffbf00, false); // Amber } }