-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathOpenCodeClient.java
More file actions
333 lines (298 loc) · 11.6 KB
/
OpenCodeClient.java
File metadata and controls
333 lines (298 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package com.opencode.minecraft.client;
import com.opencode.minecraft.OpenCodeMod;
import com.opencode.minecraft.client.http.OpenCodeHttpClient;
import com.opencode.minecraft.client.http.SseEvent;
import com.opencode.minecraft.client.session.SessionInfo;
import com.opencode.minecraft.client.session.SessionManager;
import com.opencode.minecraft.client.session.SessionStatus;
import com.opencode.minecraft.config.ModConfig;
import com.opencode.minecraft.game.MessageRenderer;
import com.opencode.minecraft.game.PauseController;
import net.minecraft.client.Minecraft;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Main coordinator for OpenCode client functionality.
* Manages HTTP client, session, and event handling.
*/
public class OpenCodeClient {
private final OpenCodeHttpClient httpClient;
private final SessionManager sessionManager;
private final PauseController pauseController;
private final MessageRenderer messageRenderer;
private final ModConfig config;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private volatile boolean initialized = false;
private volatile java.util.function.Consumer<String> guiMessageListener = null;
private volatile Runnable guiResponseCompleteListener = null;
public OpenCodeClient(ModConfig config, PauseController pauseController) {
this.config = config;
this.pauseController = pauseController;
this.httpClient = new OpenCodeHttpClient(config);
this.sessionManager = new SessionManager(httpClient);
this.messageRenderer = new MessageRenderer();
// Listen for session status changes
sessionManager.addStatusListener(this::onStatusChange);
// Set up response handler for streaming responses
httpClient.setResponseHandler(this::handleResponse);
// Start initialization
initialize();
}
private void initialize() {
// Check health and connect
scheduler.schedule(this::connect, 1, TimeUnit.SECONDS);
}
private void connect() {
httpClient.checkHealth()
.thenAccept(healthy -> {
if (healthy) {
OpenCodeMod.LOGGER.info("Connected to OpenCode server");
sessionManager.onConnected();
// Subscribe to events
httpClient.subscribeToEvents(this::handleEvent);
// Resume last session if available
if (config.lastSessionId != null) {
sessionManager.useSession(config.lastSessionId)
.exceptionally(e -> {
OpenCodeMod.LOGGER.debug("Could not resume session: {}", e.getMessage());
return null;
});
}
initialized = true;
} else {
scheduleReconnect();
}
})
.exceptionally(e -> {
OpenCodeMod.LOGGER.debug("Connection failed: {}", e.getMessage());
scheduleReconnect();
return null;
});
}
private void scheduleReconnect() {
if (config.autoReconnect) {
scheduler.schedule(this::connect, config.reconnectIntervalMs, TimeUnit.MILLISECONDS);
}
}
private void handleEvent(SseEvent event) {
// Dispatch to main thread
Minecraft.getInstance().execute(() -> {
switch (event.getType()) {
case "session.status" -> {
String statusType = event.getStatusType();
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...");
}
}
case "message.part.updated" -> {
handlePartUpdated(event);
}
case "message.created" -> {
// Don't clutter chat with message creation events
// messageRenderer.startNewMessage();
}
case "session.error" -> {
messageRenderer.sendErrorMessage("Session error occurred");
}
case "server.connected" -> {
messageRenderer.sendSystemMessage("Connected to OpenCode");
}
case "server.heartbeat" -> {
// Ignore heartbeats
}
default -> {
// Silently ignore other events
}
}
});
}
private void handlePartUpdated(SseEvent event) {
String partType = event.getPartType();
if (partType == null) return;
switch (partType) {
case "text" -> {
// Text output with delta
if (event.hasDelta()) {
sessionManager.onDeltaReceived();
pauseController.onDeltaReceived();
String delta = event.getDelta();
if (delta != null && !delta.isEmpty()) {
// 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);
}
}
}
}
case "tool" -> {
// Tool invocation
String toolName = event.getToolName();
String toolState = event.getToolState();
if (toolName != null && toolState != null) {
messageRenderer.sendToolMessage(toolName, toolState);
}
}
case "step-start" -> {
// Step started
String title = event.getStepTitle();
if (title != null) {
messageRenderer.sendSystemMessage("Step: " + title);
}
}
case "file" -> {
// File operation
String filePath = event.getFilePath();
if (filePath != null) {
// Just show filename, not full path
String fileName = filePath.contains("/")
? filePath.substring(filePath.lastIndexOf('/') + 1)
: filePath;
messageRenderer.sendSystemMessage("File: " + fileName);
}
}
case "reasoning" -> {
// LLM is thinking - show indicator but not content
if (event.hasDelta()) {
sessionManager.onDeltaReceived();
pauseController.onDeltaReceived();
// Don't show reasoning content, just indicate thinking
}
}
default -> {
// Other part types - just ensure we track activity
if (event.hasDelta()) {
sessionManager.onDeltaReceived();
pauseController.onDeltaReceived();
}
}
}
}
private void handleResponse(String line) {
// Handle streaming JSON response from prompt
// The SSE events will handle the actual content
}
private void onStatusChange(SessionStatus status) {
pauseController.setStatus(status);
}
/**
* Creates a new session
*/
public CompletableFuture<SessionInfo> createSession() {
return sessionManager.createSession()
.thenApply(session -> {
OpenCodeMod.getConfigManager().setLastSessionId(session.getId());
return session;
});
}
/**
* Lists all sessions
*/
public CompletableFuture<List<SessionInfo>> listSessions() {
return sessionManager.listSessions();
}
/**
* Switches to an existing session
*/
public CompletableFuture<SessionInfo> useSession(String sessionId) {
return sessionManager.useSession(sessionId)
.thenApply(session -> {
OpenCodeMod.getConfigManager().setLastSessionId(session.getId());
return session;
});
}
/**
* Sends a prompt to the current session via the TUI.
* Response will come through SSE events.
*/
public CompletableFuture<Void> sendPrompt(String text) {
pauseController.setUserTyping(false);
pauseController.setStatus(SessionStatus.BUSY);
messageRenderer.addUserMessage(text);
return sessionManager.sendPrompt(text)
.thenAccept(response -> {
// Prompt was sent to TUI, response will come via SSE
Minecraft.getInstance().execute(() -> {
if (response != null && response.startsWith("Error:")) {
messageRenderer.sendErrorMessage(response);
pauseController.setStatus(SessionStatus.IDLE);
}
// Otherwise, wait for SSE events to deliver the response
});
});
}
/**
* Cancels the current generation
*/
public CompletableFuture<Void> cancel() {
return sessionManager.cancel();
}
/**
* Gets the current session
*/
public SessionInfo getCurrentSession() {
return sessionManager.getCurrentSession();
}
/**
* Gets the current status
*/
public SessionStatus getStatus() {
return sessionManager.getStatus();
}
/**
* Gets the message history for a session
*/
public CompletableFuture<com.google.gson.JsonArray> getSessionMessages(String sessionId) {
return httpClient.getSessionMessages(sessionId);
}
/**
* Sets a listener for real-time message updates (for GUI)
*/
public void setGuiMessageListener(java.util.function.Consumer<String> 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
*/
public boolean isReady() {
return initialized && httpClient.isConnected();
}
/**
* Ticks the session manager for status timeout checks
*/
public void tick() {
sessionManager.tick();
}
/**
* Shuts down the client
*/
public void shutdown() {
scheduler.shutdown();
httpClient.shutdown();
}
}