diff --git a/README.md b/README.md index a60f974..f28544b 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ locode is a full-screen terminal app built with [Ink](https://github.com/vadimde - **Ctrl+F**: open and focus a file panel docked to the right of the chat (hidden by default); press again to close it. It has two tabs — **Files**, the project's collapsible file tree (directories in cyan, same `node_modules`/`.git`/`dist` exclusions as `@` mentions), and **Activity**, the files `read_file`/`write_file`/`edit_file` have touched so far this session, most recent first, with a status glyph (`·` read, `+` written, `~` edited) and a repeat count. **Ctrl+G** switches between the two tabs. While the panel is focused, `↑`/`↓` move the selection (auto-scrolling to keep it in view), `↵`/`←`/`→` expand or collapse the selected folder, and **Esc** hands keyboard focus back to the chat input without closing the panel — typing is disabled while the panel has focus, so the same arrow key doesn't simultaneously recall chat history. - **Backends**: `--backend ollama` (default) or `--backend lmstudio`, or `--base-url ` for anything else that speaks the same API. -- **Tools**: `read_file`, `list_files`, `grep`, `web_search`, `web_fetch`, `git_status`, `bash_output`, `todo_write` run automatically. `write_file`, `edit_file`, `bash`, `bash_kill`, and `git_commit` show a diff/preview in a bordered box and ask you to pick Yes / Yes-always-this-session / No with the arrow keys before running. +- **Tools**: `read_file`, `list_files`, `grep`, `definition`, `references`, `diagnostics`, `web_search`, `web_fetch`, `git_status`, `bash_output`, `todo_write` run automatically. `write_file`, `edit_file`, `multi_edit`, `bash`, `bash_kill`, and `git_commit` show a diff/preview in a bordered box and ask you to pick Yes / Yes-always-this-session / No with the arrow keys before running. - **Permission modes**: `default` (ask before every mutating tool), `plan` (research only — every mutating tool is blocked outright, no prompt; the model is expected to describe what it would do in its final answer instead), `auto-edit` (file edits auto-approved, `bash`/`git_commit` still ask), `auto-accept` (everything auto-approved — use with care). Cycle with `Shift+Tab` or set directly with `/perm `. - **Task checklists**: for multi-step work the model can call `todo_write` to show a live checklist (`☐`/`◐`/`☑`) in the transcript instead of silently working through a list you can't see progress on. - **Project instructions**: a `CLAUDE.md` (or `AGENTS.md`) file in the project root is automatically read at session start and folded into the system prompt — put repo-specific conventions there and every session picks them up without being told. @@ -105,6 +105,8 @@ locode is a full-screen terminal app built with [Ink](https://github.com/vadimde - **Tool-calling mode**: on connect, locode probes whether the model reliably uses native OpenAI-style function calling. If not, it switches to a prompt-based fallback mode where the model is instructed to emit tool calls as fenced ` ```tool_call ``` ` JSON blocks, which locode parses itself. The result is cached per backend+model so future sessions skip the probe. Override with `--tool-mode native|fallback|auto` or the in-session `/mode` command. - **Context tracking & compaction**: the status bar shows `ctx NN%` — context window usage, from real `usage.prompt_tokens` when the backend reports it (requested via `stream_options.include_usage`), or a `~`-prefixed char-based estimate otherwise. The window size itself is auto-detected (Ollama's `/api/show`, then LM Studio's `/api/v0/models`) and cached per backend+model; falls back to a configurable default (`locode config set contextWindow `, or `$LOCODE_CONTEXT_WINDOW`) if neither responds. At 85% usage, locode automatically asks the model to summarize the conversation and replaces the history with that summary (a notice tells you when this happens) — or trigger it yourself anytime with `/compact`. +- **Code intelligence (LSP)**: `definition`, `references`, and `diagnostics` use a real language server (LSP) for go-to-definition, find-all-references, and type/syntax error checks — the same engine an editor's Problems panel uses, more precise than `grep`. A server is lazily started per language on first use and reused for the whole session: `typescript-language-server` (TypeScript/JavaScript), `pyright-langserver` (Python), `gopls` (Go), `rust-analyzer` (Rust), and `clangd` (C/C++ — one clangd covers both). The relevant server binary must be on your PATH; if it isn't, the tool returns a clear "install X" error. After any edit, locode syncs the file to the live server so a subsequent `diagnostics` call reflects the change (it waits for the server to publish fresh diagnostics rather than reading a stale snapshot). Add or override servers with `locode config set lspServers` (see Config). + Note: even models with genuine native tool-calling support occasionally emit a tool call as plain text instead of a real structured call — this is model sampling variance, not a bug. If a turn seems to "describe" a tool call instead of running it, just ask again or try `/mode fallback`. ## Slash commands @@ -144,6 +146,11 @@ locode config set autoCompactThreshold 0.85 # fraction of context window at whi locode config set requestTimeoutMs 300000 # per-request timeout in ms (default 180000); raise this if # your backend queues requests behind a concurrency limit # (e.g. Ollama's OLLAMA_NUM_PARALLEL) under multi-session load +locode config set lspServers '{"java":{"command":"jdtls","extensions":[".java"]}}' # add a language server + # (JSON object keyed by language id; built-in ids + # override command/args, new ids add support and + # require extensions). Built-ins: typescript, + # python, go, rust, c (C/C++ share clangd). locode config get locode config path ``` diff --git a/src/tools/codeIntel.test.ts b/src/tools/codeIntel.test.ts new file mode 100644 index 0000000..c10fffd --- /dev/null +++ b/src/tools/codeIntel.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// The LSP tools (definition/references/diagnostics) are thin dispatchers over lspManager. We mock +// the manager functions so the tests run without spawning a language server, and assert each tool +// forwards the right arguments (path resolved against cwd, 1-indexed→handled by the manager, +// includeDeclaration default) and returns the manager's result verbatim. + +vi.mock("../codeintel/lspManager.js", () => ({ + getDefinition: vi.fn(async () => ({ definitions: [{ path: "/abs/a.ts", line: 3, column: 5 }] })), + getReferences: vi.fn(async () => ({ references: [{ path: "/abs/a.ts", line: 3, column: 5 }] })), + getDiagnostics: vi.fn(async () => ({ + diagnostics: [{ path: "/abs/a.ts", line: 1, column: 1, severity: "error", message: "oops" }], + })), +})); + +import { definitionTool, referencesTool, diagnosticsTool } from "./codeIntel.js"; +import { getDefinition, getReferences, getDiagnostics } from "../codeintel/lspManager.js"; + +const ctx = { cwd: "/proj" }; + +describe("definition tool", () => { + beforeEach(() => vi.clearAllMocks()); + + it("forwards path/line/column/cwd to getDefinition and returns its result", async () => { + const out = await definitionTool.handler({ path: "src/a.ts", line: 3, column: 5 }, ctx); + expect(getDefinition).toHaveBeenCalledWith("src/a.ts", 3, 5, "/proj"); + expect(out).toEqual({ definitions: [{ path: "/abs/a.ts", line: 3, column: 5 }] }); + }); + + it("is read-only (no permission prompt)", () => { + expect(definitionTool.mutating).toBe(false); + }); +}); + +describe("references tool", () => { + beforeEach(() => vi.clearAllMocks()); + + it("defaults includeDeclaration to true when omitted", async () => { + await referencesTool.handler({ path: "src/a.ts", line: 3, column: 5 }, ctx); + expect(getReferences).toHaveBeenCalledWith("src/a.ts", 3, 5, "/proj", true); + }); + + it("passes an explicit includeDeclaration through", async () => { + await referencesTool.handler({ path: "src/a.ts", line: 3, column: 5, include_declaration: false }, ctx); + expect(getReferences).toHaveBeenCalledWith("src/a.ts", 3, 5, "/proj", false); + }); + + it("returns the manager's references result", async () => { + const out = await referencesTool.handler({ path: "src/a.ts", line: 3, column: 5 }, ctx); + expect(out).toEqual({ references: [{ path: "/abs/a.ts", line: 3, column: 5 }] }); + }); +}); + +describe("diagnostics tool", () => { + beforeEach(() => vi.clearAllMocks()); + + it("forwards path/cwd to getDiagnostics and returns its result", async () => { + const out = await diagnosticsTool.handler({ path: "src/a.ts" }, ctx); + expect(getDiagnostics).toHaveBeenCalledWith("src/a.ts", "/proj"); + expect(out).toEqual({ + diagnostics: [{ path: "/abs/a.ts", line: 1, column: 1, severity: "error", message: "oops" }], + }); + }); + + it("is read-only", () => { + expect(diagnosticsTool.mutating).toBe(false); + }); +}); \ No newline at end of file