Skip to content

Commit 7041b2c

Browse files
elkaixcursoragent
andauthored
feat(tui+lsp): TUI theme system, streaming phase 0, LSP subsystem, and report panel polish (#157)
* style(tui): use default foreground for diff +/- markers Diff add/remove rows are distinguished by background tint only; line numbers and +/- glyphs no longer use green/red accent foreground. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(lsp): add plugin-based LSP code intelligence subsystem (PLIP-10) Port the reference LSP subsystem to a first-class Python feature: a new `LSP` agent tool (definition, references, hover, document/workspace symbols, implementation, full call hierarchy), session-scoped server lifecycle over the Host stdio abstraction, passive diagnostics injected after file edits, and plugin-only server discovery/recommendation (no bundled binaries). Hardening folded in from review: - framing raises LspServerDown on EOF so the read loop fails pending requests instead of busy-spinning (C01/C10); wired LspClient.on_crash -> instance mark_crashed so the max_restarts cap is enforced against real crashes. - LSP tool distinguishes "no server for file type" from an empty server response (definition-not-found now renders guidance, not a false "no server"). - request failures return an error result instead of a false-success ok(). - file-tool edit hook (write/replace) clears delivered diagnostics for the file and wraps change/save notifications in try/except so an LSP hiccup never fails an already-successful write (C03). - no-server file types surface a one-line plugin install hint (per ext/session), gated by recommendation_disabled/never. Persisted >=5 ignore auto-disable is deferred: save_config() rewrites the whole file with no lock/atomic rename, so incremental concurrent writes are unsafe (multi-instance clobber risk). Registered on default + coder agents only; excluded from offline/fail-closed code_reviewer. Servers are plugin-only with PluginPolicy.external_exec gating. * feat(tui): theme package, slash input UX, and streaming phase 0 Replace monolithic theme.py with spec/palette/resolver adapters, trim periwinkle inline highlights, add slash prefix and subcommand suggest highlighting, grey out low context-usage bar, and coalesce live streaming repaints. Refresh native install script help and progress UX. * feat(tui): polish theme, transcript spacing, usage activity, and install UX Switch inline code and ANSI syntax highlights from cyan to accent/periwinkle and blue, add trailing scrollback gaps between tool output and agent prose, style the welcome banner branch with muted warning yellow, and ship the token-activity heatmap for /usage. Refresh install banner animation and tests. * feat(tui): pythinker-x theme port, tool header accent tokens, and diff/spacing polish Port pythinker-x theme: 32 bundled syntax theme names, Catppuccin Frappe/Macchiato styles, `/theme code` syntax picker, and aligned diff palette. Tool-call subject highlights switch from cyan to brand periwinkle accent; welcome banner uses neutral grey for branch and yellow for model name. Fix diff marker spacing for @-prefixed lines and add composing-block blank-row before the activity line. * feat(tui): report panel polish, secondary token, LSP subagent guard test Report rendering: - Add `pythinker_report_markdown()` + `report_markdown_style_overrides()` so only H1 headings render bold inside report panels; all other roles drop bold weight - Replace `### section` headings with `# title` routed through report markdown so section headers are bold while body prose stays regular - Add `REPORT_FILE_MARKER` glyph (⌁ / +) for finding location rows; use a grid table to keep long paths indented consistently - Add `secondary` CoreToken and `TuiTokens.secondary` field (dark #AAB0B6, light #8A93A0) for scope/note text that needs less emphasis than `muted` - Replace `Rule(severity …)` section separators with `_render_section_header()` (title line + plain rule below) for cleaner visual hierarchy - Pill-background summary line: severity dots and counts share `tool_pending_bg` background for compact badge appearance - Correct `PROMPT_GLYPH` dark hex to #F1F3F5 (was left behind when activity_label was updated in b9662b7) Worklog / blocks: - `_tool_token_style` now raises `ValueError` on unknown token names instead of silently falling back to `info`; all reachable style values are valid TUI tokens - Insert `BLANK_ROW` between the collapsed card and its sub-tool activity group Selector: - Extract `_default_match` typed nested function in `code_theme.py` to fix pyright `reportUnknownLambdaType` on the prior lambda LSP gaps (from lsp-code-scan-analysis report): - Add `test_lsp_provider_registered_in_subagent_soul` guard test: asserts `LspDiagnosticsInjectionProvider` is in `_injection_providers` and `rearm_injection` is wired for subagent-role `PythinkerSoul` instances - Update blackbox port-status ledger: LSP row promoted from future-approved-only to done (d936b32, 63 tests, PLIP-10) * feat(tui): split markdown pipeline and add audit report rendering Refactor shell markdown into focused modules with shared fence scanning, size guards, and capped streaming parse cache. Add audit-profile normalization for dense parity reports (collapsed matrices, field tables, path compaction, and quote gutters) and fix report-prose misparsing of aligned field lines. Align dark prompt border tokens with core border palette. * fix(tui): correct plan approval prompt state and dialog chrome Show awaiting approval while ExitPlanMode waits for user choice, pause the Considering spinner during question/approval panels, reorder choices, and improve dialog spacing and copy. Also harden LSP frame reads and sync tests. * fix(tui+lsp): resolve CI failures and CodeRabbit findings; harden RunAgents Make the PR branch green and address the review. CI failures (15 tests + ruff): - Update agent-spec / config-dump / pyinstaller snapshots for the new LSP tool - Add explicit encoding to LSP framing/symbol_context/tool sources (static check) - Sync dark prompt frame/separator/dialog borders to their core theme tokens - Flush the live view immediately for external (approval/steer) messages and supervise the frame/status refresh loops so a refresh failure surfaces instead of silently freezing the view (fixes 3 external-approval tests) - Wire `_mode` + completer onto the bottom-toolbar unit test's bare session - Fix import sorting / line length CodeRabbit findings: - Critical: inline code spans now actually clear an inherited background (Rich `Style(bgcolor=None)` is a no-op; mutate the copied style instead) - LSP: bounded JSON-RPC frame size + graceful-shutdown timeout, didChange document version tracking, open-doc state cleared on restart, empty diagnostics payloads clear stale entries, code-0 dedup key, TOCTOU re-check, documentSymbol file_count - Theme picker: compare resolved themes by identity so distinct Catppuccin variants no longer match - Usage activity: anchor the chart to the payload window (not wall-clock) and fix the current-streak partial-today contract - Config: validate LSP numeric settings; `/usage` rejects extra activity args; `/theme code <Name>` preserves case; guard install-script cursor escapes to animated mode; drop unused worklog icon params RunAgents: strip stray whitespace-only string entries the model emits between agent objects so a multi-agent launch no longer fails validation. Also: AGENTS.md gains a "Pre-PR gate" checklist (run the full gate, snapshot fix-direction, encoding/bundling/changelog checks) to stop these slipping to CI. * fix(tui): suppress consecutive ToolSearch probes in transcript scrollback Multiple ToolSearch calls during deferred tool discovery now collapse to a single scrollback entry (the last probe), mirroring the blackbox reference's `isAbsorbedSilently` contract for ToolSearch. Intermediate discovery calls no longer produce repeated "Tools(…)" noise lines in the transcript. Also ships two other tested fixes from the collaborative branch: - fix(todo): accept Cursor/Claude TodoWrite shape using `content` instead of `title` (model_validator normalises before Pydantic validation) - feat(ui/slash): bare-segment matching for namespaced slash commands so `/designer` surfaces `/skill:designer-skill` without adding duplicate paths * feat(tui): fuzzy-match slash commands by distinctive word Skills share a common prefix (pythinker-), so segment-prefix matching cannot disambiguate them. Add a lowest-priority fuzzy subsequence tier on the bare segment (gated at 2+ chars) so a misspelled distinctive word like /gurd surfaces /skill:pythinker-guard. Stronger prefix/alias/segment tiers still rank first; the inserted text stays the canonical command. * fix(soul): tighten truncated-response continuation reminder Reword the output-token-limit nudge to resume mid-thought without recap, and update the matching test assertion. * fix(soul): align output-token-limit nudge text with reference byte-exactly The system-reminder injected after a truncated response now matches the blackbox reference verbatim per the AGENTS.md byte-exact text contract: 'Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.' Update the pinning test assertion to match the new text. * fix(tools): gate ToolSearch to models that support deferred tool search ToolSearch only makes sense with Anthropic's tool_reference/defer_loading beta. The type="anthropic" compat proxies (z.ai/GLM, Kimi, MiniMax, opencode) point at their own endpoints that don't forward that beta, and non-Anthropic providers don't have it at all. Offering ToolSearch there is noise: it re-lists already-visible tools, and weaker tool-callers (observed: GLM-5.2) loop on it forever instead of calling tools directly. Add supports_deferred_tool_search() (genuine api.anthropic.com host + non- haiku, with ENABLE_TOOL_SEARCH override) mirroring the reference's gate, and hide ToolSearch in PythinkerToolset._is_tool_visible when unsupported. The tool stays registered so a /model switch to Claude re-enables it instantly. Rendering is intentionally unchanged (compact absorber kept, not reverted to the reference's verbose generic card). * fix(tools): stop ToolSearch description claiming nonexistent deferral Root-cause follow-up to the ToolSearch gate: pythinker emits no defer_loading and ToolSearch only searches already-visible, already-callable tools (it unlocks nothing). The old description told the model that "deferred/hidden tool loading" might leave capabilities off the prompt — a false claim that primed weaker models to loop searching for tools they already had. Rewrite it to state plainly that it reveals nothing new and to call tools directly. * fix(tui): hang-indent space-aligned report preview during streaming Run the lightweight space-column normalizer in the composing preview path and wrap long Severity/Location/What rows with continuation indent so wrap fragments no longer orphan at column 0. Document ToolSearch absence in the default-agent tool snapshot test. * fix(tools): normalize Cursor TodoWrite shape and compact failed todo cards Boundary-normalize content→title before SetTodoList validation, persist canonical title-only session state, and render validation failures as compact actionable errors instead of a fake persisted todo tree. * fix(tools): harden todo renderer and blank-title normalization Treat blank titles as missing for Cursor-shape detection, compute indent from ANSI-stripped text, and show invalid args for malformed complete lists. * fix(pr-157): address CodeRabbit review and CI check failures Resolve review findings across LSP, TUI streaming, toolset, and tests: fail-closed git check-ignore filtering, symbol boundary fixes, report-update refresh, markdown fence/audit hardening, ToolSearch execution gate, and lint/ pyright/test stability improvements including import sort in visualize. * fix(lsp+ui): treat non-git dirs as unfiltered and suppress fuzzy slash noise Allow git check-ignore exit 128 (not a repository) so LSP location filtering works in tmp_path tests and non-git workspaces while keeping fail-closed on real check-ignore errors. Hide fuzzy slash completions when a stronger prefix/alias match exists so /mo resolves to /model only. * fix(pr-157): resolve remaining CodeRabbit review threads * fix(pr-157): address CodeRabbit audit.py and code-quality re-export findings * fix(ui): re-export markdown shim via module attribute assignment * fix(pr-157): satisfy code-quality on re-exports and LSP returns * docs(lsp): expand LSP docs with operation table and plugin schema; fix empty-diagnostic clear - Add table of all 9 LSP operations with their wire method names to lsp.md - Document plugin server config schema (.lsp.json / lspServers) with all fields, defaults, constraints, and ${VAR} / ${VAR:-default} env expansion including PYTHINKER_PLUGIN_ROOT / PYTHINKER_PLUGIN_DATA built-ins - Note .gitignore filtering on reference/definition/implementation/workspaceSymbol results - Fix DiagnosticRegistry to clear stale pending state on empty publishDiagnostics payloads (LSP "clear all diagnostics for this URI" signal) * fix(pr-157): remove unused _REEXPORT_REGISTRY and _PALETTE_ONLY_FILES globals --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 33595ea commit 7041b2c

155 files changed

Lines changed: 15787 additions & 2647 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,41 @@ Pick the smallest reliable gate for the change, then run broader gates before re
224224
If a gate cannot run because of missing system tools (for example `npm`), report that explicitly
225225
instead of claiming success.
226226

227+
## Pre-PR gate (run before pushing or opening a PR)
228+
229+
CI failures that "slip to GitHub" almost always trace to pushing after a *partial* local check
230+
(for example running `ruff`/`pyright` on a single file instead of the whole package). Before you
231+
push a branch or open a PR that touches shipped code, run the full gate and clear every item below.
232+
Running a focused check on only the files you edited is **not** sufficient — snapshot, static, and
233+
bundling tests fail on files you did not touch.
234+
235+
1. **Full gate, not partial.** Run `make check-pythinker-code` (ruff + format + pyright) **and**
236+
`make test-pythinker-code`. Paste/confirm the actual "All checks passed" / passing summary — a
237+
green `ruff` alone is not a green `check` (pyright and format are separate). For changes to
238+
another workspace package, run that package's `make check-* && make test-*` too.
239+
2. **Include `tests_e2e`.** CI runs `tests` and `tests_e2e`. New slash commands, wire events, or
240+
agent-spec/tool changes move the wire-handshake snapshot and the agent-spec/config/pyinstaller
241+
snapshots. Re-run the affected tests; apply deliberate snapshot updates with
242+
`uv run pytest <node> --inline-snapshot=fix` and **read the resulting diff** before committing.
243+
3. **Snapshot fix-direction.** A hardcoded expected value that changed deliberately → update the
244+
**test**. An invariant of the form "two values must stay equal" (e.g. a prompt token that must
245+
track a core theme token) → fix the **code** that drifted, never the test.
246+
4. **New source files clear the static checks.** `tests/test_ai_static_requirements.py` enforces
247+
explicit text encoding (`encoding="utf-8"`, `errors="replace"` for tool decodes) and other
248+
invariants across `src/pythinker_code/**`. Note the ruff-vs-static conflict: ruff `UP012` strips
249+
`"utf-8"` from a **string-literal** `.encode("utf-8")`, but the static check wants an explicit
250+
`encoding=`. Encode/decode via a **local variable or call result**, not a literal, so both gates
251+
pass (see `lsp/framing.py`).
252+
5. **New bundled files/tools update the manifests.** A new tool `*.md`, prompt, or package adds
253+
entries to `tests/utils/test_pyinstaller_utils.py` (`datas` + `hiddenimports`) and, for new
254+
config keys, `tests/core/test_config.py::test_default_config_dump`.
255+
6. **Changelog.** Any change to shipped paths (`src/*`, `packages/*`, installers, release
256+
workflows, `pythinker.spec`) needs a new `- ...` line under `## Unreleased` in `CHANGELOG.md`, or
257+
the `changelog-entry-required` check fails the PR.
258+
7. **Confirm what is actually new.** Diff against `origin/main` (`git log origin/main..HEAD`,
259+
`git diff origin/main...HEAD --stat`) so the PR scope — and the review/verification surface — is
260+
what you intend, not stale local commits.
261+
227262
## Project architecture
228263

229264
### Runtime path

‎CHANGELOG.md‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,87 @@ GitHub Releases page; `0.8.0` is the new starting line.
1515

1616
## Unreleased
1717

18+
- **TUI composing preview wraps space-aligned report prose cleanly.** The
19+
streaming preview now runs the same lightweight space-column normalizer used at
20+
finalize and wraps long `Severity`/`Location`/`What` rows with a hanging
21+
continuation indent, so wrapped fragments no longer orphan at column 0.
22+
- **ToolSearch hidden from models that can't use it.** `ToolSearch` is now offered
23+
only when the active model genuinely supports the deferred tool-search workflow
24+
(Anthropic's `tool_reference`/`defer_loading` beta on `api.anthropic.com`). The
25+
`type="anthropic"` compat proxies (z.ai/GLM, Kimi, MiniMax, opencode) and all
26+
non-Anthropic providers no longer see it, fixing a loop where weaker tool-callers
27+
(e.g. GLM-5.2) repeatedly "searched" for tools instead of calling them. Override
28+
with `ENABLE_TOOL_SEARCH=true|false`. The tool's description no longer claims that
29+
hidden/deferred tools exist (pythinker loads no tools lazily), removing the prompt
30+
that primed the loop in the first place.
31+
- **Output-token-limit nudge text aligned with reference.** The system-reminder injected when a response is cut off by the output token limit now matches the reference byte-exactly: "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces."
32+
- **`SetTodoList` accepts Cursor-style todo payloads.** Todo items sent with `content` instead of `title` (the shape models learn from Cursor/Claude `TodoWrite`) are normalized at the validation boundary (`content` → `title` when `title` is absent; canonical `title` wins; `content` is dropped) and persist as title-only session state instead of failing with missing-`title` errors.
33+
- **Failed `SetTodoList` cards stay compact.** Validation failures no longer render a broken todo tree with blank labels plus a raw Pydantic dump; the card shows a short actionable summary (with full detail only when expanded).
34+
- **ToolSearch scrollback suppression.** Consecutive `ToolSearch` probes during deferred tool discovery are now collapsed: only the last probe in each run is shown in the transcript, mirroring the blackbox `isAbsorbedSilently` contract. Intermediate discovery calls no longer produce repeated "Tools(…)" lines.
35+
- **Bare skill/flow slash names.** The slash menu now matches `skill:`/`flow:`
36+
commands on their bare segment, so typing `/designer` (or `/design`) surfaces
37+
`/skill:designer-skill`; accepting inserts the canonical command name. When no
38+
prefix matches, a fuzzy fallback surfaces the distinctive word even when
39+
misspelled (`/gurd` → `/skill:pythinker-guard`), so skills sharing a common
40+
prefix stay reachable.
41+
- **TUI composing preview gap.** Removed the visible double-blank row between
42+
`Composing…` and the in-progress preview (leading newline from commit
43+
boundaries no longer leaks through the plain-text preview path), and aligned
44+
the Rich `Live` paint rate with the 25 Hz reveal scheduler (was 10 Hz).
45+
- **ToolSearch TUI display.** `ToolSearch` results now render as a compact
46+
"N tools discovered (Agent, Grep, …)" summary instead of dumping the full
47+
tool catalog with descriptions; ctrl+o expands to tool names only.
48+
- **Tool header highlights.** Read/Write/Edit/Grep and similar tool-call subjects
49+
now use the brand periwinkle `accent` token instead of cyan `info`; line ranges
50+
stay on the yellow `warning` token.
51+
- **pythinker-x theme port.** Diff palette, 32 bundled syntax theme names, Catppuccin
52+
Frappe/Macchiato styles, and `/theme code` syntax picker aligned with the Pythinker-X TUI.
53+
- **TUI inline code color.** Inline `` `code` `` highlights and the `pythinker-ansi`
54+
syntax theme now use brand periwinkle/accent and blue ANSI roles instead of cyan.
55+
- **TUI transcript spacing.** User prompts leave one blank row before the agent stream
56+
starts; finished tool cards and flushed agent paragraphs leave a trailing blank row
57+
before the next block (Bash/Read output → next ⏺ paragraph, etc.).
58+
- **Welcome banner colors.** Branch uses light neutral grey; model name uses the muted
59+
yellow warning token.
60+
- **TUI theme package.** Centralize dark/light palettes, prompt classes, and Rich/PTK
61+
adapters in `ui/theme/` with `/theme current|doctor|tokens` inspection commands.
62+
- **TUI diff markers.** Inline diff rows now leave a space after `+`/`-` markers so
63+
`@`-prefixed lines (e.g. CSS `@keyframes`) do not run together with the sign.
64+
- **Composing block spacing.** Staged agent paragraphs keep one blank row before the
65+
Composing activity line while the stream is still live.
66+
- **Slash input UX.** Prefix-highlight skills and plugins while typing; ghost-complete
67+
and highlight fixed subcommands such as `/theme current`.
68+
- **TUI streaming smoothness (Phase 0).** Coalesce Rich Live repaints to a 25 Hz frame budget,
69+
render live previews as plain text (no per-token markdown re-parse), stage committed slices
70+
inside the Live region until finalize, and use a fixed-width blinking streaming caret that
71+
does not reflow wrapped lines.
72+
- **LSP code intelligence.** Plugin-provided language servers power a new `LSP` agent tool
73+
(go-to-definition, find-references, hover, symbols, call hierarchy) with session-scoped
74+
server lifecycle, passive diagnostics injected after file edits, and plugin-based server
75+
discovery/recommendation — no bundled language-server binaries.
76+
- **Token activity card.** `/usage daily|weekly|cumulative` (and the bare `/usage` default
77+
when no provider adapter is configured) now render a 52-week × 7-day heatmap of
78+
total tokens consumed each day, with a `Lifetime · Peak · Streak · Longest task` summary
79+
line and a footer that lets the user switch between daily/weekly/cumulative views. Data is
80+
read from the local session wire files; the per-provider adapter behavior is unchanged.
81+
- **RunAgents tolerates blank list entries.** Models occasionally emit bare `"\n"` strings
82+
between the agent objects in the `agents` array; those are now stripped before validation so
83+
a multi-agent launch no longer fails with a validation error, while genuinely invalid entries
84+
are still rejected.
85+
- **Report panel rendering.** Standardized report panels render only the panel title and section
86+
headers bold (body prose stays regular weight), tag finding locations with a file marker, and
87+
use a dedicated `secondary` theme token for scope/note text.
88+
- **Theme token consistency.** The dark prompt frame/separator/dialog borders and the prompt
89+
glyph now track their canonical core theme tokens, and inline code spans correctly drop an
90+
inherited background.
91+
- **External approvals repaint promptly.** Out-of-band approval requests and steer input now
92+
force an immediate live-view repaint instead of waiting for the streaming frame budget, and
93+
the live-view refresh loop is supervised so a refresh-loop failure surfaces instead of
94+
silently freezing the view.
95+
- **LSP robustness.** Bounded JSON-RPC frame size and graceful-shutdown timeout, document
96+
version tracking for `didChange`, open-document state cleared on server restart, empty
97+
diagnostics payloads clear stale entries, and tightened `/usage` activity-argument validation.
98+
1899
## 0.47.0 (2026-06-16)
19100

20101
- **Plugin marketplaces and activation policy.** `pythinker plugin marketplace` can add,

‎docs/.vitepress/config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default withMermaid(defineConfig({
4848
text: 'Customization',
4949
items: [
5050
{ text: 'Model Context Protocol', link: '/en/customization/mcp' },
51+
{ text: 'Language Server Protocol', link: '/en/customization/lsp' },
5152
{ text: 'Plugins (Beta)', link: '/en/customization/plugins' },
5253
{ text: 'Hooks (Beta)', link: '/en/customization/hooks' },
5354
{ text: 'Agent Skills', link: '/en/customization/skills' },

‎docs/en/customization/architecture.md‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ canonical list lives in `src/pythinker_code/wire/types.py` (`Event` union): `Ste
100100
| `src/pythinker_code/soul/context.py` | Conversation history, checkpoints, JSONL persistence. | `Context` |
101101
| `src/pythinker_code/soul/toolset.py` | Loads built-in + MCP tools, injects deps, executes calls. | `PythinkerToolset` |
102102
| `src/pythinker_code/soul/slash.py` | Slash-command registry and dispatch. | `registry` |
103-
| `src/pythinker_code/soul/dynamic_injection.py` (+ `dynamic_injections/`) | Injects budgeted `<system-reminder>` content per step: plan-mode, auto-mode, model-defense. | `DynamicInjectionProvider` |
103+
| `src/pythinker_code/soul/dynamic_injection.py` (+ `dynamic_injections/`) | Injects budgeted `<system-reminder>` content per step: plan-mode, auto-mode, model-defense, LSP diagnostics. | `DynamicInjectionProvider` |
104104
| `src/pythinker_code/soul/permission.py` | Per-step permission profiles (`read_only`/`plan`/`ask`/`implement`/`review`/`verify`) and destructiveness classification. | `tool_destructive_reason`, `shell_command_signature` |
105105
| `src/pythinker_code/soul/denwarenji.py` | D-Mail checkpoint rewind (`BackToTheFuture`). | — |
106106
| `src/pythinker_code/soul/flow_runner.py` | Ralph Loop driver for `/flow` and iterative commands. | — |
@@ -142,6 +142,7 @@ wrapped with `UntrustedData`.
142142
| `src/pythinker_code/tools/file/` | `ReadFile`, `WriteFile`, `StrReplaceFile`, `Glob`, `Grep`, `ReadMediaFile` |
143143
| `src/pythinker_code/tools/shell/` | `Shell` |
144144
| `src/pythinker_code/tools/web/` | `SearchWeb`, `FetchURL` (conditional on deps) |
145+
| `src/pythinker_code/tools/lsp/` | `Lsp` (model name `LSP`; plugin-backed language servers) |
145146
| `src/pythinker_code/tools/agent/` | `Agent`, `RunAgents` |
146147
| `src/pythinker_code/tools/background/` | `TaskOutput`, `TaskList`, `TaskInput`, `TaskStop`, `TaskHandoff` |
147148
| `src/pythinker_code/tools/` (other) | `AskUserQuestion`, `EnterPlanMode`/`ExitPlanMode`, `Think`, `SetTodoList`, `Memory`, `Recall`, `Scratchpad`, `Suggest`, `Progress`, `ReadSkill`, `SendDMail`, `ListMcpResources`/`ReadMcpResource` |
@@ -201,7 +202,19 @@ Full session lifecycle: `initialize`, `new_session`, `load_session`, `resume_ses
201202
| --- | --- | --- |
202203
| `src/pythinker_code/skill/`, `src/pythinker_code/skills/` | Skill discovery/loading across scopes (project > user > extra > built-in), local specialization, flow skills; injected via `PYTHINKER_SKILLS`. Bundled skills live in `skills/`. | `Skill`, `discover_skills_from_roots`, `index_skills`, `format_skills_for_prompt`, `Flow`, `SkillLockFile` |
203204
| `src/pythinker_code/hooks/` | Lifecycle hook engine: 13 events, server-side shell commands and client-side Wire subscriptions; fail-open (block only on explicit exit code 2 / structured deny). | `HookEngine`, `HookDef`, `HookEventType`, `HOOK_EVENT_TYPES`, `run_hook`, `events` |
204-
| `src/pythinker_code/plugin/` | Plugin discovery, install (local/git/zip with SSRF + traversal guards, staged atomic install), and subprocess tool execution with fresh credential injection. | `parse_plugin_json`, `PluginSpec`, `install_plugin`, `list_plugins`, `load_plugin_tools`, `PluginTool` |
205+
| `src/pythinker_code/plugin/` | Plugin discovery, install (local/git/zip with SSRF + traversal guards, staged atomic install), subprocess tool execution, MCP and LSP server configs (`plugin_lsp_servers`). | `parse_plugin_json`, `PluginSpec`, `install_plugin`, `list_plugins`, `load_plugin_tools`, `PluginTool`, `plugin_mcp_servers`, `plugin_lsp_servers` |
206+
207+
## LSP subsystem
208+
209+
Session-scoped language-server processes over `Host.exec` stdio (JSON-RPC Content-Length framing).
210+
Servers are plugin-only; one `LspService` per `Runtime`, shared by subagents, torn down in
211+
`cleanup_runtime_resources()`.
212+
213+
| Path | Purpose | Key entry points |
214+
| --- | --- | --- |
215+
| `src/pythinker_code/lsp/` | Client, server lifecycle, routing, diagnostics registry, plugin loader, recommendation. | `LspService`, `LspServerManager`, `LspClient`, `DiagnosticRegistry`, `plugin_lsp_servers` |
216+
217+
Trust boundary: LSP subprocesses run with agent privileges; output is untrusted project content.
205218

206219
## Memory, background, and notifications
207220

‎docs/en/customization/lsp.md‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Language Server Protocol (LSP)
2+
3+
Pythinker Code can connect to [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) servers for semantic code intelligence: go-to-definition, find-references, hover, symbols, and call hierarchy.
4+
5+
## Plugin-only servers
6+
7+
LSP servers are **not** configured in user or project TOML. They come only from installed plugins — inline `lspServers` in `plugin.json` or a plugin-root `.lsp.json` file. Pythinker does not bundle language-server binaries.
8+
9+
Enable executable plugin artifacts (`plugins.external_exec = true` or `pythinker plugin enable <name>`) so plugin-provided LSP subprocesses are allowed.
10+
11+
### Server config schema
12+
13+
Each entry in `lspServers` (or the root object of `.lsp.json`) maps a server name to a config object:
14+
15+
```json
16+
{
17+
"my-server": {
18+
"command": "pylsp",
19+
"args": ["--check-parent-process"],
20+
"extensionToLanguage": { ".py": "python" },
21+
"env": { "VIRTUAL_ENV": "${VIRTUAL_ENV:-}" },
22+
"initializationOptions": {},
23+
"startupTimeout": 30.0,
24+
"maxRestarts": 3
25+
}
26+
}
27+
```
28+
29+
| Field | Required | Description |
30+
|-------|----------|-------------|
31+
| `command` | yes | Executable to launch |
32+
| `args` | no | Additional CLI arguments |
33+
| `extensionToLanguage` | yes | Maps file extensions to LSP language IDs |
34+
| `env` | no | Extra environment variables for the server process |
35+
| `initializationOptions` | no | Passed verbatim in the LSP `initialize` request |
36+
| `startupTimeout` | no | Seconds to wait for server ready (default `30.0`, must be `> 0`) |
37+
| `maxRestarts` | no | Max automatic restarts on crash (default `3`, `0` disables) |
38+
39+
Values in `command`, `args`, and `env` support `${VAR}` and `${VAR:-default}` expansion against the process environment. Two plugin-local path variables are always available: `${PYTHINKER_PLUGIN_ROOT}` (the plugin directory) and `${PYTHINKER_PLUGIN_DATA}` (a writable per-plugin data directory). The `CLAUDE_PLUGIN_ROOT` / `CLAUDE_PLUGIN_DATA` spellings are accepted as aliases.
40+
41+
## Agent tool
42+
43+
The `LSP` tool is available on the default agent and the `coder` subagent (not on read-only profiles such as `code_reviewer`). It exposes nine operations with 1-based line/character positions (editor-style).
44+
45+
| Operation | LSP method |
46+
|-----------|------------|
47+
| `goToDefinition` | `textDocument/definition` |
48+
| `findReferences` | `textDocument/references` |
49+
| `hover` | `textDocument/hover` |
50+
| `documentSymbol` | `textDocument/documentSymbol` |
51+
| `workspaceSymbol` | `workspace/symbol` |
52+
| `goToImplementation` | `textDocument/implementation` |
53+
| `prepareCallHierarchy` | `textDocument/prepareCallHierarchy` |
54+
| `incomingCalls` | `callHierarchy/incomingCalls` |
55+
| `outgoingCalls` | `callHierarchy/outgoingCalls` |
56+
57+
Results from `findReferences`, `goToDefinition`, `goToImplementation`, and `workspaceSymbol` automatically filter out paths that match the project's `.gitignore`.
58+
59+
Servers start lazily on first use per language and stay alive for the session. Subagents share the root session's LSP processes.
60+
61+
## Passive diagnostics
62+
63+
After `WriteFile` or `StrReplaceFile`, the session notifies open language servers and surfaces new compiler/linter diagnostics on the next turn via dynamic context injection (budget-capped). Diagnostics are labeled as LSP-reported, not agent-asserted.
64+
65+
## Configuration
66+
67+
Feature switches only — in `~/.pythinker/config.toml`:
68+
69+
```toml
70+
[lsp]
71+
enabled = true
72+
recommendation_disabled = false
73+
recommendation_never = []
74+
```
75+
76+
When you edit a file whose extension matches a discoverable but not-yet-installed plugin server, Pythinker may suggest installing that plugin (respecting `recommendation_never` and auto-disabling after repeated ignores).
77+
78+
## Trust boundary
79+
80+
LSP servers run as subprocesses with the agent's privileges. Hover, symbol, and diagnostic text is treated as untrusted project content (same class as `ReadFile` output).

0 commit comments

Comments
 (0)