- LSPTool: goto_definition, find_references, hover, diagnostics, document_symbols Wraps LSPServerManager.sendRequest() with JsonNode response formatting - BriefTool: enable/disable/toggle/status for brief output mode via ToolContext - NotificationTool: cross-platform notifications (Windows toast, macOS osascript, Linux notify-send) with terminal bell fallback - Registered all 3 in AppConfig (total: 31 tools) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>pull/1/head
parent
12557f23f0
commit
d65d63038f
@ -0,0 +1,105 @@ |
||||
package com.claudecode.tool.impl; |
||||
|
||||
import com.claudecode.tool.Tool; |
||||
import com.claudecode.tool.ToolContext; |
||||
|
||||
import java.util.Map; |
||||
import java.util.concurrent.atomic.AtomicBoolean; |
||||
import java.util.concurrent.atomic.AtomicReference; |
||||
|
||||
/** |
||||
* Brief 工具 —— 对应 claude-code 中 brief/detailed 输出模式切换。 |
||||
* <p> |
||||
* 控制 Agent 输出的详细程度。开启 brief 模式后,Agent 会: |
||||
* <ul> |
||||
* <li>使用更简洁的回复</li> |
||||
* <li>省略冗余的解释</li> |
||||
* <li>只显示关键信息</li> |
||||
* </ul> |
||||
* <p> |
||||
* 模式状态通过 ToolContext 共享(key: "BRIEF_MODE")。 |
||||
*/ |
||||
public class BriefTool implements Tool { |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "Brief"; |
||||
} |
||||
|
||||
@Override |
||||
public String description() { |
||||
return """ |
||||
Toggle brief output mode. When enabled, responses are concise and focused. \ |
||||
When disabled, responses include full explanations and context. |
||||
|
||||
Actions: |
||||
- enable: Turn on brief mode (shorter responses) |
||||
- disable: Turn off brief mode (detailed responses) |
||||
- toggle: Switch between brief and detailed |
||||
- status: Check current mode"""; |
||||
} |
||||
|
||||
@Override |
||||
public String inputSchema() { |
||||
return """ |
||||
{ |
||||
"type": "object", |
||||
"properties": { |
||||
"action": { |
||||
"type": "string", |
||||
"enum": ["enable", "disable", "toggle", "status"], |
||||
"description": "Action to perform" |
||||
} |
||||
}, |
||||
"required": ["action"] |
||||
}"""; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isReadOnly() { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public String execute(Map<String, Object> input, ToolContext context) { |
||||
String action = (String) input.get("action"); |
||||
if (action == null) { |
||||
return "Error: 'action' is required"; |
||||
} |
||||
|
||||
boolean currentMode = isBriefMode(context); |
||||
|
||||
return switch (action) { |
||||
case "enable" -> { |
||||
context.set("BRIEF_MODE", true); |
||||
yield "Brief mode enabled. Responses will be concise."; |
||||
} |
||||
case "disable" -> { |
||||
context.set("BRIEF_MODE", false); |
||||
yield "Brief mode disabled. Responses will be detailed."; |
||||
} |
||||
case "toggle" -> { |
||||
boolean newMode = !currentMode; |
||||
context.set("BRIEF_MODE", newMode); |
||||
yield newMode ? "Brief mode enabled." : "Brief mode disabled."; |
||||
} |
||||
case "status" -> "Brief mode: " + (currentMode ? "ON (concise)" : "OFF (detailed)"); |
||||
default -> "Error: Unknown action: " + action; |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* 检查当前是否为 brief 模式。 |
||||
*/ |
||||
public static boolean isBriefMode(ToolContext context) { |
||||
Object mode = context.get("BRIEF_MODE"); |
||||
if (mode instanceof Boolean b) return b; |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public String activityDescription(Map<String, Object> input) { |
||||
String action = (String) input.getOrDefault("action", "toggle"); |
||||
return "📋 Brief mode " + action; |
||||
} |
||||
} |
||||
@ -0,0 +1,310 @@ |
||||
package com.claudecode.tool.impl; |
||||
|
||||
import com.claudecode.lsp.LSPDiagnosticRegistry; |
||||
import com.claudecode.lsp.LSPServerManager; |
||||
import com.claudecode.tool.Tool; |
||||
import com.claudecode.tool.ToolContext; |
||||
import com.fasterxml.jackson.databind.JsonNode; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
import java.nio.file.Path; |
||||
import java.util.*; |
||||
|
||||
/** |
||||
* LSP 工具 —— 对应 claude-code 中通过 LSP 提供的代码智能功能。 |
||||
* <p> |
||||
* 暴露 Language Server Protocol 能力给 Agent: |
||||
* <ul> |
||||
* <li>goto_definition — 跳转到定义</li> |
||||
* <li>find_references — 查找引用</li> |
||||
* <li>hover — 获取悬停信息(类型、文档)</li> |
||||
* <li>diagnostics — 获取文件诊断信息</li> |
||||
* <li>document_symbols — 获取文件符号列表</li> |
||||
* </ul> |
||||
* <p> |
||||
* 底层使用 Phase 3C 实现的 LSPServerManager 和 LSPClient。 |
||||
*/ |
||||
public class LSPTool implements Tool { |
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LSPTool.class); |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "LSP"; |
||||
} |
||||
|
||||
@Override |
||||
public String description() { |
||||
return """ |
||||
Query Language Server Protocol for code intelligence. Provides: |
||||
- goto_definition: Jump to where a symbol is defined |
||||
- find_references: Find all usages of a symbol |
||||
- hover: Get type info and documentation for a symbol |
||||
- diagnostics: Get errors/warnings for a file |
||||
- document_symbols: List all symbols in a file |
||||
|
||||
Use this when you need to understand code structure, find symbol definitions, \ |
||||
or check for compile errors. Requires an LSP server configured for the file type."""; |
||||
} |
||||
|
||||
@Override |
||||
public String inputSchema() { |
||||
return """ |
||||
{ |
||||
"type": "object", |
||||
"properties": { |
||||
"action": { |
||||
"type": "string", |
||||
"enum": ["goto_definition", "find_references", "hover", "diagnostics", "document_symbols"], |
||||
"description": "The LSP action to perform" |
||||
}, |
||||
"file_path": { |
||||
"type": "string", |
||||
"description": "Absolute path to the source file" |
||||
}, |
||||
"line": { |
||||
"type": "integer", |
||||
"description": "1-based line number (required for goto_definition, find_references, hover)" |
||||
}, |
||||
"column": { |
||||
"type": "integer", |
||||
"description": "1-based column number (required for goto_definition, find_references, hover)" |
||||
} |
||||
}, |
||||
"required": ["action", "file_path"] |
||||
}"""; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isReadOnly() { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public String execute(Map<String, Object> input, ToolContext context) { |
||||
String action = (String) input.get("action"); |
||||
String filePath = (String) input.get("file_path"); |
||||
Number lineNum = (Number) input.get("line"); |
||||
Number colNum = (Number) input.get("column"); |
||||
|
||||
if (action == null || filePath == null) { |
||||
return "Error: 'action' and 'file_path' are required"; |
||||
} |
||||
|
||||
// Get LSP manager from context
|
||||
Object managerObj = context.get("LSP_SERVER_MANAGER"); |
||||
if (!(managerObj instanceof LSPServerManager manager)) { |
||||
return "Error: LSP server manager not available. No language server configured."; |
||||
} |
||||
|
||||
Path path = Path.of(filePath); |
||||
int line = lineNum != null ? lineNum.intValue() : 1; |
||||
int column = colNum != null ? colNum.intValue() : 1; |
||||
// Convert to 0-based for LSP protocol
|
||||
int lspLine = line - 1; |
||||
int lspCol = column - 1; |
||||
|
||||
return switch (action) { |
||||
case "goto_definition" -> gotoDefinition(manager, path, lspLine, lspCol); |
||||
case "find_references" -> findReferences(manager, path, lspLine, lspCol); |
||||
case "hover" -> hover(manager, path, lspLine, lspCol); |
||||
case "diagnostics" -> getDiagnostics(context, path); |
||||
case "document_symbols" -> documentSymbols(manager, path); |
||||
default -> "Error: Unknown action: " + action; |
||||
}; |
||||
} |
||||
|
||||
private String gotoDefinition(LSPServerManager manager, Path file, int line, int col) { |
||||
try { |
||||
Map<String, Object> params = textDocumentPositionParams(file, line, col); |
||||
JsonNode result = manager.sendRequest(file.toString(), "textDocument/definition", params); |
||||
if (result == null || result.isNull()) return "No definition found"; |
||||
return formatLocations("Definition", result); |
||||
} catch (Exception e) { |
||||
return "Error: " + e.getMessage(); |
||||
} |
||||
} |
||||
|
||||
private String findReferences(LSPServerManager manager, Path file, int line, int col) { |
||||
try { |
||||
Map<String, Object> params = textDocumentPositionParams(file, line, col); |
||||
params.put("context", Map.of("includeDeclaration", true)); |
||||
JsonNode result = manager.sendRequest(file.toString(), "textDocument/references", params); |
||||
if (result == null || result.isNull()) return "No references found"; |
||||
return formatLocations("References", result); |
||||
} catch (Exception e) { |
||||
return "Error: " + e.getMessage(); |
||||
} |
||||
} |
||||
|
||||
private String hover(LSPServerManager manager, Path file, int line, int col) { |
||||
try { |
||||
Map<String, Object> params = textDocumentPositionParams(file, line, col); |
||||
JsonNode result = manager.sendRequest(file.toString(), "textDocument/hover", params); |
||||
if (result == null || result.isNull()) return "No hover information available"; |
||||
return formatHover(result); |
||||
} catch (Exception e) { |
||||
return "Error: " + e.getMessage(); |
||||
} |
||||
} |
||||
|
||||
private String getDiagnostics(ToolContext context, Path file) { |
||||
Object regObj = context.get("LSP_DIAGNOSTIC_REGISTRY"); |
||||
if (regObj instanceof LSPDiagnosticRegistry registry) { |
||||
var files = registry.checkAndExtract(); |
||||
if (files.isEmpty()) { |
||||
return "No diagnostics for " + file.getFileName(); |
||||
} |
||||
String formatted = LSPDiagnosticRegistry.formatForContext(files); |
||||
return formatted.isEmpty() ? "No diagnostics" : formatted; |
||||
} |
||||
return "Diagnostic registry not available"; |
||||
} |
||||
|
||||
private String documentSymbols(LSPServerManager manager, Path file) { |
||||
try { |
||||
Map<String, Object> params = Map.of( |
||||
"textDocument", Map.of("uri", file.toUri().toString())); |
||||
JsonNode result = manager.sendRequest(file.toString(), "textDocument/documentSymbol", params); |
||||
if (result == null || result.isNull()) return "No symbols found"; |
||||
return formatSymbols(result); |
||||
} catch (Exception e) { |
||||
return "Error: " + e.getMessage(); |
||||
} |
||||
} |
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private Map<String, Object> textDocumentPositionParams(Path file, int line, int col) { |
||||
Map<String, Object> params = new LinkedHashMap<>(); |
||||
params.put("textDocument", Map.of("uri", file.toUri().toString())); |
||||
params.put("position", Map.of("line", line, "character", col)); |
||||
return params; |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private String formatLocations(String label, JsonNode result) { |
||||
StringBuilder sb = new StringBuilder(); |
||||
sb.append(label).append(":\n"); |
||||
|
||||
if (result.isArray()) { |
||||
if (result.isEmpty()) return "No " + label.toLowerCase() + " found"; |
||||
int count = 0; |
||||
for (JsonNode loc : result) { |
||||
String uri = loc.path("uri").asText(""); |
||||
String path = uri.replaceFirst("^file:///", "").replaceFirst("^file://", ""); |
||||
sb.append(" ").append(path); |
||||
JsonNode range = loc.get("range"); |
||||
if (range != null) { |
||||
JsonNode start = range.get("start"); |
||||
if (start != null) { |
||||
sb.append(":").append(start.path("line").asInt() + 1); |
||||
sb.append(":").append(start.path("character").asInt() + 1); |
||||
} |
||||
} |
||||
sb.append("\n"); |
||||
if (++count >= 20) { |
||||
sb.append(" ... and ").append(result.size() - 20).append(" more\n"); |
||||
break; |
||||
} |
||||
} |
||||
} else if (result.isObject()) { |
||||
String uri = result.path("uri").asText(""); |
||||
sb.append(" ").append(uri.replaceFirst("^file:///", "")).append("\n"); |
||||
} |
||||
return sb.toString(); |
||||
} |
||||
|
||||
private String formatHover(JsonNode result) { |
||||
if (result.isObject()) { |
||||
JsonNode contents = result.get("contents"); |
||||
if (contents == null) return "No information"; |
||||
if (contents.isTextual()) return contents.asText(); |
||||
if (contents.isObject()) { |
||||
return contents.path("value").asText("No information"); |
||||
} |
||||
if (contents.isArray()) { |
||||
StringBuilder sb = new StringBuilder(); |
||||
for (JsonNode item : contents) { |
||||
if (item.isTextual()) sb.append(item.asText()).append("\n"); |
||||
else if (item.isObject()) sb.append(item.path("value").asText()).append("\n"); |
||||
} |
||||
return sb.toString().trim(); |
||||
} |
||||
} |
||||
return result.toString(); |
||||
} |
||||
|
||||
private String formatSymbols(JsonNode result) { |
||||
if (!result.isArray()) return "No symbols"; |
||||
|
||||
StringBuilder sb = new StringBuilder(); |
||||
sb.append("Symbols:\n"); |
||||
int count = 0; |
||||
for (JsonNode sym : result) { |
||||
String symName = sym.path("name").asText("?"); |
||||
int kind = sym.path("kind").asInt(0); |
||||
String kindStr = symbolKindName(kind); |
||||
sb.append(String.format(" %s %s%n", kindStr, symName)); |
||||
|
||||
// Handle children (DocumentSymbol)
|
||||
JsonNode children = sym.get("children"); |
||||
if (children != null && children.isArray()) { |
||||
for (JsonNode child : children) { |
||||
String cName = child.path("name").asText("?"); |
||||
int cKind = child.path("kind").asInt(0); |
||||
sb.append(String.format(" %s %s%n", symbolKindName(cKind), cName)); |
||||
} |
||||
} |
||||
|
||||
if (++count >= 50) { |
||||
sb.append(" ... and more\n"); |
||||
break; |
||||
} |
||||
} |
||||
return sb.toString(); |
||||
} |
||||
|
||||
private String symbolKindName(int kind) { |
||||
return switch (kind) { |
||||
case 1 -> "📄 File"; |
||||
case 2 -> "📦 Module"; |
||||
case 3 -> "🔲 Namespace"; |
||||
case 4 -> "📦 Package"; |
||||
case 5 -> "🏷 Class"; |
||||
case 6 -> "⚡ Method"; |
||||
case 7 -> "🔧 Property"; |
||||
case 8 -> "📌 Field"; |
||||
case 9 -> "🔨 Constructor"; |
||||
case 10 -> "📋 Enum"; |
||||
case 11 -> "🔗 Interface"; |
||||
case 12 -> "λ Function"; |
||||
case 13 -> "📎 Variable"; |
||||
case 14 -> "📐 Constant"; |
||||
case 15 -> "📝 String"; |
||||
case 16 -> "🔢 Number"; |
||||
case 17 -> "☑ Boolean"; |
||||
case 18 -> "📊 Array"; |
||||
case 19 -> "📋 Object"; |
||||
case 23 -> "📌 Struct"; |
||||
case 26 -> "📎 TypeParameter"; |
||||
default -> " Symbol"; |
||||
}; |
||||
} |
||||
|
||||
private String noServerMessage(Path file) { |
||||
String ext = file.toString(); |
||||
int dot = ext.lastIndexOf('.'); |
||||
ext = dot >= 0 ? ext.substring(dot) : "unknown"; |
||||
return "No LSP server configured for " + ext + " files. Configure in plugin settings."; |
||||
} |
||||
|
||||
@Override |
||||
public String activityDescription(Map<String, Object> input) { |
||||
String action = (String) input.getOrDefault("action", "query"); |
||||
String file = (String) input.getOrDefault("file_path", ""); |
||||
String name = Path.of(file).getFileName().toString(); |
||||
return "🔍 LSP " + action + " in " + name; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue