From 368125a699f94b9732c21dec15c516dd71953820 Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Wed, 18 Dec 2024 11:12:35 +0800 Subject: [PATCH] refactor(eclipse): update tabby-chat-panel api to 0.4.0. --- clients/eclipse/.gitignore | 1 + clients/eclipse/feature/feature.xml | 4 +- clients/eclipse/package.json | 3 +- clients/eclipse/plugin/META-INF/MANIFEST.MF | 2 +- clients/eclipse/plugin/chat-panel/index.html | 44 +- .../tabbyml/tabby4eclipse/chat/ChatView.java | 426 ++++++++++++------ .../tabby4eclipse/chat/ChatViewUtils.java | 2 +- .../tabbyml/tabby4eclipse/chat/Request.java | 29 -- clients/eclipse/scripts/copy-dependencies.js | 34 ++ clients/eclipse/scripts/copy-tabby-agent.js | 22 - pnpm-lock.yaml | 3 + 11 files changed, 336 insertions(+), 234 deletions(-) delete mode 100644 clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Request.java create mode 100644 clients/eclipse/scripts/copy-dependencies.js delete mode 100644 clients/eclipse/scripts/copy-tabby-agent.js diff --git a/clients/eclipse/.gitignore b/clients/eclipse/.gitignore index 5565249acfcc..321d5a8fee6b 100644 --- a/clients/eclipse/.gitignore +++ b/clients/eclipse/.gitignore @@ -1,3 +1,4 @@ dist node_modules plugin/tabby-agent +plugin/chat-panel/create-thread-from-iframe.js diff --git a/clients/eclipse/feature/feature.xml b/clients/eclipse/feature/feature.xml index f6c3bc62f7f4..5d0505a199b8 100644 --- a/clients/eclipse/feature/feature.xml +++ b/clients/eclipse/feature/feature.xml @@ -2,7 +2,7 @@ @@ -19,6 +19,6 @@ + version="0.0.2.30"/> diff --git a/clients/eclipse/package.json b/clients/eclipse/package.json index a2809d861144..315ce8b5f747 100644 --- a/clients/eclipse/package.json +++ b/clients/eclipse/package.json @@ -2,10 +2,11 @@ "name": "tabby4eclipse", "private": true, "scripts": { - "build": "node scripts/copy-tabby-agent.js" + "build": "node scripts/copy-dependencies.js" }, "devDependencies": { "tabby-agent": "workspace:*", + "tabby-threads": "workspace:*", "fs-extra": "^11.1.1" } } diff --git a/clients/eclipse/plugin/META-INF/MANIFEST.MF b/clients/eclipse/plugin/META-INF/MANIFEST.MF index 24033e174cb9..91fa845d4d87 100644 --- a/clients/eclipse/plugin/META-INF/MANIFEST.MF +++ b/clients/eclipse/plugin/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Tabby Plugin for Eclipse Bundle-SymbolicName: com.tabbyml.tabby4eclipse;singleton:=true -Bundle-Version: 0.0.2.29 +Bundle-Version: 0.0.2.30 Bundle-Activator: com.tabbyml.tabby4eclipse.Activator Bundle-Vendor: com.tabbyml Require-Bundle: org.eclipse.ui, diff --git a/clients/eclipse/plugin/chat-panel/index.html b/clients/eclipse/plugin/chat-panel/index.html index 976acb59d0ad..919cd80d8a76 100644 --- a/clients/eclipse/plugin/chat-panel/index.html +++ b/clients/eclipse/plugin/chat-panel/index.html @@ -58,6 +58,7 @@

Welcome to Tabby Chat

+ diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatView.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatView.java index 6103c83edffe..1e61864d10d0 100644 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatView.java +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatView.java @@ -5,6 +5,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.Path; @@ -54,9 +56,11 @@ public class ChatView extends ViewPart { private boolean isHtmlLoaded = false; private boolean isChatPanelLoaded = false; - private List pendingScripts = new ArrayList<>(); private Config.ServerConfig currentConfig; + private List pendingScripts = new ArrayList<>(); + private Map> pendingChatPanelRequest = new HashMap<>(); + private boolean isDark; private RGB bgColor; private RGB bgActiveColor; @@ -81,27 +85,8 @@ public void completed(ProgressEvent event) { handleLoaded(); } }); - // Inject callbacks - browserFunctions.add(new BrowserFunction(browser, "handleReload") { - @Override - public Object function(Object[] arguments) { - reloadContent(true); - return null; - } - }); - - browserFunctions.add(new BrowserFunction(browser, "handleChatPanelRequest") { - @Override - public Object function(Object[] arguments) { - if (arguments.length > 0) { - logger.info("HandleChatPanelRequest: " + arguments[0]); - Request request = gson.fromJson(arguments[0].toString(), Request.class); - handleChatPanelRequest(request); - } - return null; - } - }); - + + injectFunctions(); load(); serverConfigHolder.addConfigDidChangeListener(() -> { reloadContent(false); @@ -129,64 +114,64 @@ public void dispose() { } public void explainSelectedText() { - sendRequestToChatPanel(new Request("sendMessage", new ArrayList<>() { + chatPanelClientInvoke("sendMessage", new ArrayList<>() { { ChatMessage chatMessage = new ChatMessage(); chatMessage.setMessage(ChatViewUtils.PROMPT_EXPLAIN); chatMessage.setSelectContext(ChatViewUtils.getSelectedTextAsFileContext()); add(chatMessage); } - })); + }); } public void fixSelectedText() { // FIXME(@icycodes): collect the diagnostic message provided by IDE or LSP - sendRequestToChatPanel(new Request("sendMessage", new ArrayList<>() { + chatPanelClientInvoke("sendMessage", new ArrayList<>() { { ChatMessage chatMessage = new ChatMessage(); chatMessage.setMessage(ChatViewUtils.PROMPT_FIX); chatMessage.setSelectContext(ChatViewUtils.getSelectedTextAsFileContext()); add(chatMessage); } - })); + }); } public void generateDocsForSelectedText() { - sendRequestToChatPanel(new Request("sendMessage", new ArrayList<>() { + chatPanelClientInvoke("sendMessage", new ArrayList<>() { { ChatMessage chatMessage = new ChatMessage(); chatMessage.setMessage(ChatViewUtils.PROMPT_GENERATE_DOCS); chatMessage.setSelectContext(ChatViewUtils.getSelectedTextAsFileContext()); add(chatMessage); } - })); + }); } public void generateTestsForSelectedText() { - sendRequestToChatPanel(new Request("sendMessage", new ArrayList<>() { + chatPanelClientInvoke("sendMessage", new ArrayList<>() { { ChatMessage chatMessage = new ChatMessage(); chatMessage.setMessage(ChatViewUtils.PROMPT_GENERATE_TESTS); chatMessage.setSelectContext(ChatViewUtils.getSelectedTextAsFileContext()); add(chatMessage); } - })); + }); } public void addSelectedTextAsContext() { - sendRequestToChatPanel(new Request("addRelevantContext", new ArrayList<>() { + chatPanelClientInvoke("addRelevantContext", new ArrayList<>() { { add(ChatViewUtils.getSelectedTextAsFileContext()); } - })); + }); } public void addActiveEditorAsContext() { - sendRequestToChatPanel(new Request("addRelevantContext", new ArrayList<>() { + chatPanelClientInvoke("addRelevantContext", new ArrayList<>() { { add(ChatViewUtils.getActiveEditorAsFileContext()); } - })); + }); } private void setupThemeStyle() { @@ -231,6 +216,167 @@ private String buildCss() { return css; } + private List parseArguments(final Object[] arguments) { + if (arguments.length < 1) { + return List.of(); + } + return gson.fromJson(arguments[0].toString(), new TypeToken>(){}); + } + + private Object serializeResult(final Object result) { + return gson.toJson(result); + } + + private void injectFunctions() { + browserFunctions.add(new BrowserFunction(browser, "handleTabbyChatPanelResponse") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + logger.debug("Response from chat panel: " + params); + if (params.size() < 3) { + return null; + } + String uuid = (String) params.get(0); + String errorMessage = (String) params.get(1); + Object result = params.get(2); + + CompletableFuture future = pendingChatPanelRequest.remove(uuid); + if (future == null) { + return null; + } + + if (errorMessage != null && !errorMessage.isEmpty()) { + future.completeExceptionally(new Exception(errorMessage)); + } else { + future.complete(result); + } + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "handleReload") { + @Override + public Object function(Object[] arguments) { + logger.debug("handleReload"); + reloadContent(true); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelNavigate") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + logger.debug("tabbyChatPanelNavigate: " + params); + if (params.size() < 1) { + return null; + } + FileContext context = gson.fromJson(gson.toJson(params.get(0)), FileContext.class); + ChatViewUtils.navigateToFileContext(context); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelRefresh") { + @Override + public Object function(Object[] arguments) { + logger.debug("tabbyChatPanelRefresh"); + reloadContent(true); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOnSubmitMessage") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + if (params.size() < 1) { + return null; + } + String message = (String) params.get(0); + List relevantContexts = params.size() > 1 + ? relevantContexts = gson.fromJson(gson.toJson(params.get(1)), new TypeToken>() { + }.getType()) + : null; + chatPanelClientInvoke("sendMessage", new ArrayList<>() { + { + ChatMessage chatMessage = new ChatMessage(); + chatMessage.setMessage(message); + chatMessage.setRelevantContext(relevantContexts); + chatMessage.setActiveContext(ChatViewUtils.getSelectedTextAsFileContext()); + add(chatMessage); + } + }); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOnApplyInEditor") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + if (params.size() < 1) { + return null; + } + String content = (String) params.get(0); + ChatViewUtils.applyContentInEditor(content); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOnLoaded") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + if (params.size() < 1) { + return null; + } + Map onLoadedParams = (Map) params.get(0); + String apiVersion = (String) onLoadedParams.getOrDefault("apiVersion", ""); + if (!apiVersion.isBlank()) { + String error = ChatViewUtils.checkChatPanelApiVersion(apiVersion); + if (error != null) { + updateContentToMessage(error); + return null; + } + } + initChatPanel(); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOnCopy") { + @Override + public Object function(Object[] arguments) { + List params = parseArguments(arguments); + if (params.size() < 1) { + return null; + } + String content = (String) params.get(0); + ChatViewUtils.setClipboardContent(content); + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOnKeyboardEvent") { + @Override + public Object function(Object[] arguments) { + // FIXME: For macOS and windows, the eclipse keyboard shortcuts are not + // available when browser is focused, + // we should handle keyboard events here. + return null; + } + }); + + browserFunctions.add(new BrowserFunction(browser, "tabbyChatPanelOpenInEditor") { + @Override + public Object function(Object[] arguments) { + // FIXME: Not implemented + return serializeResult(false); + } + }); + } + private void load() { try { // Find chat panel html file @@ -254,6 +400,7 @@ private void handleLoaded() { isHtmlLoaded = true; isChatPanelLoaded = false; applyStyle(); + createChatPanelClient(); reloadContent(false); } @@ -315,6 +462,15 @@ private void updateContentToChatPanel() { showChatPanel(true); } + + // execute js functions + + private void executeScript(String script) { + browser.getDisplay().asyncExec(() -> { + browser.execute(script); + }); + } + private void showMessage(String message) { if (message != null) { executeScript(String.format("showMessage('%s')", message)); @@ -345,105 +501,9 @@ private void applyStyle() { browser.setVisible(true); } - private void sendRequestToChatPanel(Request request) { - String json = gson.toJson(request); - String script = String.format("sendRequestToChatPanel('%s')", StringUtils.escapeCharacters(json)); - if (isChatPanelLoaded) { - executeScript(script); - } else { - pendingScripts.add(script); - } - } - - private void executeScript(String script) { - browser.getDisplay().asyncExec(() -> { - browser.execute(script); - }); - } - - private void handleChatPanelRequest(Request request) { - switch (request.getMethod()) { - case "navigate": { - List params = request.getParams(); - if (params.size() < 1) { - return; - } - FileContext context = gson.fromJson(gson.toJson(params.get(0)), FileContext.class); - ChatViewUtils.navigateToFileContext(context); - break; - } - case "refresh": { - reloadContent(true); - break; - } - case "onSubmitMessage": { - List params = request.getParams(); - if (params.size() < 1) { - return; - } - String message = (String) params.get(0); - List relevantContexts = params.size() > 1 - ? relevantContexts = gson.fromJson(gson.toJson(params.get(1)), new TypeToken>() { - }.getType()) - : null; - sendRequestToChatPanel(new Request("sendMessage", new ArrayList<>() { - { - ChatMessage chatMessage = new ChatMessage(); - chatMessage.setMessage(message); - chatMessage.setRelevantContext(relevantContexts); - chatMessage.setActiveContext(ChatViewUtils.getSelectedTextAsFileContext()); - add(chatMessage); - } - })); - break; - } - case "onApplyInEditor": { - List params = request.getParams(); - if (params.size() < 1) { - return; - } - String content = (String) params.get(0); - ChatViewUtils.applyContentInEditor(content); - break; - } - case "onLoaded": { - List params = request.getParams(); - if (params.size() < 1) { - return; - } - Map onLoadedParams = (Map) params.get(0); - String apiVersion = (String) onLoadedParams.getOrDefault("apiVersion", ""); - if (!apiVersion.isBlank()) { - String error = ChatViewUtils.checkChatPanelApiVersion(apiVersion); - if (error != null) { - updateContentToMessage(error); - return; - } - } - initChatPanel(); - break; - } - case "onCopy": { - List params = request.getParams(); - if (params.size() < 1) { - return; - } - String content = (String) params.get(0); - ChatViewUtils.setClipboardContent(content); - break; - } - case "onKeyboardEvent": { - // FIXME: For macOS and windows, the eclipse keyboard shortcuts are not - // available when browser is focused, - // we should handle keyboard events here. - break; - } - } - } - private void initChatPanel() { isChatPanelLoaded = true; - sendRequestToChatPanel(new Request("init", new ArrayList<>() { + chatPanelClientInvoke("init", new ArrayList<>() { { add(new HashMap<>() { { @@ -455,13 +515,13 @@ private void initChatPanel() { } }); } - })); - sendRequestToChatPanel(new Request("updateTheme", new ArrayList<>() { + }); + chatPanelClientInvoke("updateTheme", new ArrayList<>() { { add(buildCss()); add(isDark ? "dark" : "light"); } - })); + }); browser.getDisplay().timerExec(100, () -> { updateContentToChatPanel(); pendingScripts.forEach((script) -> { @@ -470,5 +530,101 @@ private void initChatPanel() { pendingScripts.clear(); }); } - + + private String wrapJsFunction(String name) { + return String.format( + String.join("\n", + "function(...args) {", + " return new Promise((resolve, reject) => {", + " const paramsJson = JSON.stringify(args)", + " const result = %s(paramsJson)", + " resolve(result)", + " });", + "}" + ), + name + ); + } + + private void createChatPanelClient() { + String script = String.format( + String.join("\n", + "if (!window.tabbyChatPanelClient) {", + " window.tabbyChatPanelClient = TabbyThreads.createThreadFromIframe(getChatPanel(), {", + " expose: {", + " navigate: %s,", + " refresh: %s,", + " onSubmitMessage: %s,", + " onApplyInEditor: %s,", + " onLoaded: %s,", + " onCopy: %s,", + " onKeyboardEvent: %s,", + " openInEditor: %s,", + " }", + " })", + "}" + ), + wrapJsFunction("tabbyChatPanelNavigate"), + wrapJsFunction("tabbyChatPanelRefresh"), + wrapJsFunction("tabbyChatPanelOnSubmitMessage"), + wrapJsFunction("tabbyChatPanelOnApplyInEditor"), + wrapJsFunction("tabbyChatPanelOnLoaded"), + wrapJsFunction("tabbyChatPanelOnCopy"), + wrapJsFunction("tabbyChatPanelOnKeyboardEvent"), + wrapJsFunction("tabbyChatPanelOpenInEditor") + ); + executeScript(script); + } + + private CompletableFuture chatPanelClientInvoke(String method, List params) { + CompletableFuture future = new CompletableFuture<>(); + String uuid = UUID.randomUUID().toString(); + pendingChatPanelRequest.put(uuid, future); + String paramsJson = StringUtils.escapeCharacters(gson.toJson(params)); + String responseCallbackFunction = "handleTabbyChatPanelResponse(results)"; + String script = String.format( + String.join("\n", + "(function() {", + " const func = window.tabbyChatPanelClient['%s']", + " if (func && typeof func === 'function') {", + " const params = JSON.parse('%s')", + " const resultPromise = func(...params)", + " if (resultPromise && typeof resultPromise.then === 'function') {", + " resultPromise.then(result => {", + " const results = JSON.stringify(['%s', null, result])", + " %s", + " }).catch(error => {", + " const results = JSON.stringify(['%s', error.message, null])", + " %s", + " })", + " } else {", + " const results = JSON.stringify(['%s', null, resultPromise])", + " %s", + " }", + " } else {", + " const results = JSON.stringify(['%s', 'Method not found: %s', null])", + " %s", + " }", + "})()" + ), + method, + paramsJson, + uuid, + responseCallbackFunction, + uuid, + responseCallbackFunction, + uuid, + responseCallbackFunction, + uuid, + method, + responseCallbackFunction + ); + logger.debug("Request to chat panel: " + uuid + ", " + method + ", " + paramsJson); + if (isChatPanelLoaded) { + executeScript(script); + } else { + pendingScripts.add(script); + } + return future; + } } diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java index 1d628192b3c2..8b9197a92f60 100644 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java +++ b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/ChatViewUtils.java @@ -33,7 +33,7 @@ public class ChatViewUtils { private static final String ID = "com.tabbyml.tabby4eclipse.views.chat"; private static final String MIN_SERVER_VERSION = "0.18.0"; - private static final String CHAT_PANEL_API_VERSION = "0.2.0"; + private static final String CHAT_PANEL_API_VERSION = "0.4.0"; private static Logger logger = new Logger("ChatView"); public static final String PROMPT_EXPLAIN = "Explain the selected code:"; diff --git a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Request.java b/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Request.java deleted file mode 100644 index c7bf057418af..000000000000 --- a/clients/eclipse/plugin/src/com/tabbyml/tabby4eclipse/chat/Request.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.tabbyml.tabby4eclipse.chat; - -import java.util.List; - -public class Request { - private String method; - private List params; - - public Request(String method, List params) { - this.method = method; - this.params = params; - } - - public String getMethod() { - return method; - } - - public void setMethod(String method) { - this.method = method; - } - - public List getParams() { - return params; - } - - public void setParams(List params) { - this.params = params; - } -} diff --git a/clients/eclipse/scripts/copy-dependencies.js b/clients/eclipse/scripts/copy-dependencies.js new file mode 100644 index 000000000000..45bc598b77a8 --- /dev/null +++ b/clients/eclipse/scripts/copy-dependencies.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node + +const fs = require('fs-extra'); +const path = require('path'); + +const cwd = process.cwd(); + +async function copyTabbyAgentScript() { + const sourceDir = path.join(cwd, 'node_modules', 'tabby-agent', 'dist', 'node'); + const targetDir = path.join(cwd, 'plugin', 'tabby-agent', 'dist', 'node'); + try { + await fs.emptyDir(targetDir); + await fs.copy(sourceDir, targetDir, { + filter: (src) => !src.endsWith('.js.map') + }); + console.log(`✅ Files copied: ${sourceDir} -> ${targetDir}`); + } catch (err) { + console.error('❌ Error copying files:', err); + } +} + +async function copyTabbyThreadsScript() { + const sourceFile = path.join(cwd, 'node_modules', 'tabby-threads', 'dist', 'iife', 'create-thread-from-iframe.js'); + const targetFile = path.join(cwd, 'plugin', 'chat-panel', 'create-thread-from-iframe.js'); + try { + await fs.copy(sourceFile, targetFile); + console.log(`✅ Files copied: ${sourceFile} -> ${targetFile}`); + } catch (err) { + console.error('❌ Error copying files:', err); + } +} + +copyTabbyAgentScript(); +copyTabbyThreadsScript(); diff --git a/clients/eclipse/scripts/copy-tabby-agent.js b/clients/eclipse/scripts/copy-tabby-agent.js deleted file mode 100644 index bc5ada12e462..000000000000 --- a/clients/eclipse/scripts/copy-tabby-agent.js +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs-extra'); -const path = require('path'); - -const cwd = process.cwd(); -const sourceDir = path.join(cwd, 'node_modules', 'tabby-agent', 'dist', 'node'); -const targetDir = path.join(cwd, 'plugin', 'tabby-agent', 'dist', 'node'); - -async function copyFiles() { - try { - await fs.emptyDir(targetDir); - await fs.copy(sourceDir, targetDir, { - filter: (src) => !src.endsWith('.js.map') - }); - console.log('✅ Files copied: node_modules/tabby-agent/dist/node -> plugin/tabby-agent/dist/node'); - } catch (err) { - console.error('❌ Error copying files:', err); - } -} - -copyFiles(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68e8671f0d11..94d67021249f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: tabby-agent: specifier: workspace:* version: link:../tabby-agent + tabby-threads: + specifier: workspace:* + version: link:../tabby-threads clients/intellij: devDependencies: