@elyracode/lsp-typescript 0.9.17 → 0.9.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.19] - 2026-07-18
4
+
5
+ ## [0.9.18] - 2026-07-18
6
+
7
+ ### Added
8
+ - Proactive symbol-aware auto-context: after an `edit` tool call, referenced type/interface identifiers in the edited code are resolved via `textDocument/hover` and appended to the tool result, so the agent sees relevant type information without a separate lookup round trip
9
+
3
10
  ## [0.9.17] - 2026-07-18
4
11
 
5
12
  ## [0.9.16] - 2026-07-15
@@ -1,4 +1,9 @@
1
- import type { ExtensionAPI } from "@elyracode/coding-agent";
1
+ import {
2
+ type EditToolDetails,
3
+ type EditToolInput,
4
+ type ExtensionAPI,
5
+ isEditToolResult,
6
+ } from "@elyracode/coding-agent";
2
7
  import { type ChildProcess, execSync, spawn } from "node:child_process";
3
8
  import { existsSync, readFileSync } from "node:fs";
4
9
  import { join, resolve } from "node:path";
@@ -67,6 +72,92 @@ function languageIdForPath(filePath: string): string {
67
72
  return "typescript";
68
73
  }
69
74
 
75
+ // ── Symbol-aware auto-context ────────────────────────────────────────────
76
+ // After an edit, proactively resolve unfamiliar-looking type/class names
77
+ // referenced in the new code via hover, and append a short summary to the
78
+ // edit's own tool result. Saves a definitions/hover round trip for the
79
+ // common case of "I just referenced a type, is this the shape I think it is".
80
+
81
+ /** Common built-ins that rarely need an explanation. */
82
+ const TS_SYMBOL_STOPLIST = new Set([
83
+ "String",
84
+ "Number",
85
+ "Boolean",
86
+ "Array",
87
+ "Object",
88
+ "Promise",
89
+ "Error",
90
+ "Map",
91
+ "Set",
92
+ "Date",
93
+ "RegExp",
94
+ "JSON",
95
+ "Symbol",
96
+ "Math",
97
+ "Function",
98
+ "Record",
99
+ "Partial",
100
+ "Readonly",
101
+ "Pick",
102
+ "Omit",
103
+ "Required",
104
+ "Awaited",
105
+ "Buffer",
106
+ "ArrayBuffer",
107
+ "Uint8Array",
108
+ "Console",
109
+ ]);
110
+
111
+ /** Extract candidate PascalCase type/class identifiers from a snippet of code. */
112
+ function extractCandidateSymbols(text: string, stoplist: Set<string>, max: number): string[] {
113
+ const found = new Set<string>();
114
+ const re = /\b[A-Z][A-Za-z0-9_]*\b/g;
115
+ let m: RegExpExecArray | null = re.exec(text);
116
+ while (m && found.size < max * 3) {
117
+ if (!stoplist.has(m[0])) found.add(m[0]);
118
+ m = re.exec(text);
119
+ }
120
+ return [...found].slice(0, max);
121
+ }
122
+
123
+ /** Find the (1-based) line/column of the first whole-word match of `symbol`, searching from `fromLine` first. */
124
+ function findSymbolPosition(
125
+ fileText: string,
126
+ symbol: string,
127
+ fromLine: number,
128
+ ): { line: number; column: number } | undefined {
129
+ const lines = fileText.split("\n");
130
+ const re = new RegExp(`\\b${symbol}\\b`);
131
+ const start = Math.max(0, fromLine - 1);
132
+ for (let i = start; i < lines.length; i++) {
133
+ const idx = lines[i].search(re);
134
+ if (idx >= 0) return { line: i + 1, column: idx + 1 };
135
+ }
136
+ for (let i = 0; i < start; i++) {
137
+ const idx = lines[i].search(re);
138
+ if (idx >= 0) return { line: i + 1, column: idx + 1 };
139
+ }
140
+ return undefined;
141
+ }
142
+
143
+ /** Reduce a hover result to a short, single-line summary. */
144
+ function summarizeHover(result: unknown): string | undefined {
145
+ if (!result) return undefined;
146
+ const hover = result as LspHoverResult;
147
+ let text: string;
148
+ if (typeof hover.contents === "string") text = hover.contents;
149
+ else if (Array.isArray(hover.contents)) {
150
+ text = hover.contents.map((c) => (typeof c === "string" ? c : c.value)).join("\n");
151
+ } else text = hover.contents.value;
152
+
153
+ const firstLine = text
154
+ .split("\n")
155
+ .map((l) => l.trim())
156
+ .find((l) => l && !l.startsWith("```"));
157
+ if (!firstLine) return undefined;
158
+ return firstLine.length > 160 ? `${firstLine.slice(0, 160)}\u2026` : firstLine;
159
+ }
160
+
70
161
  function findBinary(workingDir: string): string | undefined {
71
162
  const local = join(workingDir, "node_modules", ".bin", "typescript-language-server");
72
163
  if (existsSync(local)) return local;
@@ -214,6 +305,17 @@ export default function (elyra: ExtensionAPI): void {
214
305
  openedFiles.add(uri);
215
306
  }
216
307
 
308
+ /** Force the server to see the current on-disk content (e.g. right after our own edit). */
309
+ function syncFile(filePath: string): void {
310
+ const absPath = resolve(cwd, filePath);
311
+ const uri = fileUri(absPath);
312
+ if (openedFiles.has(uri)) {
313
+ sendNotification("textDocument/didClose", { textDocument: { uri } });
314
+ openedFiles.delete(uri);
315
+ }
316
+ openFile(filePath);
317
+ }
318
+
217
319
  function formatLocations(result: unknown, workingDir: string): string {
218
320
  if (!result) return "No results found.";
219
321
 
@@ -432,9 +534,56 @@ export default function (elyra: ExtensionAPI): void {
432
534
  }
433
535
  },
434
536
  });
537
+
538
+ // ── Hook: auto-context on edit (see "Symbol-aware auto-context" above) ──
539
+ elyra.on("tool_result", async (event) => {
540
+ if (!initialized || !isEditToolResult(event) || event.isError) return undefined;
541
+
542
+ const input = event.input as unknown as EditToolInput;
543
+ if (typeof input?.path !== "string" || !Array.isArray(input.edits)) return undefined;
544
+ if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(input.path)) return undefined;
545
+
546
+ try {
547
+ syncFile(input.path);
548
+ const absPath = resolve(cwd, input.path);
549
+ const fileText = readFileSync(absPath, "utf-8");
550
+ const details = event.details as EditToolDetails | undefined;
551
+ const fromLine = details?.firstChangedLine ?? 1;
552
+
553
+ const candidates = new Set<string>();
554
+ for (const edit of input.edits) {
555
+ for (const s of extractCandidateSymbols(edit.newText, TS_SYMBOL_STOPLIST, 4)) candidates.add(s);
556
+ }
557
+ if (candidates.size === 0) return undefined;
558
+
559
+ const uri = fileUri(absPath);
560
+ const lookups = [...candidates].slice(0, 5).map(async (symbol) => {
561
+ const pos = findSymbolPosition(fileText, symbol, fromLine);
562
+ if (!pos) return undefined;
563
+ try {
564
+ const result = await sendRequest("textDocument/hover", {
565
+ textDocument: { uri },
566
+ position: positionFromLineCol(pos.line, pos.column),
567
+ });
568
+ const summary = summarizeHover(result);
569
+ return summary ? `- ${symbol}: ${summary}` : undefined;
570
+ } catch {
571
+ return undefined;
572
+ }
573
+ });
574
+
575
+ const lines = (await Promise.all(lookups)).filter((l): l is string => !!l);
576
+ if (lines.length === 0) return undefined;
577
+
578
+ const contextBlock = `\n\n[Related symbols \u2014 auto-resolved via LSP, no extra tool call needed]\n${lines.join("\n")}`;
579
+ return { content: [...event.content, { type: "text" as const, text: contextBlock }] };
580
+ } catch {
581
+ return undefined;
582
+ }
583
+ });
435
584
  });
436
585
 
437
- // ── Shutdown ────────────────────────────────────────────────────────
586
+ // ── Shutdown ────────────────────────────────────────────
438
587
 
439
588
  elyra.on("session_shutdown", async () => {
440
589
  if (lspProcess && initialized) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elyracode/lsp-typescript",
3
- "version": "0.9.17",
3
+ "version": "0.9.19",
4
4
  "description": "TypeScript LSP integration for Elyra — semantic code navigation and diagnostics",
5
5
  "type": "module",
6
6
  "keywords": [