summaryrefslogtreecommitdiffhomepage
path: root/notes/lsp-design.md
blob: 32f29bed5f7059f6ec2c5b4acdea91b484ff3e7b (plain)
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
# LSP integration — design + plan

> Orchestrator-authored plan (notes/ is orchestrator-editable). Drives the
> `prompts/<unit>.md` TASK blocks. Status: AWAITING USER SIGN-OFF on the
> decisions in §6 before summoning.

## 1. Goal

A **general-purpose, config-driven LSP** (Language Server Protocol) integration that
is trivial to set up for any project, exposed to the agent as a tool and to the
frontend as status. Hard requirements:

- **MUST work for this repo** (`arch-rewrite`, TypeScript) — zero/low config.
- **MUST work for the Roblox project** (`/home/tradam/projects/roblox`, Luau via
  `luau-lsp`), including the bug opencode got wrong (§2).
- Frontend endpoint: is the LSP connected, and which servers are loaded **per tab**
  (per conversation).
- **Per-conversation CWD**: persist it, expose get/set to the frontend (this does
  not exist yet — cwd is currently per-turn only).
- Frontend **handoff** document.

## 2. The bug we must NOT reproduce (root cause, confirmed)

opencode's `client/registerCapability` handler ignores every registration except
`textDocument/diagnostic` (`references/opencode/.../lsp/client.ts:198-206` does
`continue` for anything else). It *declares* `workspace.didChangeWatchedFiles.
dynamicRegistration: true` (client.ts:249-251) but never honors the server's
watcher registration and never runs a real filesystem watcher. So when `luau-lsp`
registers a watch for `sourcemap.json` / `**/*.luau`, opencode silently drops it.
Disk changes never reach the server → stale sourcemap → `TypeError: Unknown
require` for any file created mid-session. (Confirmed against
`/home/tradam/projects/roblox/LUA_TOOLING.md:61-66`.)

**Our fix (spec-compliant, general — fixes sourcemap-style staleness for ANY
server):**
1. Declare `workspace.didChangeWatchedFiles.dynamicRegistration: true`.
2. **Honor** `client/registerCapability` / `unregisterCapability` for
   `workspace/didChangeWatchedFiles`: store the registered `watchers` (globPattern
   + kind).
3. Run a **real filesystem watcher** over the workspace root; on create/change/
   delete matching a registered glob, send a `workspace/didChangeWatchedFiles`
   notification with the correct `FileChangeType`.
4. Keep derived files fresh: optional per-server **`watch` sidecar command** (for
   Roblox: `rojo sourcemap <project> --watch -o sourcemap.json`). The Roblox docs
   only avoided `--watch` *because* opencode couldn't consume the updates — once
   (1-3) work, the sidecar + watcher close the loop with zero edits to the Roblox
   project.

## 3. Architecture (synthesized from old-dispatch + opencode references)

- **JSON-RPC over stdio**, LSP `Content-Length` framing. Lazy-spawn one server
  process per `(serverID, workspaceRoot)`; dedup concurrent spawns; track `broken`
  servers (no retry storm). Initialize handshake (45s timeout) → `initialized` →
  `workspace/didChangeConfiguration`.
- **Diagnostics:** push (`textDocument/publishDiagnostics`) + pull
  (`textDocument/diagnostic`), merged + deduped. `touchFile → didOpen/didChange →
  waitForDiagnostics`.
- **Server registry / matching:** by file extension + workspace-root markers.
  Workspace root = the conversation's cwd (or nearest root-marker ancestor up to
  cwd). Spawn cwd = workspace root.
- **Pure core / injected shell** (constitution): the protocol codec (framing,
  request/response correlation, capability-registration state machine, diagnostic
  merge) is a **pure module** tested with an injected in-memory duplex stream — no
  real child process, zero internal mocks. The edges (spawn child process, fs
  watcher, read file) are injected adapters.
- **Lifecycle/cleanup:** the extension owns its manager; child processes MUST be
  killed on shutdown (no leaked LSP/sidecar procs — cf. the bracket-trick scar
  tissue). Needs the host to call `deactivate()` on shutdown (verify; CR to
  host-bin if missing).

## 4. Units & waves (one owner per unit; single-writer)

**Wave 1 (parallel — disjoint packages, kernel-only deps):**
- **`conversation-store`** (existing, owner-summon): add per-conversation **cwd**
  persistence to its contract + impl + tests (`getCwd`/`setCwd`, separate key
  space). Exposes the typed surface session-orchestrator + transport-http consume.
- **`lsp`** (NEW extension): the whole LSP client/manager/registry/file-watcher/
  config-resolver + the model-facing `lsp` tool + a typed `lspServiceHandle`
  (`status(root)`, `diagnostics(...)`). Depends only on kernel contracts (uses
  `ctx.cwd`) + reads a per-cwd config file. The big unit.
- **`transport-contract`** (existing, types-only): add `CwdResponse`/`SetCwdRequest`
  and `LspStatusResponse` (per-tab loaded servers + connected status).

**Wave 2 (parallel — depend on wave 1):**
- **`transport-http`**: `GET`/`PUT /conversations/:id/cwd` (persist via
  conversation-store) + `GET /conversations/:id/lsp` (resolve conversationId→cwd→
  `lspServiceHandle.status(root)`).
- **`session-orchestrator`**: when `/chat` omits cwd, default to the persisted
  per-conversation cwd (load from conversation-store); persist cwd when provided.

**Wave 3 (composition):**
- **`host-bin`**: register `lsp` in `CORE_EXTENSIONS`; wire shutdown so LSP/sidecar
  procs are killed (deactivate). Orchestrator does the build wiring (root tsconfig
  ref for `packages/lsp`, `bun install`, any new dep, devDep
  `typescript-language-server` for live verification).

**Wave 4 (orchestrator):** full-graph typecheck/test/check; live-verify against
this repo (TS) + Roblox (luau-lsp); write the FE handoff.

## 5. Config & how each target works

- **This repo (zero config):** built-in `typescript` server definition (root
  markers `tsconfig.json`/`package.json`; command `typescript-language-server
  --stdio`), matched on `.ts`/`.tsx`.
- **Roblox (its existing config, ideally zero new setup):** read
  `<cwd>/.dispatch/lsp.json` if present, else fall back to `<cwd>/opencode.json`'s
  `lsp` key (the Roblox project already has it). When a server's
  `initialization.luau-lsp.sourcemap.autogenerate === true` with a
  `rojoProjectFile`, auto-spawn the `rojo sourcemap --watch` sidecar (the §2 fix).

## 6. DECISIONS FOR THE USER (boundary/granularity/scope — ORCHESTRATOR §5.2)

See the chat message accompanying this doc.