Skip to content

feat(vscode): add configurable Node.js path for language server#2773

Merged
davydkov merged 3 commits intomainfrom
vscode-node-path
Mar 19, 2026
Merged

feat(vscode): add configurable Node.js path for language server#2773
davydkov merged 3 commits intomainfrom
vscode-node-path

Conversation

@davydkov
Copy link
Copy Markdown
Member

@davydkov davydkov commented Mar 19, 2026

Summary

  • Add likec4.node.path setting to configure the Node.js binary used to run the language server
  • Show a helpful error message with a link to settings when the language server fails to start (e.g. incompatible Node.js version)
  • Prompt to restart the extension host when the Node.js path is changed

Test plan

  • Verify the setting appears in VSCode settings UI under "LikeC4 > Node: Path"
  • Verify default (empty) behaves as before — uses node from PATH
  • Verify custom path starts LSP using that binary
  • Verify error message appears when LSP fails to start

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Configurable Node.js path setting so you can specify a custom Node executable for the extension.
    • UI prompts to restart the extension host after changing the Node path.
    • Improved startup error detection with a guided “Configure Node” action to help resolve initialization issues.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 19, 2026

🦋 Changeset detected

Latest commit: 2f971b6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
likec4-vscode Patch
likec4 Patch
@likec4/docs-astro Patch
@likec4/playground Patch
@likec4/style-preset Patch
@likec4/styles Patch
@likec4/config Patch
@likec4/core Patch
@likec4/diagram Patch
@likec4/generators Patch
@likec4/language-server Patch
@likec4/language-services Patch
@likec4/layouts Patch
@likec4/leanix-bridge Patch
@likec4/log Patch
@likec4/mcp Patch
@likec4/react Patch
@likec4/tsconfig Patch
@likec4/vite-plugin Patch
@likec4/vscode-preview Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

Adds a configurable Node.js executable path for the VS Code extension's language server, plus a changeset, localization entry, Claude settings file, and client logic to apply the configured runtime, prompt for restarts, and surface startup failures.

Changes

Cohort / File(s) Summary
Release & Config
\.changeset/vscode-node-path-setting.md, \.claude/settings.json
New changeset marking likec4-vscode for a patch release; added Claude settings JSON with an allowlist for CLI patterns and limited capabilities.
VS Code Extension Metadata
packages/vscode/package.json, packages/vscode/package.nls.json
Added likec4.node.path configuration (string, default "") and localized label ext.config.node.path.title; adjusted qna and keywords.
Language Client Logic
packages/vscode/src/node/useLanguageClient.ts
Replaced hardcoded node runtime with nodeRuntime from config; added watcher to prompt/trigger extension host restart on config changes; added startup-failure detection that shows an error with a "Configure Node" action.

Sequence Diagram

sequenceDiagram
    actor User
    participant Config as Extension Config
    participant Client as Language Client
    participant Server as Language Server

    User->>Config: Update likec4.node.path
    Config->>Client: Emit configuration change
    Client->>Client: Compute new nodeRuntime (watchEffect)
    Client->>User: Prompt to restart extension host
    User->>Client: Confirm restart
    Client->>Server: Start/Initialize using nodeRuntime
    Server-->>Client: Ready / Running
    alt Server fails to start
        Server-->>Client: Error / Stopped
        Client->>User: Show error with "Configure Node" action
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰✨ I hopped through settings, found a node-shaped trail,
A path you can set so the server won't fail,
Restart when you wish, or adjust with a click,
The rabbit approved — the runtime's now slick! 🥕🛤️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description adequately covers the changes with a clear summary and test plan, but does not follow the provided template with its checklist of contribution guidelines. Consider using the repository's standard PR description template to ensure all checklist items (guidelines, commits, tests, documentation) are properly addressed and verified.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding a configurable Node.js path setting for the VS Code language server.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vscode-node-path
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
packages/vscode/package.json (1)

189-193: Consider adding a title property for consistency with similar settings.

The likec4.graphviz.path setting (line 152-156) includes a title property, but this new setting omits it. Adding a title would improve consistency in the VS Code settings UI.

♻️ Suggested improvement
         "likec4.node.path": {
           "type": "string",
+          "title": "%ext.config.node.path.title%",
           "default": "",
           "description": "Path to the Node.js executable used to run the language server. If empty, uses 'node' from PATH."
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/package.json` around lines 189 - 193, The new VS Code setting
"likec4.node.path" lacks a title property; add a "title" field (e.g., "Node.js
Path") to the likec4.node.path JSON object to match the style used by the
existing likec4.graphviz.path setting so the setting appears with a consistent
label in the VS Code UI.
packages/vscode/src/node/useLanguageClient.ts (3)

47-47: The nodeRuntime is computed once at initialization—it won't react to config changes.

The nodeRuntime value is calculated during module initialization and used in serverOptions. If the user changes likec4.node.path after the extension activates, the language client would still use the original runtime path until the extension host restarts. This behavior is acceptable given the restart prompt, but consider adding a comment to clarify this is intentional.

Also applies to: 51-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/src/node/useLanguageClient.ts` at line 47, The nodeRuntime
variable is currently computed once from config.node?.path and captured into
serverOptions so it won't update if the user changes likec4.node.path at
runtime; update the file (around the nodeRuntime declaration and serverOptions
usage in useLanguageClient.ts, including the related block covering lines 51–90)
by adding a clear comment above the nodeRuntime declaration and above
serverOptions stating this behavior is intentional and that changes require an
extension host restart (or that you chose not to react to runtime config
changes), referencing the nodeRuntime symbol, the config object, and
serverOptions so future readers understand why it is not recomputed dynamically.

162-178: The watchEffect + watch pattern is redundant.

The nodepath ref is initialized from config.node.path, then watchEffect re-syncs it on every change, and watch observes nodepath. This indirection is unnecessary—you can watch config.node.path directly via a computed or use watchEffect alone with the restart logic.

Also, with once: true, users won't see the restart prompt if they change the path multiple times before restarting.

♻️ Simplified approach
-  const nodepath = ref(config.node.path)
-
-  watchEffect(() => {
-    nodepath.value = config.node.path
-  })
-
-  watch(nodepath, (_newPath) => {
+  watch(() => config.node.path, (_newPath, oldPath) => {
+    if (oldPath === undefined) return // Skip initial
     vscode.window.showInformationMessage(
       'Run command "Restart Extension Host" to use the updated Node.js path',
       'Restart Now',
     ).then((selection) => {
       if (!selection) {
         return
       }
       vscode.commands.executeCommand('workbench.action.restartExtensionHost')
     })
-  }, {
-    once: true,
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/src/node/useLanguageClient.ts` around lines 162 - 178, The
current pattern creates an unnecessary indirection by syncing the nodepath ref
via watchEffect and then watching nodepath; remove the nodepath ref and its
watchEffect and instead watch config.node.path directly (or use a computed that
returns config.node.path) in the watch callback that shows the restart prompt;
also remove the { once: true } option so the prompt is shown on every change (or
explicitly decide the intended one-time behavior) — update the watch call that
references nodepath to reference config.node.path (or the computed) and keep the
existing restart prompt logic inside that watcher.

143-160: Startup failure detection may trigger false positives.

The current logic calls suggestChangeNode() whenever the client reaches State.Stopped, even if it previously reached State.Running. If the server starts successfully but stops normally within 5 seconds (e.g., user closes VS Code or deactivates extension), the error message would still appear.

Consider tracking whether the server successfully reached Running state before showing the error.

♻️ Proposed improvement
+  let hasReachedRunning = false
+
   const onDidChangeState = useDisposable(
     client.onDidChangeState(({ newState }) => {
       if (newState === State.Running) {
+        hasReachedRunning = true
         // If the server is running for 5 seconds,
         // we can assume it started successfully and unsubscribe
         setTimeout(() => {
           if (client.isRunning()) {
             onDidChangeState.dispose()
           }
         }, 5000).unref()
       }
       // If the server stops within the first 5 seconds after starting, suggest checking Node.js version
-      if (newState === State.Stopped) {
+      if (newState === State.Stopped && !hasReachedRunning) {
         suggestChangeNode()
         onDidChangeState.dispose()
       }
     }),
   )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/src/node/useLanguageClient.ts` around lines 143 - 160, The
stop handler currently calls suggestChangeNode() on any State.Stopped transition
which can false-positive after a successful run; update the onDidChangeState
callback (the client.onDidChangeState used with useDisposable) to track whether
the server ever reached State.Running (e.g., a boolean reachedRunning set to
true when newState === State.Running) and only call suggestChangeNode() when
newState === State.Stopped and reachedRunning is false; retain the existing
disposal logic (onDidChangeState.dispose()) and the 5s timeout/client.isRunning
check unchanged.
.claude/settings.json (1)

1-18: This file appears unrelated to the PR objective.

This Claude Code settings file grants development permissions but isn't part of the "configurable Node.js path" feature. Consider whether it should be in a separate commit or PR to keep the feature commit focused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/settings.json around lines 1 - 18, The .claude/settings.json change
(the JSON object containing "permissions" -> "allow" with entries like "Bash(gh
*)", "Bash(git *)", etc.) is unrelated to the configurable Node.js path feature;
remove this file from the current commit/PR or move it to a separate commit/PR
so the feature branch only contains relevant changes. Undo or revert the
.claude/settings.json addition from the branch (e.g., remove it from the index
or create a new commit that deletes it) and re-run the PR so only files related
to the Node.js path feature remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/vscode/src/node/useLanguageClient.ts`:
- Line 29: The access to config.node is inconsistent: change the initialization
of nodepath in useLanguageClient.ts (symbol: nodepath) to match the optional
chaining used later (or remove optional chaining later if config.node is
guaranteed); specifically, either initialize nodepath using config.node?.path
(to safely handle undefined config.node) or remove the ?. in any other
references (e.g., where config.node?.path is used) to assume config.node is
always present—pick one approach and apply it consistently across nodepath and
all uses of config.node.path.
- Line 31: The destructured variable logger from useExtensionLogger() in
useLanguageClient.ts is unused; remove logger from the destructuring (leave
output) to eliminate the unused variable warning. Update the line that currently
reads "const { output, logger } = useExtensionLogger()" to only destructure
output so that useLanguageClient no longer references the unused logger symbol.

---

Nitpick comments:
In @.claude/settings.json:
- Around line 1-18: The .claude/settings.json change (the JSON object containing
"permissions" -> "allow" with entries like "Bash(gh *)", "Bash(git *)", etc.) is
unrelated to the configurable Node.js path feature; remove this file from the
current commit/PR or move it to a separate commit/PR so the feature branch only
contains relevant changes. Undo or revert the .claude/settings.json addition
from the branch (e.g., remove it from the index or create a new commit that
deletes it) and re-run the PR so only files related to the Node.js path feature
remain.

In `@packages/vscode/package.json`:
- Around line 189-193: The new VS Code setting "likec4.node.path" lacks a title
property; add a "title" field (e.g., "Node.js Path") to the likec4.node.path
JSON object to match the style used by the existing likec4.graphviz.path setting
so the setting appears with a consistent label in the VS Code UI.

In `@packages/vscode/src/node/useLanguageClient.ts`:
- Line 47: The nodeRuntime variable is currently computed once from
config.node?.path and captured into serverOptions so it won't update if the user
changes likec4.node.path at runtime; update the file (around the nodeRuntime
declaration and serverOptions usage in useLanguageClient.ts, including the
related block covering lines 51–90) by adding a clear comment above the
nodeRuntime declaration and above serverOptions stating this behavior is
intentional and that changes require an extension host restart (or that you
chose not to react to runtime config changes), referencing the nodeRuntime
symbol, the config object, and serverOptions so future readers understand why it
is not recomputed dynamically.
- Around line 162-178: The current pattern creates an unnecessary indirection by
syncing the nodepath ref via watchEffect and then watching nodepath; remove the
nodepath ref and its watchEffect and instead watch config.node.path directly (or
use a computed that returns config.node.path) in the watch callback that shows
the restart prompt; also remove the { once: true } option so the prompt is shown
on every change (or explicitly decide the intended one-time behavior) — update
the watch call that references nodepath to reference config.node.path (or the
computed) and keep the existing restart prompt logic inside that watcher.
- Around line 143-160: The stop handler currently calls suggestChangeNode() on
any State.Stopped transition which can false-positive after a successful run;
update the onDidChangeState callback (the client.onDidChangeState used with
useDisposable) to track whether the server ever reached State.Running (e.g., a
boolean reachedRunning set to true when newState === State.Running) and only
call suggestChangeNode() when newState === State.Stopped and reachedRunning is
false; retain the existing disposal logic (onDidChangeState.dispose()) and the
5s timeout/client.isRunning check unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdbe1446-3b74-4d8a-84a7-a418ee09ba13

📥 Commits

Reviewing files that changed from the base of the PR and between d8e075d and 4e3ca46.

📒 Files selected for processing (5)
  • .changeset/vscode-node-path-setting.md
  • .claude/settings.json
  • packages/vscode/package.json
  • packages/vscode/package.nls.json
  • packages/vscode/src/node/useLanguageClient.ts

- Remove unused `logger` variable
- Fix inconsistent optional chaining on config.node
- Simplify watchEffect + watch into direct watch on config.node.path
- Guard startup failure detection against false positives
- Remove unused imports (ref, watchEffect)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/vscode/src/node/useLanguageClient.ts`:
- Around line 123-160: The onDidChangeState handler currently disposes itself
after the first stable run so later restarts that fail won’t trigger
suggestChangeNode; update the listener logic on client.onDidChangeState (and the
hasReachedRunning flag) to retain the handler across restarts and detect an
actual failed start transition (Starting -> Stopped) instead of disposing on
first Running/timeout. Concretely: track the previous state (e.g., prevState
variable), remove the onDidChangeState.dispose() calls (and the timeout-based
dispose), call suggestChangeNode() when prevState === State.Starting && newState
=== State.Stopped (or when newState === State.Stopped && !hasReachedRunning),
and rely on the existing once() wrapper around suggestChangeNode to ensure the
prompt only appears once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05798e87-8da3-41ed-b5da-fc4442e72d2e

📥 Commits

Reviewing files that changed from the base of the PR and between 4e3ca46 and 2f971b6.

📒 Files selected for processing (1)
  • packages/vscode/src/node/useLanguageClient.ts

Comment on lines +123 to +160
const suggestChangeNode = once(() => {
const message =
'Language server failed to start. This may be caused by an incompatible Node.js version. Please make sure you have Node.js 22.22 or later installed and configured in the extension settings.'
output.error(message)
output.show(true)
vscode.window
.showErrorMessage(
message,
'Configure Node',
)
.then(selection => {
if (selection === 'Configure Node') {
vscode.commands.executeCommand('workbench.action.openSettings', 'likec4.node.path')
}
})
})

let hasReachedRunning = false

const onDidChangeState = useDisposable(
client.onDidChangeState(({ newState }) => {
if (newState === State.Running) {
hasReachedRunning = true
// If the server is running for 5 seconds,
// we can assume it started successfully and unsubscribe
setTimeout(() => {
if (client.isRunning()) {
onDidChangeState.dispose()
}
}, 5000).unref()
}
// If the server never reached Running, suggest checking Node.js version
if (newState === State.Stopped && !hasReachedRunning) {
suggestChangeNode()
onDidChangeState.dispose()
}
}),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This startup-failure detection only covers the first start attempt.

The wrapper in packages/vscode/src/useLanguageClient.ts (Lines 43-53) restarts the same client instance later, but this listener disposes itself after the first stable run. Any later Starting -> Stopped failure will skip suggestChangeNode(), which narrows the new UX to the initial activation only. Since suggestChangeNode is already wrapped in once(), you can watch the actual failed-start transition directly and keep the handler armed across restarts.

♻️ Proposed fix
-  let hasReachedRunning = false
-
-  const onDidChangeState = useDisposable(
-    client.onDidChangeState(({ newState }) => {
-      if (newState === State.Running) {
-        hasReachedRunning = true
-        // If the server is running for 5 seconds,
-        // we can assume it started successfully and unsubscribe
-        setTimeout(() => {
-          if (client.isRunning()) {
-            onDidChangeState.dispose()
-          }
-        }, 5000).unref()
-      }
-      // If the server never reached Running, suggest checking Node.js version
-      if (newState === State.Stopped && !hasReachedRunning) {
-        suggestChangeNode()
-        onDidChangeState.dispose()
-      }
-    }),
-  )
+  useDisposable(
+    client.onDidChangeState(({ oldState, newState }) => {
+      if (oldState === State.Starting && newState === State.Stopped) {
+        suggestChangeNode()
+      }
+    }),
+  )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/src/node/useLanguageClient.ts` around lines 123 - 160, The
onDidChangeState handler currently disposes itself after the first stable run so
later restarts that fail won’t trigger suggestChangeNode; update the listener
logic on client.onDidChangeState (and the hasReachedRunning flag) to retain the
handler across restarts and detect an actual failed start transition (Starting
-> Stopped) instead of disposing on first Running/timeout. Concretely: track the
previous state (e.g., prevState variable), remove the onDidChangeState.dispose()
calls (and the timeout-based dispose), call suggestChangeNode() when prevState
=== State.Starting && newState === State.Stopped (or when newState ===
State.Stopped && !hasReachedRunning), and rely on the existing once() wrapper
around suggestChangeNode to ensure the prompt only appears once.

@davydkov davydkov merged commit f4698a6 into main Mar 19, 2026
17 checks passed
@davydkov davydkov deleted the vscode-node-path branch March 19, 2026 23:42
@likec4-ci likec4-ci bot mentioned this pull request Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant