Skip to content

feat: experimental AI-assisted semantic layout#2878

Merged
davydkov merged 36 commits into
mainfrom
ai-layout
May 8, 2026
Merged

feat: experimental AI-assisted semantic layout#2878
davydkov merged 36 commits into
mainfrom
ai-layout

Conversation

@davydkov

Copy link
Copy Markdown
Member

Summary

  • Add AI layout advisor that analyzes diagram semantics and suggests graphviz layout hints (rank constraints, edge weights, invisible edges) for more readable diagrams
  • New VSCode chat participant and command for triggering AI layout enhancement
  • New AiLayoutPrinter in @likec4/layouts that applies AI-suggested hints to DOT output

Note: This feature is experimental and may change in future releases.

Test plan

  • Verify AI layout produces valid DOT output for sample diagrams
  • Test VSCode chat participant triggers layout enhancement
  • Check existing layout tests still pass (pnpm test in packages/layouts)

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Apr 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0e34eb8

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

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

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

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an experimental AI-assisted semantic layout advisor: new AI modules serialize views, call LLM providers, parse JSON layout hints, and apply them via an AiLayoutViewPrinter. Integrates end-to-end into the language server, Graphviz layouter, VS Code extension (chat participant + command), and MCP tooling, plus build/generation infra and caching updates.

Changes

AI-assisted Semantic Layout (end-to-end)

Layer / File(s) Summary
Data Shape / Types
packages/layouts/src/graphviz/ai/types.ts
New public types: AILayoutProvider, AILayoutRequest, AIEnforcementEdge, AILayoutHints.
LLM Input Serialization
packages/layouts/src/graphviz/ai/llm-input.ts
prepareLLMInput(view) serializes ComputedView → compact LLM-friendly SerializedView and mapping.
Prompt / System Text
packages/layouts/src/graphviz/ai/prompt-system.md, packages/layouts/scripts/generate.ts
Added system prompt markdown and a generator that emits LAYOUT_SYSTEM_PROMPT as generated TS.
LLM Output Parsing
packages/layouts/src/graphviz/ai/llm-output.ts
parseOutput extracts JSON from LLM text, validates via zod, remaps serialized IDs → original IDs, and returns `AILayoutHints
Orchestration
packages/layouts/src/graphviz/ai/orchestrator.ts, packages/layouts/src/graphviz/ai/index.ts
enhanceLayoutWithAI(view, provider, signal) composes request (system/user + diagram), calls provider, parses response, and returns hints; new ai barrel exports API.
Graphviz AI Printer
packages/layouts/src/graphviz/AiLayoutPrinter.ts
New AiLayoutViewPrinter extends DotPrinter to reorder nodes/edges, apply rank constraints, invisible edges, reverse/exclude flags, per-edge weight/minlen, and AI-driven styling.
Layouter Integration
packages/layouts/src/graphviz/GraphvizLayoter.ts
Added printToDot(params, hints?), aiLayout(params, hints), and normalizeDot helper; selects AiLayoutViewPrinter when hints provided and normalizes DOT output.
Views API & Caching
packages/language-server/src/protocol.ts, packages/language-server/src/Rpc.ts, packages/language-server/src/views/LikeC4Views.ts, packages/language-server/src/views/index.ts
Protocol LayoutView.Params gains optional hints?: AILayoutHints; RPC forwards hints as layoutHints; layoutView short-circuits to AI path when hints exist; ProjectStorage cache keys refactored to per-view keys and added clearView/clearAll; manual-layout updates clear per-view cache.
VS Code Command & Chat Integration
packages/vscode/src/commands/semanticLayoutWithAI.ts, packages/vscode/src/node/useLikeC4ChatParticipant.ts, packages/vscode/package.json, packages/vscode-preview/protocol.ts
New command and chat participant to trigger AI layout; VS Code manifest declares likec4.layout-assistant chat participant and likec4.semantic-layout-with-ai command; protocol messages added for AI state and webview trigger.
VS Code Wiring & UI
packages/vscode/src/useMessenger.ts, packages/vscode-preview/src/state.ts, packages/vscode/src/panel/useDiagramPanel.ts
Webview and extension broadcast helpers added (broadcastAiLayoutUpdate), spinner state hook useShowSpinner, and webview handler to invoke the AI command.
MCP Tooling
packages/mcp/src/tools/apply-semantic-layout.ts, packages/mcp/src/server/createMCPServer.ts, packages/mcp/src/ctx.ts
MCP prompt/resource registrations and new apply-semantic-layout tool that calls enhanceLayoutWithAI and applies/persists AI-enhanced layout; added shared MCP context setters/getters.
Printer Refactors & Edge Styling
packages/layouts/src/graphviz/DotPrinter.ts, .../ElementViewPrinter.ts, .../DynamicViewPrinter.ts, .../ProjectsViewPrinter.ts, .../DeploymentViewPrinter.ts
DotPrinter refactored (G property, build/postBuild flow, selectViewNodes/selectViewEdges hooks, enableNewRankIfNeeded); subclasses updated to fluent postBuild, deferred edge style application, and tile/rank refactors to accommodate AI hints.
Parser / WASM / Utils
packages/layouts/src/graphviz/GraphvizParser.ts, .../wasm/GraphvizWasmAdapter.ts, .../utils.ts
Parser now catches per-edge parsing errors; WASM adapter improves retry logic and syntax-error handling; added unit conversion helpers and IconSizePoints.
Build / Infra
packages/layouts/package.json, packages/layouts/build.config.ts, packages/layouts/turbo.json, packages/layouts/.gitignore, dprint.json
New ./ai export, bundle input for AI index, generate script, turbo task for generation, generated file ignored, and dprint excludes prompt markdown.
Dev UX / Validation
packages/layouts/validate.ts
Top-level validate script that generates a baseline layout and an AI-enhanced layout using available providers for manual verification.
Small Utilities / Exports
packages/core/src/utils/unsafe.ts, packages/core/src/utils/index.ts, packages/config/src/node/load-config.ts
Added unsafePartial<T>() export; exported it from utils index; removed unused local type alias in config loader.

Sequence Diagram

sequenceDiagram
    participant VS as VS Code (User)
    participant Chat as Chat Participant
    participant Ext as Extension (command)
    participant RPC as Language Server RPC
    participant LS as Language Server (views)
    participant Layouter as Graphviz Layouter
    participant AI as AI Provider
    participant Cache as ProjectStorage

    VS->>Chat: invoke semantic-layout (chat command)
    VS->>Ext: execute command (menu/command)
    Chat->>RPC: fetchComputedModel(projectId)
    RPC-->>Chat: computed model (view, styles)
    Chat->>Chat: prepareLLMInput(view)
    Chat->>AI: sendRequest(systemPrompt + diagram)
    AI-->>Chat: LLM response (JSON)
    Chat->>Chat: parseOutput -> AILayoutHints
    Chat->>RPC: layoutView(viewId, projectId, hints)
    RPC->>LS: likec4Services.Views.layoutView(params with layoutHints)
    LS->>Layouter: layouter.aiLayout(view, styles, hints)
    Layouter->>Layouter: AiLayoutViewPrinter applies ranks/edges/invisibleEdges
    Layouter-->>LS: LayoutResult (diagram)
    LS->>Cache: ProjectStorage.clearView(viewId)
    LS-->>RPC: layoutView response
    RPC-->>Chat/Ext: success
    Chat->>VS: broadcastAiLayoutUpdate(state: completed)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • likec4/likec4#2681: Overlapping changes to language-server and Graphviz layouter integration and RPC/layout flows.
  • likec4/likec4#2713: Related manual-layout and DotPrinter refactor work affecting applyManualLayout surfaces.
  • likec4/likec4#2520: Prior Graphviz printer and printer-hierarchy changes that intersect with this PR’s DotPrinter/refactor work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ai-layout

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 17

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/language-server/src/filesystem/LikeC4ManualLayouts.ts (1)

128-136: ⚠️ Potential issue | 🟡 Minor

Make snapshot hash deterministic by sorting scanned files first.

Current hash accumulation depends on scanDirectory iteration order. If order varies across platforms/filesystems, unchanged inputs can still produce a different hash.

🔧 Proposed fix
-      const files = await fs.scanDirectory(outDir, isManualLayoutFile)
+      const files = await fs.scanDirectory(outDir, isManualLayoutFile)
+      files.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString()))
       if (files.length === 0) {
         return null
       }

Also applies to: 157-157

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

In `@packages/language-server/src/filesystem/LikeC4ManualLayouts.ts` around lines
128 - 136, The snapshot hash is non-deterministic because it accumulates via
iterating the unsorted array from fs.scanDirectory; before the for (const file
of files) loop in LikeC4ManualLayouts.ts, sort the files array deterministically
(e.g., files.sort((a,b) => a.uri.toString().localeCompare(b.uri.toString())) or
by path) so stringHash receives a stable order, and apply the same sorting
change where files are scanned/hashed again (the other occurrence around the
later hash accumulation).
packages/vscode/src/node/mcp-server.ts (1)

54-61: ⚠️ Potential issue | 🟠 Major

Multi-root workspaces are collapsed to the first folder here.

LIKEC4_WORKSPACE is parsed as an array, but Line 58 immediately narrows it to first(workspacePaths) before calling fromWorkspace(). In a multi-root VS Code workspace, every folder after the first one will be invisible to this MCP server even though registration sends all folder URIs.

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

In `@packages/vscode/src/node/mcp-server.ts` around lines 54 - 61, The code
collapses multi-root workspaces by taking only first(workspacePaths) and calling
fromWorkspace(workspacePath); instead, handle all parsed entries from
readEnvVar('LIKEC4_WORKSPACE') (workspacePaths) rather than narrowing to
workspacePath: either call fromWorkspace for each path in workspacePaths
(falling back to process.cwd() when workspacePaths is empty) or update
fromWorkspace to accept an array and pass workspacePaths directly; update
bootstrap to iterate and register/initialize each resulting likec4 instance
appropriately so every folder in the multi-root workspace is visible.
🟡 Minor comments (11)
.vscode/launch.json-99-103 (1)

99-103: ⚠️ Potential issue | 🟡 Minor

Use runtimeArgs for the Node condition flag.

The --conditions=sources flag is a Node runtime option and should be in runtimeArgs, not args. With "runtimeExecutable": "tsx", args are passed to the program being debugged, while runtimeArgs are passed to tsx itself, which then passes them to the underlying Node process for proper condition resolution.

Proposed fix
       "program": "${file}",
-      "args": [
-        "--conditions=sources"
-      ],
+      "runtimeArgs": [
+        "--conditions=sources"
+      ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vscode/launch.json around lines 99 - 103, The launch configuration
incorrectly places the Node runtime flag "--conditions=sources" under "args"
(which are passed to the program) instead of "runtimeArgs" (which are passed to
the runtime); update the launch.json so that when "runtimeExecutable": "tsx" is
used the "--conditions=sources" entry is moved from the "args" array into the
"runtimeArgs" array (remove it from "args" and add it to "runtimeArgs") so
tsx/Node receives the flag correctly.
packages/language-server/src/lsp/DocumentSymbolProvider.ts-164-167 (1)

164-167: ⚠️ Potential issue | 🟡 Minor

parentFqn is not actually propagated, and fqn is redundantly computed.

Line 166 adds parentFqn, but it is never consumed in getElementsSymbol. Also, Line 187 computes fqn while Line 195 recomputes the same value inline. Please either wire parentFqn through downstream symbol naming logic or remove it for now, and at minimum reuse fqn to avoid dead code.

Suggested cleanup diff
   protected getExtendElementSymbol(astElement: ast.ExtendElement): DocumentSymbol[] {
@@
-    const fqn = readStrictFqn(astElement.element)
+    const fqn = readStrictFqn(astElement.element)
@@
-        name: readStrictFqn(astElement.element),
+        name: fqn,
         range: cst.range,
         selectionRange: nameNode.range,
-        children: body.elements.flatMap(e => this.getElementsSymbol(e, readStrictFqn(astElement.element))),
+        children: body.elements.flatMap(e => this.getElementsSymbol(e, fqn)),
       },
     ]
   }

Also applies to: 187-195

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

In `@packages/language-server/src/lsp/DocumentSymbolProvider.ts` around lines 164
- 167, The getElementsSymbol method currently accepts parentFqn but never uses
it and also computes fqn twice; fix by either wiring parentFqn into downstream
naming (pass parentFqn into any recursive calls or symbol-name builders used in
getElementsSymbol) or remove the unused parentFqn parameter, and in either case
stop recomputing the same value — reuse the local fqn variable (computed at line
~187) wherever the inline recomputation occurs (line ~195) and ensure recursive
calls (if any) use the chosen fqn propagation logic so symbols are named
consistently; update the signature of getElementsSymbol (and callers) only if
you remove parentFqn.
packages/language-server/src/lsp/CompletionProvider.ts-92-95 (1)

92-95: ⚠️ Potential issue | 🟡 Minor

Handle empty project lists before building the import choice snippet.

If ProjectsManager.all is empty, this produces \${1||}, which is not a valid choice placeholder. In an empty or not-yet-indexed workspace, the import completion will insert malformed snippet text instead of a usable placeholder.

Suggested fix
-      case 'import' === keyword.value:
-        acceptSnippet({
-          insertText: `{ $0 } from '\${1|${this.services.shared.workspace.ProjectsManager.all.join(',')}|}'`,
-        })
-        break
+      case 'import' === keyword.value: {
+        const projectIds = this.services.shared.workspace.ProjectsManager.all
+        const projectRef = projectIds.length > 0
+          ? `\${1|${projectIds.join(',')}|}`
+          : '${1:project}'
+        acceptSnippet({
+          insertText: `{ $0 } from '${projectRef}'`,
+        })
+        break
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/language-server/src/lsp/CompletionProvider.ts` around lines 92 - 95,
The import completion builds a choice snippet from
this.services.shared.workspace.ProjectsManager.all which yields an invalid
`\${1||}` when the list is empty; update the case handling for when
keyword.value === 'import' (the acceptSnippet call) to detect if
ProjectsManager.all is empty and, if so, use a simple placeholder (e.g.
`${1:project}` or `${1:''}`) instead of a choice list, otherwise build the
`\${1|...|}` choice from the joined project names; ensure the conditional
selects the correct insertText string before calling acceptSnippet so no
malformed snippet is emitted.
packages/vscode-preview/protocol.ts-30-39 (1)

30-39: ⚠️ Potential issue | 🟡 Minor

Add an error state to communicate failed AI layout operations to the webview.

The state union only covers 'in-progress' | 'completed'. When AI layout operations fail (API errors, timeouts, cancellations), no completion notification is sent, leaving the webview's loading spinner stuck indefinitely. Add 'error' to the state union and ensure handlers send a completion notification on failures:

Current notification definition
export const BroadcastAILayoutStateUpdate: NotificationType<{
  viewId: ViewId
  projectId: ProjectId
-  state: 'in-progress' | 'completed'
+  state: 'in-progress' | 'completed' | 'error'
}> = {
  method: 'ai-layout-update',
}

The handlers in semanticLayoutWithAI.ts and useLikeC4ChatParticipant.ts need to send the error state when operations fail so the webview can hide the spinner.

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

In `@packages/vscode-preview/protocol.ts` around lines 30 - 39, Update the
BroadcastAILayoutStateUpdate notification type to include an 'error' variant in
the state union (change state: 'in-progress' | 'completed' to 'in-progress' |
'completed' | 'error'), then update the failure paths in the AI layout handlers
(referencing the semanticLayoutWithAI.ts handler functions and the
useLikeC4ChatParticipant.ts participants) to send a BroadcastAILayoutStateUpdate
with state: 'error' (and include any error info in the payload if available)
whenever an API error, timeout, or cancellation occurs so the webview can stop
the spinner.
packages/vscode-preview/src/state.ts-12-12 (1)

12-12: ⚠️ Potential issue | 🟡 Minor

Remove unused onMount import (Line 12).

onMount is imported but never used; this will keep showing up in static analysis/lint output.

Suggested fix
-import { atom, batched, onMount, onSet } from 'nanostores'
+import { atom, batched, onSet } from 'nanostores'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode-preview/src/state.ts` at line 12, The import list in state.ts
includes an unused symbol onMount which triggers lint/static analysis warnings;
remove onMount from the import statement (keep atom, batched, onSet) so the
top-level import reads only the actually used symbols, ensuring no other code
references onMount before removing it.
packages/layouts/src/graphviz/ai/prompt-system.md-52-52 (1)

52-52: ⚠️ Potential issue | 🟡 Minor

Fix typo in rule field name.

excludeFromRanking arra should be excludeFromRanking array.

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

In `@packages/layouts/src/graphviz/ai/prompt-system.md` at line 52, Fix the typo
in the documentation: change the text fragment that reads "excludeFromRanking
arra" to "excludeFromRanking array" in the sentence describing the
excludeFromRanking behavior (the rule name is excludeFromRanking); ensure only
the word "arra" is replaced with "array" so the rule name and surrounding
explanation remain unchanged.
packages/vscode/data/skills/likec4-dsl/references/deployment.md-108-108 (1)

108-108: ⚠️ Potential issue | 🟡 Minor

Tighten this sentence for the skill data.

"Strict eval requires named instance identifier" is awkward English and harder for the model to learn from. "Strict eval requires a named instance identifier" or "requires the instance to be named" is clearer.

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

In `@packages/vscode/data/skills/likec4-dsl/references/deployment.md` at line 108,
The sentence "Strict eval requires named instance identifier" is awkward; update
the text in deployment.md (the table row that currently reads "Strict eval
requires named instance identifier" under the feature column and the example
column showing "Named: `IDENTIFIER = instanceOf ELEMENT`") to clearer wording
such as "Strict eval requires a named instance identifier" or "Strict eval
requires the instance to be named" so the skill data reads unambiguously.
packages/vscode/data/skills/likec4-dsl/references/model.md-139-140 (1)

139-140: ⚠️ Potential issue | 🟡 Minor

Keep this invalid-extension example consistent with the contract above.

Lines 139-140 say omitting KIND "silently targets the wrong relation", but Lines 51-57 define that case as wrong/invalid whenever a typed relationship exists. That contradiction will train the skill toward the wrong matcher behavior.

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

In `@packages/vscode/data/skills/likec4-dsl/references/model.md` around lines 139
- 140, Update the invalid-extension example wording so it matches the contract
defined earlier: change the phrase on the example around KIND (lines referencing
the comment about omitting KIND) from "silently targets the wrong relation" to
state that omitting KIND is invalid whenever a typed relationship exists; ensure
the example text for the snippet extend cloud.ui.dashboard -> cloud.backend.api
"calls" { ... } explicitly labels that omission as invalid (per the rule in the
block around lines 51-57) rather than implying silent targeting.
packages/vscode/tsdown.config.ts-112-116 (1)

112-116: ⚠️ Potential issue | 🟡 Minor

Clean destination before copying skills to avoid stale artifacts.

Line 115 copies recursively but never removes previously copied files. If a skill file is removed upstream, stale files can remain in ./data/skills/likec4-dsl.

💡 Suggested fix
 async function copySkills() {
   const skillDir = resolve('../../skills/likec4-dsl')
   console.info('Copy SKILLs: %s', skillDir)
+  emptyDir('./data/skills/likec4-dsl')
+  await mkdir('./data/skills/likec4-dsl', { recursive: true })
   await cp(skillDir, './data/skills/likec4-dsl', { recursive: true })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/tsdown.config.ts` around lines 112 - 116, The copySkills
function currently copies from resolve('../../skills/likec4-dsl') into
'./data/skills/likec4-dsl' without removing any existing files, causing stale
artifacts; update copySkills to check for the destination
'./data/skills/likec4-dsl', delete it recursively (e.g., using fs.promises.rm or
fs.rmSync with recursive/force) before calling cp, and ensure any errors during
removal are handled or propagated so the subsequent await cp(skillDir,
'./data/skills/likec4-dsl', { recursive: true }) writes a fresh copy.
packages/vscode/data/skills/likec4-dsl/references/predicates.md-9-10 (1)

9-10: ⚠️ Potential issue | 🟡 Minor

Remove the stray quote in the predicate-types list.

The trailing ' after ``include only makes this read like a broken code token in the skill reference.

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

In `@packages/vscode/data/skills/likec4-dsl/references/predicates.md` around lines
9 - 10, Remove the stray trailing apostrophe in the predicate-types list: locate
the line containing "Custom predicates - used to override properties of
elements/relationships (can be used with `include` only)`" in predicates.md and
delete the final single-quote character so the text ends after the closing
backtick; ensure the code token remains as `include` only with no extra
punctuation.
packages/vscode/src/commands/semanticLayoutWithAI.ts-132-140 (1)

132-140: ⚠️ Potential issue | 🟡 Minor

Handle CancellationError separately to avoid showing error toast on user cancellation.

When sendRequest() within enhanceLayoutWithAI() throws vscode.CancellationError() (thrown at line 286 when cancellation is detected), it currently falls through to the generic error handler and displays an error toast. User cancellations should exit silently instead.

Add an explicit check before the current else clause to handle error instanceof vscode.CancellationError without showing any message.

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

In `@packages/vscode/src/commands/semanticLayoutWithAI.ts` around lines 132 - 140,
The catch block in enhanceLayoutWithAI currently treats vscode.CancellationError
like a real failure and shows an error toast; update the catch to check for
cancellation first (if error instanceof vscode.CancellationError) and
return/exit silently without logging or showing a message, before the existing
"No language model" and generic error branches; reference the catch in
semanticLayoutWithAI.ts that surrounds the call to
sendRequest()/enhanceLayoutWithAI() and use the vscode.CancellationError type
for the check.
🧹 Nitpick comments (7)
packages/vscode/src/useRpc.ts (1)

91-103: Consider using true as const for consistent type narrowing.

The error case uses false as const for precise type narrowing, but the success case uses res.success which loses the literal type. For consistency and better discriminated union support:

       return {
         location: res.location ? client.protocol2CodeConverter.asLocation(res.location) : null,
-        success: res.success,
+        success: true as const,
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/src/useRpc.ts` around lines 91 - 103, In changeView, the
success branch returns success: res.success which loses the literal boolean
type—change it to return success: true as const (while keeping the mapped
location logic) so the returned discriminated union uses a literal true; locate
this in the async changeView method that calls queue(() =>
client.sendRequest(ChangeView.req, req)) and adjust the success return object to
use true as const instead of res.success.
packages/layouts/turbo.json (1)

7-17: Consider adding dependsOn to the generate task.

This helps Turbo orchestrate generation order predictably when multiple packages define generate.

🔁 Suggested update
   "tasks": {
     "generate": {
+      "dependsOn": [
+        "^generate"
+      ],
       "inputs": [
         "scripts/**",
         "src/**/*.md",
         "package.json"
       ],
Based on learnings: Configure Turbo task dependencies in `turbo.json` to orchestrate build order across the monorepo.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/layouts/turbo.json` around lines 7 - 17, Add a "dependsOn" entry to
the "generate" task so Turbo can order generation across packages; update the
"generate" object to include dependsOn (e.g. ["^generate"]) to make each
package's generate task depend on its workspace ancestors' generate tasks,
ensuring predictable orchestration when multiple packages define "generate".
packages/vscode/src/useMessenger.ts (1)

129-130: Reuse the shared AI-layout payload type instead of re-declaring it inline.

This anonymous { viewId; projectId; state } shape duplicates the contract already owned by BroadcastAILayoutStateUpdate. Pull it into an exported type and reuse it on both sides so the extension and preview can't drift independently.

As per coding guidelines, "Agent types and interfaces should be defined separately and reused across the codebase".

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

In `@packages/vscode/src/useMessenger.ts` around lines 129 - 130, The inline
anonymous payload in broadcastAiLayoutUpdate should be replaced with a shared
exported type so both sender and receiver use the same contract; define and
export a reusable interface/type (e.g., AILayoutStatePayload or reuse the
existing BroadcastAILayoutStateUpdate payload type) and update
broadcastAiLayoutUpdate's params signature to reference that exported type
instead of the inline { viewId; projectId; state } shape, ensuring the
messenger.sendNotification call still passes the same object shape.
packages/vscode/src/node/useLikeC4ChatParticipant.ts (1)

190-197: Remove obsolete commented block to keep the participant lean.

Lines 190-197 are stale commented code paths. Keeping them around makes this hot path noisier to maintain.

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

In `@packages/vscode/src/node/useLikeC4ChatParticipant.ts` around lines 190 - 197,
Remove the stale commented block that defines participant.titleProvider and the
commented useChatParticipant call in useLikeC4ChatParticipant.ts: delete the
commented lines referencing participant.titleProvider, provideChatTitle, and the
commented useChatParticipant('likec4', ..., { iconPath:
vscode.Uri.joinPath(extensionContext.value!.extensionUri,
'data/icon-256-light.png') }) so the file only contains active, relevant
participant setup and remains lean.
packages/layouts/src/graphviz/ElementViewPrinter.ts (1)

98-100: Avoid as any for the rank-block marker.

The new coordination flag is now hidden behind as any, which makes key drift harder to catch. A small typed helper/constant would keep this path aligned with the repo’s TypeScript rules.

As per coding guidelines, "Use TypeScript with explicit types; avoid using any and type casts with as".

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

In `@packages/layouts/src/graphviz/ElementViewPrinter.ts` around lines 98 - 100,
Replace the ad-hoc cast in ElementViewPrinter where
this.G.set('likec4_rankBlocks' as any, applied) is used: define a typed
constant/helper (e.g. const LIKEC4_RANK_BLOCKS_KEY = 'likec4_rankBlocks' as
const or an appropriately typed keyof/enum value) and use that constant in the
this.G.set call instead of using as any, keeping the key name likec4_rankBlocks
and matching the G map's expected key type so TypeScript catches any drift.
packages/layouts/src/graphviz/AiLayoutPrinter.ts (1)

166-170: Model synthetic invisible edges explicitly instead of casting them through any.

origin: 'invisible' as any hides that these injected edges do not match GraphologyEdgeAttributes. Please widen the edge-attribute model for synthetic edges rather than lying to the type system here.

As per coding guidelines, "Use TypeScript with explicit types; avoid using any and type casts with as."

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

In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts` around lines 166 - 170, The
code casts synthetic edges with origin: 'invisible' as any; instead extend the
Graphology edge-attribute type to explicitly allow synthetic attributes and use
that type when calling graphology.addEdgeWithKey. Create a new type (e.g.,
SyntheticEdgeAttributes extends GraphologyEdgeAttributes { origin?: 'invisible';
hierarchyDistance?: number; weight?: number }) or widen the existing
GraphologyEdgeAttributes union, then replace the inline cast and pass an object
typed as SyntheticEdgeAttributes (referencing addEdgeWithKey, edgeId, source,
target and the origin property) so the injected invisible edges are correctly
typed without using any.
packages/vscode/vscode.proposed.chatParticipantAdditions.d.ts (1)

1-6: Vendored VS Code proposed API declaration file - looks appropriate.

This file correctly mirrors VS Code's proposed chatParticipantAdditions API declarations needed for the experimental chat participant feature. The Microsoft copyright and version marker indicate this is sourced from VS Code's proposed APIs.

Consider adding a brief comment after the version marker to clarify that this file is vendored and should be updated by copying from VS Code's proposed API definitions rather than edited manually:

📝 Suggested documentation
 // version: 3
+// This file is vendored from VS Code's proposed APIs. Do not edit manually.
+// Source: https://github.com/microsoft/vscode/blob/main/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/vscode.proposed.chatParticipantAdditions.d.ts` around lines 1
- 6, Add a short clarifying comment immediately after the "// version: 3" marker
stating that this is a vendored VS Code proposed API declaration and should be
refreshed by copying from VS Code's proposed API definitions rather than edited
in place; update the file header near the existing copyright block and the "//
version: 3" line so maintainers can quickly find and recognize the vendored
chatParticipantAdditions declaration.
🤖 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/core/src/utils/unsafe.ts`:
- Around line 5-7: The exported unsafePartial function uses an unchecked `as`
cast and is unused; remove it from production exports (delete unsafePartial from
packages/core/src/utils/unsafe.ts and remove its re-export in utils index), or
move the helper into test code (e.g., a *.spec.ts helper) if needed for tests,
or replace it with an assertion-style API (rename to assertIsComplete<T>(value:
Partial<NoInfer<T>>): asserts value is T and implement runtime checks before
asserting) and only export that safe/assertion API; update or remove references
to unsafePartial accordingly.

In `@packages/layouts/scripts/generate.ts`:
- Line 5: The template literal generation for LAYOUT_SYSTEM_PROMPT only escapes
backticks but not interpolation markers, so any `${...}` in systemPrompt can be
interpreted; update the string preparation for the generated constant (where
generated is built from systemPrompt) to also escape interpolation markers by
replacing all occurrences of `${` with `\${` (in addition to the existing
backtick escape) before injecting into the template literal.

In `@packages/layouts/src/graphviz/ai/llm-input.ts`:
- Around line 108-115: The current code finds a nonLeafEdge and aborts by
logging and throwing, which rejects any view containing compound endpoints;
instead, iterate view.edges and for each edge either serialize it (use
ElementViewPrinter.addEdge() which supports compound endpoints) or, if truly
unsupported, skip that single edge with a non-fatal log (e.g. logger.warn)
rather than throwing; replace the single nonLeafEdge check and throw with
per-edge handling that calls addEdge for edges where leafs.has(source) &&
leafs.has(target) or otherwise calls addEdge when compound endpoints are
supported, and only skip/log the unsupported ones so the rest of the view can be
serialized.

In `@packages/layouts/src/graphviz/ai/llm-output.ts`:
- Around line 121-130: Change nodeId and edgeId to return undefined instead of
throwing for unknown serialized IDs (so they become safe lookups), update
mapToNodeId to map and filter out undefineds, and modify mapToNonEmpty to accept
a mapper that can return undefined, filter those out, and return undefined if
the resulting array is empty; finally ensure callers that build AILayoutHints
(e.g., the parseOutput flow that uses ranks, edgeOrder, edgeWeight, etc.) filter
out undefined entries per-field before constructing the final hints object so a
single hallucinated n*/e* doesn't invalidate the whole response.

In `@packages/layouts/src/graphviz/ai/orchestrator.ts`:
- Around line 17-18: The function promises to "never throw" but currently awaits
prepareLLMInput(), provider.sendRequest(), and parseOutput() without error
handling; wrap the sequence that calls prepareLLMInput, provider.sendRequest,
and parseOutput in a single try/catch inside the orchestrator function so any
provider, cancellation, or parse error is caught and the function returns
undefined (allowing the plain-Graphviz fallback) instead of rethrowing; ensure
the catch logs or records the error (if needed) but does not propagate it.

In `@packages/layouts/src/graphviz/ai/prompt-system.md`:
- Around line 86-90: The prompt currently contradicts itself by demanding "valid
JSON only" while the example contains JavaScript-style comments and an invalid
sample value (edgeWeight.e3 = 0) outside the declared 1..10 range; update the
prompt text and examples in prompt-system.md so the sample is a single compact,
uncommented JSON object that obeys the schema (omit fields that are empty,
include a "reasoning" string if used) and replace the invalid value
(edgeWeight.e3) with a number in 1..10; ensure no // comments remain in the
examples and that the instruction about "All fields are optional" and the
"reasoning" field usage are reflected exactly in the valid JSON sample.

In `@packages/layouts/src/graphviz/GraphvizLayoter.ts`:
- Around line 158-171: aiLayout currently returns the parsed diagram directly
and skips the post-processing performed by layout() for dynamic views, so
AI-enhanced dynamic views end up with a different diagram shape; after obtaining
diagram = parseGraphvizJson(json, view) in aiLayout, run the same dynamic-view
post-processing that layout() uses (i.e., compute and attach
sequenceLayout/sequence information for dynamic views using the same condition
and helper used in layout()) before returning, ensuring LayoutResult matches the
normal path.

In `@packages/layouts/src/graphviz/GraphvizParser.ts`:
- Around line 245-251: The current code swallows parse errors for individual
edges by catching exceptions around parseGraphvizEdge(...) and calling
logger.warn(loggable(e)), which yields silently incomplete layouts; instead
remove the silent swallow and rethrow the error (or throw a new Error with
contextual info including graphvizEdge, computedEdge, and view.id) from the
catch block so the caller of GraphvizParser/parseGraphvizEdge sees the failure
and can retry/fallback at the whole-layout level; update the catch to propagate
the error rather than logging only.

In `@packages/layouts/src/graphviz/wasm/GraphvizWasmAdapter.ts`:
- Around line 61-63: The retry currently calls fn() directly, bypassing
GraphvizWasmAdapter ops bookkeeping (GraphvizWasmAdapter.opsCount) and the
unload-after-20 workaround; change the retry to re-run the same wrapped
execution that increments opsCount and triggers the unload logic instead of
invoking fn() directly—e.g., call the adapter's existing execution wrapper (the
method that originally invoked fn and updates opsCount) or a small helper like
runWithBookkeeping(fn) so retries reuse the same bookkeeping and preserve
this/context and arguments.

In `@packages/layouts/validate.ts`:
- Around line 189-202: enhanceLayoutWithAI can return undefined on
parse/validation failure, but the code currently calls nonNullable(hints) which
will crash; update the flow after calling enhanceLayoutWithAI (the hints
variable) to explicitly check for undefined/null and handle it with a targeted
error message or early return before calling layouter.printToDot — e.g., guard
hints and log/throw a clear error like "AI layout hints missing for amazonView"
so layouter.printToDot is only invoked when hints is present.
- Around line 30-38: The file reads for systemPrompt
(fs.readFileSync('src/graphviz/ai/prompt-system.md', ...)) and model
(fs.readJsonSync('./model.json')) assume process.cwd(); change them to resolve
paths relative to the script using import.meta.url: derive the script directory
via fileURLToPath(import.meta.url) and path.dirname, then join that directory
with 'src/graphviz/ai/prompt-system.md' and 'model.json' before calling
fs.readFileSync/fs.readJsonSync so the reads work regardless of the current
working directory.

In `@packages/vscode/data/skills/likec4-dsl/references/identifier-validity.md`:
- Around line 5-7: The identifier regex (`/^[a-zA-Z_][a-zA-Z0-9_-]*$/`)
currently allows a trailing hyphen while the invalid examples list includes
"payment-" and the DSL grammar accepts the trailing-hyphen form; fix by making
the regex and grammar consistent with the intended rule: either (A) change the
regex to disallow a trailing hyphen by requiring the final character be an
alphanumeric or underscore (so hyphens may only appear between
alphanumeric/underscore characters), and update the DSL grammar productions to
enforce the same constraint, or (B) remove "payment-" from the invalid examples
and adjust any grammar restrictions so trailing hyphens are permitted; update
the identifier regex literal, the invalid examples table (remove or add
"payment-"), and the DSL rule(s) that define identifiers to match the chosen
behavior.

In `@packages/vscode/src/commands/semanticLayoutWithAI.ts`:
- Around line 42-149: The code currently always calls
messenger.broadcastAiLayoutUpdate({ viewId, projectId, state: 'completed' })
after vscode.window.withProgress even if earlier returns/cancellations/errors
happened; move the broadcast so it only runs after a successful apply: after
rpc.changeView returns saved with saved.success === true (and after any
saved.location logging) call messenger.broadcastAiLayoutUpdate with state
'completed'; ensure early returns (model missing, computedView missing, hints
null, token.isCancellationRequested, layout failure, or save failure) do not
reach that broadcast (i.e., remove the current trailing call and place the
success broadcast immediately after the successful save block), referencing
enhanceLayoutWithAI, rpc.layoutView, rpc.changeView,
token.isCancellationRequested, and messenger.broadcastAiLayoutUpdate to locate
the spots to change.

In `@packages/vscode/src/node/mcp-server.ts`:
- Around line 70-80: The informational logging calls using
mcp.sendLoggingMessage (e.g., the initial server-ready send and the per-project
sends inside the for loop over likec4.languageServices.projects()) must be made
best-effort so failed promises don't abort bootstrap; wrap each
sendLoggingMessage call in a local try/catch (or attach .catch()) and
swallow/log the error locally instead of letting it propagate, ensuring
bootstrap continues even if telemetry fails.

In `@packages/vscode/src/node/useLikeC4ChatParticipant.ts`:
- Around line 84-125: The code marks AI work as completed and disposes the
cancelStream too early and skips disposal on errors; wrap the
enhanceLayoutWithAI call and subsequent layout application (e.g., calls to
layoutView/changeView) in try/catch/finally so that cancelStream.dispose()
always runs in finally, only call messenger.broadcastAiLayoutUpdate({ state:
'completed', ... }) after successfully validating/applying/saving the hints, and
send a failure/canceled broadcast inside the catch for errors or cancellations;
look for enhanceLayoutWithAI, cancelStream.dispose(), and
messenger.broadcastAiLayoutUpdate to implement these flow changes.

In `@packages/vscode/src/node/useMcpRegistration.ts`:
- Around line 26-27: The code is serializing the Ref returned by
useWorkspaceId() instead of its string value, causing “[object Object]” to be
sent; update uses of workspaceId in the MCP registration payloads to read
workspaceId.value (e.g., where workspaceId is passed into JSON.stringify or used
as the workspace identifier in registerWithMcp / sendRegistrationPayload) so the
actual string ID is serialized rather than the Ref object.

In `@packages/vscode/src/node/useTelemetry.ts`:
- Around line 26-37: The new telemetry callbacks (sendErrorData and
sendEventData) bypass the existing dev-mode suppression: update the
telemetryLogger setup so these callbacks honor the same
dev-mode/telemetry-enabled guard used by sendTelemetry/sendTelemetryError (e.g.,
check vscode.env.isTelemetryEnabled or your reporter's dev-mode flag) before
calling reporter.sendTelemetryErrorEvent or reporter.sendTelemetryEvent; ensure
this check is applied inside sendErrorData and sendEventData in the
useDisposable(...) block so logError/logUsage still no-op in dev builds.

---

Outside diff comments:
In `@packages/language-server/src/filesystem/LikeC4ManualLayouts.ts`:
- Around line 128-136: The snapshot hash is non-deterministic because it
accumulates via iterating the unsorted array from fs.scanDirectory; before the
for (const file of files) loop in LikeC4ManualLayouts.ts, sort the files array
deterministically (e.g., files.sort((a,b) =>
a.uri.toString().localeCompare(b.uri.toString())) or by path) so stringHash
receives a stable order, and apply the same sorting change where files are
scanned/hashed again (the other occurrence around the later hash accumulation).

In `@packages/vscode/src/node/mcp-server.ts`:
- Around line 54-61: The code collapses multi-root workspaces by taking only
first(workspacePaths) and calling fromWorkspace(workspacePath); instead, handle
all parsed entries from readEnvVar('LIKEC4_WORKSPACE') (workspacePaths) rather
than narrowing to workspacePath: either call fromWorkspace for each path in
workspacePaths (falling back to process.cwd() when workspacePaths is empty) or
update fromWorkspace to accept an array and pass workspacePaths directly; update
bootstrap to iterate and register/initialize each resulting likec4 instance
appropriately so every folder in the multi-root workspace is visible.

---

Minor comments:
In @.vscode/launch.json:
- Around line 99-103: The launch configuration incorrectly places the Node
runtime flag "--conditions=sources" under "args" (which are passed to the
program) instead of "runtimeArgs" (which are passed to the runtime); update the
launch.json so that when "runtimeExecutable": "tsx" is used the
"--conditions=sources" entry is moved from the "args" array into the
"runtimeArgs" array (remove it from "args" and add it to "runtimeArgs") so
tsx/Node receives the flag correctly.

In `@packages/language-server/src/lsp/CompletionProvider.ts`:
- Around line 92-95: The import completion builds a choice snippet from
this.services.shared.workspace.ProjectsManager.all which yields an invalid
`\${1||}` when the list is empty; update the case handling for when
keyword.value === 'import' (the acceptSnippet call) to detect if
ProjectsManager.all is empty and, if so, use a simple placeholder (e.g.
`${1:project}` or `${1:''}`) instead of a choice list, otherwise build the
`\${1|...|}` choice from the joined project names; ensure the conditional
selects the correct insertText string before calling acceptSnippet so no
malformed snippet is emitted.

In `@packages/language-server/src/lsp/DocumentSymbolProvider.ts`:
- Around line 164-167: The getElementsSymbol method currently accepts parentFqn
but never uses it and also computes fqn twice; fix by either wiring parentFqn
into downstream naming (pass parentFqn into any recursive calls or symbol-name
builders used in getElementsSymbol) or remove the unused parentFqn parameter,
and in either case stop recomputing the same value — reuse the local fqn
variable (computed at line ~187) wherever the inline recomputation occurs (line
~195) and ensure recursive calls (if any) use the chosen fqn propagation logic
so symbols are named consistently; update the signature of getElementsSymbol
(and callers) only if you remove parentFqn.

In `@packages/layouts/src/graphviz/ai/prompt-system.md`:
- Line 52: Fix the typo in the documentation: change the text fragment that
reads "excludeFromRanking arra" to "excludeFromRanking array" in the sentence
describing the excludeFromRanking behavior (the rule name is
excludeFromRanking); ensure only the word "arra" is replaced with "array" so the
rule name and surrounding explanation remain unchanged.

In `@packages/vscode-preview/protocol.ts`:
- Around line 30-39: Update the BroadcastAILayoutStateUpdate notification type
to include an 'error' variant in the state union (change state: 'in-progress' |
'completed' to 'in-progress' | 'completed' | 'error'), then update the failure
paths in the AI layout handlers (referencing the semanticLayoutWithAI.ts handler
functions and the useLikeC4ChatParticipant.ts participants) to send a
BroadcastAILayoutStateUpdate with state: 'error' (and include any error info in
the payload if available) whenever an API error, timeout, or cancellation occurs
so the webview can stop the spinner.

In `@packages/vscode-preview/src/state.ts`:
- Line 12: The import list in state.ts includes an unused symbol onMount which
triggers lint/static analysis warnings; remove onMount from the import statement
(keep atom, batched, onSet) so the top-level import reads only the actually used
symbols, ensuring no other code references onMount before removing it.

In `@packages/vscode/data/skills/likec4-dsl/references/deployment.md`:
- Line 108: The sentence "Strict eval requires named instance identifier" is
awkward; update the text in deployment.md (the table row that currently reads
"Strict eval requires named instance identifier" under the feature column and
the example column showing "Named: `IDENTIFIER = instanceOf ELEMENT`") to
clearer wording such as "Strict eval requires a named instance identifier" or
"Strict eval requires the instance to be named" so the skill data reads
unambiguously.

In `@packages/vscode/data/skills/likec4-dsl/references/model.md`:
- Around line 139-140: Update the invalid-extension example wording so it
matches the contract defined earlier: change the phrase on the example around
KIND (lines referencing the comment about omitting KIND) from "silently targets
the wrong relation" to state that omitting KIND is invalid whenever a typed
relationship exists; ensure the example text for the snippet extend
cloud.ui.dashboard -> cloud.backend.api "calls" { ... } explicitly labels that
omission as invalid (per the rule in the block around lines 51-57) rather than
implying silent targeting.

In `@packages/vscode/data/skills/likec4-dsl/references/predicates.md`:
- Around line 9-10: Remove the stray trailing apostrophe in the predicate-types
list: locate the line containing "Custom predicates - used to override
properties of elements/relationships (can be used with `include` only)`" in
predicates.md and delete the final single-quote character so the text ends after
the closing backtick; ensure the code token remains as `include` only with no
extra punctuation.

In `@packages/vscode/src/commands/semanticLayoutWithAI.ts`:
- Around line 132-140: The catch block in enhanceLayoutWithAI currently treats
vscode.CancellationError like a real failure and shows an error toast; update
the catch to check for cancellation first (if error instanceof
vscode.CancellationError) and return/exit silently without logging or showing a
message, before the existing "No language model" and generic error branches;
reference the catch in semanticLayoutWithAI.ts that surrounds the call to
sendRequest()/enhanceLayoutWithAI() and use the vscode.CancellationError type
for the check.

In `@packages/vscode/tsdown.config.ts`:
- Around line 112-116: The copySkills function currently copies from
resolve('../../skills/likec4-dsl') into './data/skills/likec4-dsl' without
removing any existing files, causing stale artifacts; update copySkills to check
for the destination './data/skills/likec4-dsl', delete it recursively (e.g.,
using fs.promises.rm or fs.rmSync with recursive/force) before calling cp, and
ensure any errors during removal are handled or propagated so the subsequent
await cp(skillDir, './data/skills/likec4-dsl', { recursive: true }) writes a
fresh copy.

---

Nitpick comments:
In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts`:
- Around line 166-170: The code casts synthetic edges with origin: 'invisible'
as any; instead extend the Graphology edge-attribute type to explicitly allow
synthetic attributes and use that type when calling graphology.addEdgeWithKey.
Create a new type (e.g., SyntheticEdgeAttributes extends
GraphologyEdgeAttributes { origin?: 'invisible'; hierarchyDistance?: number;
weight?: number }) or widen the existing GraphologyEdgeAttributes union, then
replace the inline cast and pass an object typed as SyntheticEdgeAttributes
(referencing addEdgeWithKey, edgeId, source, target and the origin property) so
the injected invisible edges are correctly typed without using any.

In `@packages/layouts/src/graphviz/ElementViewPrinter.ts`:
- Around line 98-100: Replace the ad-hoc cast in ElementViewPrinter where
this.G.set('likec4_rankBlocks' as any, applied) is used: define a typed
constant/helper (e.g. const LIKEC4_RANK_BLOCKS_KEY = 'likec4_rankBlocks' as
const or an appropriately typed keyof/enum value) and use that constant in the
this.G.set call instead of using as any, keeping the key name likec4_rankBlocks
and matching the G map's expected key type so TypeScript catches any drift.

In `@packages/layouts/turbo.json`:
- Around line 7-17: Add a "dependsOn" entry to the "generate" task so Turbo can
order generation across packages; update the "generate" object to include
dependsOn (e.g. ["^generate"]) to make each package's generate task depend on
its workspace ancestors' generate tasks, ensuring predictable orchestration when
multiple packages define "generate".

In `@packages/vscode/src/node/useLikeC4ChatParticipant.ts`:
- Around line 190-197: Remove the stale commented block that defines
participant.titleProvider and the commented useChatParticipant call in
useLikeC4ChatParticipant.ts: delete the commented lines referencing
participant.titleProvider, provideChatTitle, and the commented
useChatParticipant('likec4', ..., { iconPath:
vscode.Uri.joinPath(extensionContext.value!.extensionUri,
'data/icon-256-light.png') }) so the file only contains active, relevant
participant setup and remains lean.

In `@packages/vscode/src/useMessenger.ts`:
- Around line 129-130: The inline anonymous payload in broadcastAiLayoutUpdate
should be replaced with a shared exported type so both sender and receiver use
the same contract; define and export a reusable interface/type (e.g.,
AILayoutStatePayload or reuse the existing BroadcastAILayoutStateUpdate payload
type) and update broadcastAiLayoutUpdate's params signature to reference that
exported type instead of the inline { viewId; projectId; state } shape, ensuring
the messenger.sendNotification call still passes the same object shape.

In `@packages/vscode/src/useRpc.ts`:
- Around line 91-103: In changeView, the success branch returns success:
res.success which loses the literal boolean type—change it to return success:
true as const (while keeping the mapped location logic) so the returned
discriminated union uses a literal true; locate this in the async changeView
method that calls queue(() => client.sendRequest(ChangeView.req, req)) and
adjust the success return object to use true as const instead of res.success.

In `@packages/vscode/vscode.proposed.chatParticipantAdditions.d.ts`:
- Around line 1-6: Add a short clarifying comment immediately after the "//
version: 3" marker stating that this is a vendored VS Code proposed API
declaration and should be refreshed by copying from VS Code's proposed API
definitions rather than edited in place; update the file header near the
existing copyright block and the "// version: 3" line so maintainers can quickly
find and recognize the vendored chatParticipantAdditions declaration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e70ed92d-eca9-4cff-9a45-ccde3303895f

📥 Commits

Reviewing files that changed from the base of the PR and between e98d9f4 and 0ed249e.

⛔ Files ignored due to path filters (11)
  • packages/generators/src/drawio/__snapshots__/parse-drawio.spec.ts.snap is excluded by !**/*.snap
  • packages/layouts/layout2.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/DeploymentViewPrinter-index.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-amazon.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-cloud.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-cloud3levels.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-index.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-no-relationships.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-two-directions.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-with-relationship.dot is excluded by !**/*.dot
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (92)
  • .changeset/ai-layout-experimental.md
  • .tool-versions
  • .vscode/launch.json
  • dprint.json
  • e2e/package.json
  • package.json
  • packages/config/src/node/load-config.ts
  • packages/core/src/utils/index.ts
  • packages/core/src/utils/unsafe.ts
  • packages/language-server/src/Rpc.ts
  • packages/language-server/src/common-exports.ts
  • packages/language-server/src/filesystem/LikeC4ManualLayouts.ts
  • packages/language-server/src/lsp/CompletionProvider.ts
  • packages/language-server/src/lsp/DocumentSymbolProvider.ts
  • packages/language-server/src/protocol.ts
  • packages/language-server/src/shared/NodeKindProvider.ts
  • packages/language-server/src/views/LikeC4Views.ts
  • packages/language-server/src/views/index.ts
  • packages/language-server/src/workspace/ProjectsManager.ts
  • packages/language-server/src/workspace/WorkspaceManager.ts
  • packages/language-services/src/node/index.ts
  • packages/layouts/.gitignore
  • packages/layouts/build.config.ts
  • packages/layouts/model.json
  • packages/layouts/package.json
  • packages/layouts/scripts/generate.ts
  • packages/layouts/scripts/tsconfig.json
  • packages/layouts/src/graphviz/AiLayoutPrinter.ts
  • packages/layouts/src/graphviz/DeploymentViewPrinter.ts
  • packages/layouts/src/graphviz/DotPrinter.ts
  • packages/layouts/src/graphviz/DynamicViewPrinter.ts
  • packages/layouts/src/graphviz/ElementViewPrinter.ts
  • packages/layouts/src/graphviz/GraphvizLayoter.ts
  • packages/layouts/src/graphviz/GraphvizParser.ts
  • packages/layouts/src/graphviz/ProjectsViewPrinter.ts
  • packages/layouts/src/graphviz/ai/index.ts
  • packages/layouts/src/graphviz/ai/llm-input.ts
  • packages/layouts/src/graphviz/ai/llm-output.ts
  • packages/layouts/src/graphviz/ai/logger.ts
  • packages/layouts/src/graphviz/ai/orchestrator.ts
  • packages/layouts/src/graphviz/ai/prompt-system.md
  • packages/layouts/src/graphviz/ai/types.ts
  • packages/layouts/src/graphviz/utils.ts
  • packages/layouts/src/graphviz/wasm/GraphvizWasmAdapter.ts
  • packages/layouts/turbo.json
  • packages/layouts/validate.ts
  • packages/vscode-preview/protocol.ts
  • packages/vscode-preview/src/main.tsx
  • packages/vscode-preview/src/state.ts
  • packages/vscode-preview/src/vscode.ts
  • packages/vscode/.gitignore
  • packages/vscode/data/skills/likec4-dsl/SKILL.md
  • packages/vscode/data/skills/likec4-dsl/evals/evals-public.json
  • packages/vscode/data/skills/likec4-dsl/evals/grading-spec.json
  • packages/vscode/data/skills/likec4-dsl/references/cli.md
  • packages/vscode/data/skills/likec4-dsl/references/configuration.md
  • packages/vscode/data/skills/likec4-dsl/references/deployment.md
  • packages/vscode/data/skills/likec4-dsl/references/dynamic-views.md
  • packages/vscode/data/skills/likec4-dsl/references/examples.md
  • packages/vscode/data/skills/likec4-dsl/references/identifier-validity.md
  • packages/vscode/data/skills/likec4-dsl/references/include-predicates-wildcards.md
  • packages/vscode/data/skills/likec4-dsl/references/model.md
  • packages/vscode/data/skills/likec4-dsl/references/predicates.md
  • packages/vscode/data/skills/likec4-dsl/references/relationships-bidirectional.md
  • packages/vscode/data/skills/likec4-dsl/references/specification.md
  • packages/vscode/data/skills/likec4-dsl/references/style-tokens-colors.md
  • packages/vscode/data/skills/likec4-dsl/references/troubleshooting.md
  • packages/vscode/data/skills/likec4-dsl/references/views.md
  • packages/vscode/package.json
  • packages/vscode/package.nls.json
  • packages/vscode/src/browser/useTelemetry.ts
  • packages/vscode/src/commands/index.ts
  • packages/vscode/src/commands/locate.ts
  • packages/vscode/src/commands/semanticLayoutWithAI.ts
  • packages/vscode/src/node/extension.ts
  • packages/vscode/src/node/language-server.ts
  • packages/vscode/src/node/mcp-server.ts
  • packages/vscode/src/node/useLanguageClient.ts
  • packages/vscode/src/node/useLikeC4ChatParticipant.ts
  • packages/vscode/src/node/useMcpRegistration.ts
  • packages/vscode/src/node/useTelemetry.ts
  • packages/vscode/src/panel/activateMessenger.ts
  • packages/vscode/src/panel/useDiagramPanel.ts
  • packages/vscode/src/useExtensionLogger.ts
  • packages/vscode/src/useMessenger.ts
  • packages/vscode/src/useRpc.ts
  • packages/vscode/tsconfig.json
  • packages/vscode/tsdown.config.ts
  • packages/vscode/turbo.json
  • packages/vscode/vscode.proposed.chatParticipantAdditions.d.ts
  • pnpm-workspace.yaml
  • tsconfig.json
💤 Files with no reviewable changes (3)
  • packages/config/src/node/load-config.ts
  • packages/layouts/src/graphviz/utils.ts
  • tsconfig.json

Comment thread packages/core/src/utils/unsafe.ts
Comment thread packages/layouts/scripts/generate.ts Outdated
Comment on lines +108 to +115
const nonLeafEdge = view.edges.find(e => !leafs.has(e.source) || !leafs.has(e.target))
if (nonLeafEdge) {
logger.error('Non-leaf edge found in view', {
view: view.id,
edge: { source: nonLeafEdge.source, target: nonLeafEdge.target },
})
throw new Error('Non-leaf edge found in view, this is not supported by the current implementation')
}

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 | 🟠 Major

Don't reject every view that has a compound endpoint.

This serializer currently aborts the entire AI path as soon as one edge touches a non-leaf node. ElementViewPrinter.addEdge() already supports compound endpoints, so container-to-node/container-to-container views will lose AI layout entirely here. Please serialize those endpoints too, or at least skip unsupported edges without failing the whole view.

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

In `@packages/layouts/src/graphviz/ai/llm-input.ts` around lines 108 - 115, The
current code finds a nonLeafEdge and aborts by logging and throwing, which
rejects any view containing compound endpoints; instead, iterate view.edges and
for each edge either serialize it (use ElementViewPrinter.addEdge() which
supports compound endpoints) or, if truly unsupported, skip that single edge
with a non-fatal log (e.g. logger.warn) rather than throwing; replace the single
nonLeafEdge check and throw with per-edge handling that calls addEdge for edges
where leafs.has(source) && leafs.has(target) or otherwise calls addEdge when
compound endpoints are supported, and only skip/log the unsupported ones so the
rest of the view can be serialized.

Comment thread packages/layouts/src/graphviz/ai/llm-output.ts Outdated
Comment thread packages/layouts/src/graphviz/ai/enhanceLayoutWithAI.ts
Comment thread packages/vscode/src/commands/semanticLayoutWithAI.ts
Comment thread packages/vscode/src/node/mcp-server.ts Outdated
Comment thread packages/vscode/src/node/useLikeC4ChatParticipant.ts Outdated
Comment thread packages/vscode/src/node/useMcpRegistration.ts Outdated
Comment thread packages/vscode/src/node/useTelemetry.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/layouts/src/graphviz/ai/llm-output.ts (2)

69-80: Minor: Use optional chaining for cleaner null check.

The conditional can be simplified using optional chaining as suggested by static analysis.

♻️ Suggested fix
 function extractJson(text: string): string {
   const match = jsonRegex.exec(text)
-  if (match && match[1]) {
+  if (match?.[1]) {
     return match[1]
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/layouts/src/graphviz/ai/llm-output.ts` around lines 69 - 80, In
extractJson, simplify the null check by using optional chaining on the regex
match (jsonRegex.exec(text)) so you can directly test and return match?.[1] (or
equivalent) instead of if (match && match[1]); update the reference to jsonRegex
and the local variable match accordingly and keep the rest of the fallback logic
(finding '{' and '}' and returning text.trim()) unchanged.

191-200: Escape regex special characters in ID replacement for defense-in-depth.

While the serialized IDs (n0, e1, etc.) are programmatically generated and unlikely to contain regex metacharacters, escaping them prevents potential issues if the ID format ever changes. This also addresses the static analysis ReDoS warning.

🛡️ Proposed fix
+  const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+
   let reasoning = parsed.reasoning
   if (reasoning) {
     reasoning = reasoning.replaceAll('][', '] [')
     entries(params.mapping.nodes).forEach(([id, nd]) => {
-      reasoning = reasoning.replaceAll(new RegExp(`\\[${id}\\]`, 'g'), '`' + nd.id + '`')
+      reasoning = reasoning.replaceAll(new RegExp(`\\[${escapeRegex(id)}\\]`, 'g'), '`' + nd.id + '`')
     })
     entries(params.mapping.edges).forEach(([id, e]) => {
-      reasoning = reasoning.replaceAll(new RegExp(`\\[${id}\\]`, 'g'), '`' + e.source + ' -> ' + e.target + '`')
+      reasoning = reasoning.replaceAll(new RegExp(`\\[${escapeRegex(id)}\\]`, 'g'), '`' + e.source + ' -> ' + e.target + '`')
     })
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/layouts/src/graphviz/ai/llm-output.ts` around lines 191 - 200, The
code constructs RegExp objects from node/edge IDs in the reasoning replacement
loop (see reasoning, entries(params.mapping.nodes) and
entries(params.mapping.edges) where replaceAll(new RegExp(`\\[${id}\\]`, 'g') is
used), which can be unsafe if IDs contain regex metacharacters; update the
replacement to escape regex-special characters in the ID before interpolating
into the RegExp (add a small utility to escape characters like .*+?^${}()|[]\\
and use that when building the RegExp) so the pattern matches the literal
bracketed ID and avoids potential ReDoS or mis-matching.
🤖 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/commands/semanticLayoutWithAI.ts`:
- Around line 155-171: The createOutputChannel function currently returns an
untyped object so TypeScript cannot infer it as an AsyncDisposable for the
`await using` call; update the function signature to explicitly return the
AsyncDisposable interface (or vscode.AsyncDisposable) and ensure the object
implements the async dispose method signature (Symbol.asyncDispose: () =>
Promise<void>) — reference createOutputChannel and the [Symbol.asyncDispose]
property and change the return type to AsyncDisposable (or
vscode.AsyncDisposable) and make the async dispose return a Promise<void> to
satisfy the static analyzer.
- Around line 225-229: Rename the typo'd parameter onStartRecevingFromModel to
onStartReceivingFromModel in the constructor signature of the class in
semanticLayoutWithAI.ts and update every usage and call site to match (including
the call that currently passes the callback where the constructor is invoked and
any references to the parameter inside methods). Ensure the parameter name
change is applied consistently (constructor parameter, property access, and any
places that call or pass the callback) so there are no unresolved identifier
errors.

In `@packages/vscode/src/node/useLikeC4ChatParticipant.ts`:
- Around line 91-94: There's a typo: rename the variable messsages to messages
in the useLikeC4ChatParticipant code where it's declared and update all
subsequent references (e.g., the places that consume it around the
Assistant/User message construction and the later usage on the following lines)
to use messages so the identifier is consistent; ensure you update both the
declaration and every reference (including the two usages noted after the
declaration).

---

Nitpick comments:
In `@packages/layouts/src/graphviz/ai/llm-output.ts`:
- Around line 69-80: In extractJson, simplify the null check by using optional
chaining on the regex match (jsonRegex.exec(text)) so you can directly test and
return match?.[1] (or equivalent) instead of if (match && match[1]); update the
reference to jsonRegex and the local variable match accordingly and keep the
rest of the fallback logic (finding '{' and '}' and returning text.trim())
unchanged.
- Around line 191-200: The code constructs RegExp objects from node/edge IDs in
the reasoning replacement loop (see reasoning, entries(params.mapping.nodes) and
entries(params.mapping.edges) where replaceAll(new RegExp(`\\[${id}\\]`, 'g') is
used), which can be unsafe if IDs contain regex metacharacters; update the
replacement to escape regex-special characters in the ID before interpolating
into the RegExp (add a small utility to escape characters like .*+?^${}()|[]\\
and use that when building the RegExp) so the pattern matches the literal
bracketed ID and avoids potential ReDoS or mis-matching.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4c13017-3965-4a14-bac3-23535b853c91

📥 Commits

Reviewing files that changed from the base of the PR and between 0ed249e and a6aa461.

📒 Files selected for processing (7)
  • packages/layouts/scripts/generate.ts
  • packages/layouts/src/graphviz/ai/llm-output.ts
  • packages/layouts/src/graphviz/ai/orchestrator.ts
  • packages/vscode-preview/protocol.ts
  • packages/vscode/src/commands/semanticLayoutWithAI.ts
  • packages/vscode/src/node/useLikeC4ChatParticipant.ts
  • packages/vscode/src/useMessenger.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/layouts/scripts/generate.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/vscode/src/useMessenger.ts
  • packages/vscode-preview/protocol.ts

Comment thread packages/vscode/src/commands/semanticLayoutWithAI.ts Outdated
Comment thread packages/vscode/src/commands/semanticLayoutWithAI.ts
Comment thread packages/vscode/src/node/useLikeC4ChatParticipant.ts Outdated
Comment thread packages/language-server/src/lsp/DocumentSymbolProvider.ts Fixed
Comment thread packages/layouts/src/graphviz/AiLayoutPrinter.ts Fixed
Comment thread packages/layouts/src/graphviz/AiLayoutPrinter.ts Fixed
Comment thread packages/layouts/src/graphviz/AiLayoutPrinter.ts Fixed
Comment thread packages/layouts/src/graphviz/AiLayoutPrinter.ts Fixed
Comment thread packages/layouts/validate.ts Fixed
Comment thread packages/layouts/validate.ts Fixed
Comment thread packages/layouts/validate.ts Fixed
Comment thread packages/mcp/src/server/createMCPServer.ts Fixed

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🧹 Nitpick comments (9)
packages/vscode/src/panel/activateMessenger.ts (1)

138-148: LGTM — conversion correctly centralized in useRpc.

Removing the panel-side protocol2CodeConverter.asLocation call is consistent with packages/vscode/src/useRpc.ts (lines 91-103), where changeView already returns a vscode.Location | null. The null-guard at line 139 keeps the subsequent showEditorNextToPreview call (which requires a non-null vscode.Location per packages/vscode/src/utils.ts) and vscode.workspace.save(location.uri) safe.

Optional nit: if result.location is typed as vscode.Location | null at the RPC boundary, the ?? null on line 138 is redundant and can be dropped:

♻️ Optional simplification
-      const location = result.location ?? null
-      if (!location) {
+      const { location } = result
+      if (!location) {
         logger.warn(`rpc.changeView returned null`)
         return result
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vscode/src/panel/activateMessenger.ts` around lines 138 - 148, The
code uses a redundant null-coalescing on result.location (result.location ??
null); remove the "?? null" so you simply read result.location (which is already
typed as vscode.Location | null at the RPC boundary) and keep the existing
null-guard and subsequent calls to showEditorNextToPreview and
vscode.workspace.save(location.uri) unchanged; update the reference in
activateMessenger where result.location is used to rely on its declared type
rather than coalescing to null.
packages/vscode/tsdown.config.ts (1)

27-27: Inconsistent ...shared spread placement across build targets.

The node-CJS build spreads ...shared at the top (line 27) so local fields win, while node-ESM (line 60) and both browser targets (lines 72, 86) spread it at the bottom so shared wins. Today there are no overlapping keys so behavior is equivalent, but any future addition to shared (e.g., sourcemap, define, external) will silently apply differently per target and be easy to miss. Consider normalizing placement (preferably ...shared first everywhere, with per-target overrides after) for predictable merge semantics.

Also applies to: 60-60, 72-72, 86-86

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

In `@packages/vscode/tsdown.config.ts` at line 27, The build target objects
currently mix placement of the shared spread (`...shared`) inconsistently
(node-CJS spreads it last while node-ESM and both browser targets spread it
first), which can change merge semantics; update each target object (node-CJS,
node-ESM, and both browser target objects) to place `...shared` consistently at
the start of the object literal so per-target fields defined after the spread
always override shared values, and ensure no other target reverses this
ordering.
packages/mcp/src/server/createMCPServer.ts (4)

146-146: Unused resource binding.

The return value of mcp.registerResource(...) is never referenced. Drop the assignment to avoid the dead variable (also fixes SonarCloud's useless assignment warning).

🧹 Cleanup
-  const resource = mcp.registerResource(
+  mcp.registerResource(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mcp/src/server/createMCPServer.ts` at line 146, The code assigns the
result of mcp.registerResource(...) to a local binding named resource but never
uses it; remove the dead binding by calling mcp.registerResource(...) directly
(i.e., replace "const resource = mcp.registerResource(...)" with just
"mcp.registerResource(...)" or otherwise discard the return) so the useless
assignment is eliminated and SonarCloud's warning is resolved.

120-120: Avoid the as ProjectId cast.

Coding guidelines discourage as casts. ctx?.arguments?.['projectId'] is typed as unknown/string; if parseModel requires a branded ProjectId, prefer going through services.projectsManager.ensureProjectId(...) (used elsewhere in this PR) which validates and returns the branded type, rather than a blind cast. As per coding guidelines: "Use TypeScript with explicit types; avoid using any and type casts with as".

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

In `@packages/mcp/src/server/createMCPServer.ts` at line 120, Replace the unsafe
cast to ProjectId when calling services.builder.parseModel: instead of passing
ctx?.arguments?.['projectId'] as ProjectId, validate and convert it using
services.projectsManager.ensureProjectId(...) (the same helper used elsewhere in
this PR) and pass the returned branded ProjectId into
services.builder.parseModel; update the call at services.builder.parseModel(...)
to use the ensured ProjectId to remove the as ProjectId cast.

48-75: apply-semantic-layout missing from the instructions tool inventory.

The new tool is registered at Line 144 but is not listed in the instructions block passed to the model. Since this string is the primary cue an LLM uses to discover capabilities, add a one-liner (and note its experimental status) so callers know it exists.

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

In `@packages/mcp/src/server/createMCPServer.ts` around lines 48 - 75, The
instructions string passed to the model is missing the new tool entry for
apply-semantic-layout; add a one-line entry to the tools/instructions block (the
multi-line string in createMCPServer.ts that lists "Available tools:")
describing "apply-semantic-layout — Apply semantic auto-layout to a view. Input:
{ viewId, project?, options? } (experimental)" so callers can discover this
capability and see its experimental status; locate the tools list in the
createMCPServer.ts instructions string and append the apply-semantic-layout
one-liner near the other tool entries.

109-112: Prefer a default parameter over nullish reassignment.

Matches the SonarCloud hint and aligns with the switch(true) / idiomatic guideline style used across the repo. The same nit applies to the inner const value = _value ?? '' at Line 124.

♻️ Proposed tidy-up
-        projectId: completable(projectIdSchema, (_value) => {
-          const value = _value ?? ''
-          return services.projects().filter(project => project.id.startsWith(value)).map(prop('id'))
-        }),
+        projectId: completable(projectIdSchema, (value = '') => {
+          return services.projects().filter(project => project.id.startsWith(value)).map(prop('id'))
+        }),
         viewId: completable(
           z.string().describe('View ID to apply semantic layout to'),
-          async (_value, ctx) => {
+          async (value = '', ctx) => {
             const projectId = ctx?.arguments?.['projectId']
             if (!projectId) {
               return []
             }
             const model = await services.builder.parseModel(projectId as ProjectId)
             if (!model) {
               return []
             }
-            const value = _value ?? ''
             return Array.from(model.views()).filter((view) => view.id.startsWith(value)).map(prop('id'))
           },
         ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mcp/src/server/createMCPServer.ts` around lines 109 - 112, The
inline completable for projectId uses a nullish reassignment; change the lambda
to use a default parameter and remove the redundant const assignment: update the
parameter signature of the completable callback (currently _value) to default to
'' (e.g., _value = '') and delete the inner const value = _value ?? '' so the
body can directly use the parameter when filtering via services.projects(). Also
apply the same change to the other similar lambda that creates const value to
keep style consistent with projectIdSchema and the repo's idiomatic pattern.
packages/mcp/src/tools/apply-semantic-layout.ts (2)

17-20: Remove commented-out completable block.

Dead/commented code should not land in main. If the completable variant is the intended direction, switch to it; otherwise delete these lines.

🧹 Proposed cleanup
       inputSchema: {
         projectId: projectIdSchema,
-        // projectId: completable(projectIdSchema, (_value) => {
-        //   const value = _value ?? ''
-        //   return languageServices.projects().filter(project => project.id.startsWith(value)).map(prop('id'))
-        // }),
         viewId: z.string().describe('View ID to apply semantic layout to'),
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mcp/src/tools/apply-semantic-layout.ts` around lines 17 - 20, Remove
the dead commented-out completable block around the projectId field in
apply-semantic-layout.ts: either delete the three commented lines starting with
"projectId: completable(projectIdSchema, ..." or, if you intend to enable the
completable behavior, uncomment and wire it up properly by using
completable(projectIdSchema, ...) and ensure it uses
languageServices.projects().filter(...).map(prop('id')); specifically address
the symbols projectId, completable, projectIdSchema, and
languageServices.projects() so the file contains no leftover commented code.

50-58: Dead fallback branch after zod validation.

The response is already validated against z.object({ content: z.object({ type: z.literal('text'), text: z.string() }) }), so response.content.type !== 'text' is unreachable. The ternary reduces to response.content.text.

🧹 Simplify
-          return response.content.type === 'text' ? response.content.text : JSON.stringify(response.content)
+          return response.content.text
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mcp/src/tools/apply-semantic-layout.ts` around lines 50 - 58, The
ternary returning response.content.type === 'text' ? response.content.text :
JSON.stringify(response.content) is dead because the response is validated by
z.object({ content: z.object({ type: z.literal('text'), text: z.string() }) }),
so simplify the function to directly return response.content.text; update the
code around the response handling in apply-semantic-layout (the validation and
the return line) to remove the unreachable branch and return the validated text
property unconditionally.
packages/mcp/src/server/StreamableLikeC4MCPServer.ts (1)

147-166: Minor: context is cleared before the HTTP server actually closes.

setMcpServerCtx(undefined) runs synchronously at Line 155, but server.close(...) completes later inside the Promise. In-flight requests on /mcp that call useMcpServer() between the unset and the actual close will throw (unctx .use() throws on unset). Consider deferring the unset until after server.close resolves, or gating it on an isStopping flag. Since stop() is typically called during process shutdown, impact is small — flagging as a chill-level refinement.

♻️ Suggested reorder
-    logger.info('Stopping MCP server')
-    this.server = undefined
-    setMcpServerCtx(undefined)
-    return new Promise((resolve) => {
-      server.close((err) => {
-        if (err) {
-          logger.error('Failed to stop MCP server', { err })
-        } else {
-          logger.info('MCP server stopped')
-        }
-        resolve()
-      })
-    })
+    logger.info('Stopping MCP server')
+    this.server = undefined
+    return new Promise((resolve) => {
+      server.close((err) => {
+        setMcpServerCtx(undefined)
+        if (err) {
+          logger.error('Failed to stop MCP server', { err })
+        } else {
+          logger.info('MCP server stopped')
+        }
+        resolve()
+      })
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mcp/src/server/StreamableLikeC4MCPServer.ts` around lines 147 - 166,
The stop() implementation clears the MCP context (setMcpServerCtx(undefined))
before the HTTP server actually finishes closing, allowing in-flight handlers
that call useMcpServer() to throw; change stop() to either set an isStopping
flag (e.g., this.isStopping = true) before calling server.close and only call
setMcpServerCtx(undefined) inside the server.close callback (or after the close
Promise resolves), and ensure useMcpServer() checks the isStopping flag or the
context existence to avoid throwing for in-flight requests during shutdown;
update references to stop(), setMcpServerCtx, server.close, and useMcpServer
accordingly.
🤖 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/layouts/package.json`:
- Line 91: The project added "ts-graphviz" to packages/layouts/package.json but
you must register it in the monorepo catalog (pnpm-workspace.yaml) and verify v3
compatibility: add ts-graphviz as a workspace dependency entry in
pnpm-workspace.yaml, then update the five DOT printer files that import v3 types
(NodeModel, RootGraphModel, SubgraphModel) to confirm they use the v3 ESM import
form and work with Node.js 20+ (adjust imports/tsconfig/moduleResolution and
build settings if needed); ensure no other packages declare a conflicting
version and run the build/tests to validate the API surface and ESM runtime
compatibility.

In `@packages/mcp/src/ctx.ts`:
- Around line 6-8: Add JSDoc to the exported function useMcpServer to document
its purpose, return type, and error behavior; explicitly describe that it
returns the current MCP server context (from mcpServerCtx.use()) and add an
`@throws` tag noting that mcpServerCtx.use() will throw if called outside the MCP
server lifecycle (context not set). Ensure the JSDoc sits immediately above the
useMcpServer declaration and references mcpServerCtx.use() in the description.

In `@packages/mcp/src/server/createMCPServer.ts`:
- Around line 158-166: The projectId completion comparator lowercases only the
input (lowerValue) but compares it to raw p.id, causing misses for IDs with
uppercase letters; update the predicate in the projectId completer (the
complete.projectId handler using services.projects() and p.id) to perform a
case-insensitive comparison by lowercasing p.id as well (e.g., compare
lowerValue against p.id.toLowerCase()) or remove lowercasing entirely so both
sides match consistently.

In `@packages/mcp/src/tools/apply-semantic-layout.ts`:
- Around line 74-99: The current flow always returns "Semantic layout applied"
even when languageServices.views.layouter.aiLayout(...) returns a falsy result;
update the logic so that you only log success, call
languageServices.editor.applyChange({op: 'save-view-snapshot', ...}) and return
the success content when result is truthy, and add an else branch that sends a
failure log via mcpServer.sendLoggingMessage(...) and returns a content message
indicating the layout was not applied (include any available reason from hints);
specifically modify the block around the result variable handling in
apply-semantic-layout.ts (references: aiLayout, result, applyChange,
mcpServer.sendLoggingMessage) to ensure the return text reflects the actual
outcome.
- Around line 25-27: Wrap the project and view lookup sequence in a try-catch:
call languageServices.projectsManager.ensureProjectId(args.projectId), await
languageServices.computedModel(projectId) and then model.view(args.viewId)
inside the try block, and in the catch block convert any thrown error into a
proper MCP error response (per the tool contract) instead of letting the
exception bubble. Ensure you catch errors from ensureProjectId and model.view
(which uses nonNullable()) and return the structured MCP error object from the
function.

In `@packages/vscode/package.json`:
- Around line 310-312: Add a runtime guard before calling the proposed chat API
to avoid runtime errors on stable VS Code: locate where
useLikeC4ChatParticipant.ts calls vscode.chat.createChatParticipant (the call
site inside the useLikeC4ChatParticipant hook) and precede it with a check such
as verifying vscode.chat && vscode.chat.createChatParticipant; if absent, return
early or log a warning so the code soft-fails instead of throwing; also ensure
callers (and any fallback in extension.ts) handle the early-return case
gracefully.

In `@packages/vscode/src/node/mcp-server.ts`:
- Line 12: The logger configuration contains a duplicated option "colors:
false"; remove the redundant duplicate so the logger config object only
specifies "colors: false" once (delete the repeated "colors: false" entry in the
logger configuration to eliminate the lint/static-analysis noise).

In `@packages/vscode/tsdown.config.ts`:
- Around line 109-113: The copySkills function calls cp on skillDir without
checking existence; add the same existence check used by copyPreview: resolve
skillDir, use fs.stat or fs.existsSync (as in copyPreview) to verify the
directory exists and is a directory, and if not, log a clear actionable message
and throw or return early before calling cp; update the copySkills body
(referencing copySkills, skillDir, and cp) to mirror copyPreview's guard so
ENOENT is replaced with a helpful error.

---

Nitpick comments:
In `@packages/mcp/src/server/createMCPServer.ts`:
- Line 146: The code assigns the result of mcp.registerResource(...) to a local
binding named resource but never uses it; remove the dead binding by calling
mcp.registerResource(...) directly (i.e., replace "const resource =
mcp.registerResource(...)" with just "mcp.registerResource(...)" or otherwise
discard the return) so the useless assignment is eliminated and SonarCloud's
warning is resolved.
- Line 120: Replace the unsafe cast to ProjectId when calling
services.builder.parseModel: instead of passing ctx?.arguments?.['projectId'] as
ProjectId, validate and convert it using
services.projectsManager.ensureProjectId(...) (the same helper used elsewhere in
this PR) and pass the returned branded ProjectId into
services.builder.parseModel; update the call at services.builder.parseModel(...)
to use the ensured ProjectId to remove the as ProjectId cast.
- Around line 48-75: The instructions string passed to the model is missing the
new tool entry for apply-semantic-layout; add a one-line entry to the
tools/instructions block (the multi-line string in createMCPServer.ts that lists
"Available tools:") describing "apply-semantic-layout — Apply semantic
auto-layout to a view. Input: { viewId, project?, options? } (experimental)" so
callers can discover this capability and see its experimental status; locate the
tools list in the createMCPServer.ts instructions string and append the
apply-semantic-layout one-liner near the other tool entries.
- Around line 109-112: The inline completable for projectId uses a nullish
reassignment; change the lambda to use a default parameter and remove the
redundant const assignment: update the parameter signature of the completable
callback (currently _value) to default to '' (e.g., _value = '') and delete the
inner const value = _value ?? '' so the body can directly use the parameter when
filtering via services.projects(). Also apply the same change to the other
similar lambda that creates const value to keep style consistent with
projectIdSchema and the repo's idiomatic pattern.

In `@packages/mcp/src/server/StreamableLikeC4MCPServer.ts`:
- Around line 147-166: The stop() implementation clears the MCP context
(setMcpServerCtx(undefined)) before the HTTP server actually finishes closing,
allowing in-flight handlers that call useMcpServer() to throw; change stop() to
either set an isStopping flag (e.g., this.isStopping = true) before calling
server.close and only call setMcpServerCtx(undefined) inside the server.close
callback (or after the close Promise resolves), and ensure useMcpServer() checks
the isStopping flag or the context existence to avoid throwing for in-flight
requests during shutdown; update references to stop(), setMcpServerCtx,
server.close, and useMcpServer accordingly.

In `@packages/mcp/src/tools/apply-semantic-layout.ts`:
- Around line 17-20: Remove the dead commented-out completable block around the
projectId field in apply-semantic-layout.ts: either delete the three commented
lines starting with "projectId: completable(projectIdSchema, ..." or, if you
intend to enable the completable behavior, uncomment and wire it up properly by
using completable(projectIdSchema, ...) and ensure it uses
languageServices.projects().filter(...).map(prop('id')); specifically address
the symbols projectId, completable, projectIdSchema, and
languageServices.projects() so the file contains no leftover commented code.
- Around line 50-58: The ternary returning response.content.type === 'text' ?
response.content.text : JSON.stringify(response.content) is dead because the
response is validated by z.object({ content: z.object({ type: z.literal('text'),
text: z.string() }) }), so simplify the function to directly return
response.content.text; update the code around the response handling in
apply-semantic-layout (the validation and the return line) to remove the
unreachable branch and return the validated text property unconditionally.

In `@packages/vscode/src/panel/activateMessenger.ts`:
- Around line 138-148: The code uses a redundant null-coalescing on
result.location (result.location ?? null); remove the "?? null" so you simply
read result.location (which is already typed as vscode.Location | null at the
RPC boundary) and keep the existing null-guard and subsequent calls to
showEditorNextToPreview and vscode.workspace.save(location.uri) unchanged;
update the reference in activateMessenger where result.location is used to rely
on its declared type rather than coalescing to null.

In `@packages/vscode/tsdown.config.ts`:
- Line 27: The build target objects currently mix placement of the shared spread
(`...shared`) inconsistently (node-CJS spreads it last while node-ESM and both
browser targets spread it first), which can change merge semantics; update each
target object (node-CJS, node-ESM, and both browser target objects) to place
`...shared` consistently at the start of the object literal so per-target fields
defined after the spread always override shared values, and ensure no other
target reverses this ordering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b65b7e9f-538c-4a11-9942-ab011cad950e

📥 Commits

Reviewing files that changed from the base of the PR and between a6aa461 and 76e9e69.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • package.json
  • packages/language-server/src/common-exports.ts
  • packages/layouts/package.json
  • packages/likec4/src/cli/build/index.ts
  • packages/mcp/README.md
  • packages/mcp/bin/likec4-mcp.mjs
  • packages/mcp/package.json
  • packages/mcp/src/ctx.ts
  • packages/mcp/src/server/StdioLikeC4MCPServer.ts
  • packages/mcp/src/server/StreamableLikeC4MCPServer.ts
  • packages/mcp/src/server/createMCPServer.ts
  • packages/mcp/src/tools/apply-semantic-layout.ts
  • packages/vscode-preview/protocol.ts
  • packages/vscode-preview/src/main.tsx
  • packages/vscode-preview/src/vscode.ts
  • packages/vscode/package.json
  • packages/vscode/src/node/mcp-server.ts
  • packages/vscode/src/panel/activateMessenger.ts
  • packages/vscode/src/useMessenger.ts
  • packages/vscode/tsconfig.json
  • packages/vscode/tsdown.config.ts
  • pnpm-workspace.yaml
💤 Files with no reviewable changes (1)
  • packages/likec4/src/cli/build/index.ts
✅ Files skipped from review due to trivial changes (5)
  • packages/mcp/bin/likec4-mcp.mjs
  • package.json
  • packages/vscode/tsconfig.json
  • packages/vscode-preview/protocol.ts
  • packages/language-server/src/common-exports.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/vscode-preview/src/main.tsx
  • packages/vscode-preview/src/vscode.ts
  • pnpm-workspace.yaml
  • packages/vscode/src/useMessenger.ts

Comment thread packages/layouts/package.json
Comment thread packages/mcp/src/ctx.ts
Comment thread packages/mcp/src/server/createMCPServer.ts Outdated
Comment thread packages/mcp/src/tools/apply-semantic-layout.ts Outdated
Comment thread packages/mcp/src/tools/apply-semantic-layout.ts
Comment thread packages/vscode/package.json
Comment thread packages/vscode/src/node/mcp-server.ts Outdated
Comment thread packages/vscode/tsdown.config.ts
davydkov and others added 21 commits May 5, 2026 23:43
- Implemented `useLikeC4ChatParticipant` service to handle chat requests for layout suggestions.
- Integrated AI layout enhancement using `enhanceLayoutWithAI`.
- Added error handling and user feedback for various stages of the layout enhancement process.
- Created a new TypeScript definition file for proposed chat participant additions, expanding the chat API with new interfaces and classes.
…ma from dependencies and add them to devDependencies in layouts package
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The data/skills/ directory is generated at build time and already gitignored.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use JSON.stringify in generate.ts to prevent template literal injection
- Add try/catch in orchestrator to match "never throws" contract
- Make llm-output resilient to hallucinated IDs (filter instead of throw)
- Fix completion state broadcast: only send 'completed' after successful save
- Fix cancelStream.dispose() leak in chat participant
- Add 'failed' state to AI layout broadcast protocol

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread packages/vscode-preview/src/state.ts Fixed

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/language-server/src/lsp/DocumentSymbolProvider.ts (1)

164-173: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

parentFqn is dropped, so propagation is currently ineffective.

Line 166 introduces parentFqn, but Lines 170 and 173 ignore it. That makes nested calls unable to carry parent context forward.

Proposed minimal wiring change
-  protected getElementsSymbol(
-    el: ast.Element | ast.Relation | ast.ExtendElement,
-    parentFqn?: string,
-  ): DocumentSymbol[] {
+  protected getElementsSymbol(
+    el: ast.Element | ast.Relation | ast.ExtendElement,
+    parentFqn?: string,
+  ): DocumentSymbol[] {
     try {
       if (ast.isExtendElement(el)) {
-        return this.getExtendElementSymbol(el)
+        return this.getExtendElementSymbol(el, parentFqn)
       }
       if (ast.isElement(el)) {
-        return this.getElementSymbol(el)
+        return this.getElementSymbol(el, parentFqn)
       }
     } catch (e) {
       logWarnError(e)
     }
     return []
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/language-server/src/lsp/DocumentSymbolProvider.ts` around lines 164
- 173, getElementsSymbol currently accepts parentFqn but doesn't forward it, so
nested symbols lose parent context; update the call sites in getElementsSymbol
to pass parentFqn into getExtendElementSymbol and getElementSymbol (and update
those functions' signatures to accept optional parentFqn), and then inside
getExtendElementSymbol and getElementSymbol propagate that parentFqn into any
recursive calls to getElementsSymbol or when building child symbol FQNs so the
parent context is preserved.
packages/language-server/src/filesystem/LikeC4ManualLayouts.ts (2)

142-160: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hash leaks unparseable-file state silently.

When JSON5.parse (or readFile) throws on a particular file, the catch on line 142 logs a warning and continues, but hash was not yet updated for that file (the stringHash call on line 136 runs before the parse-throwing path completes — actually it runs before the parse since line 136 precedes parse... wait, let me recheck).

Actually JSON5.parse is on line 135 and hash = stringHash(...) is on line 136 — so a parse failure on line 135 skips the hash update entirely, and the same broken file with different content would produce the same snapshot hash. The result is that fixing/breaking an already-corrupt snapshot will not invalidate the cache.

Consider hashing the raw content and file.uri before attempting the parse, so corrupt files still contribute to the hash:

🛡️ Proposed fix
       for (const file of files) {
         try {
           const content = await fs.readFile(file.uri)
+          hash = stringHash(hash + file.uri.toString() + content)
           const parsed = JSON5.parse<LayoutedView>(content)
-          hash = stringHash(hash + file.uri.toString() + content)
           const resolved = this.resolveIconPathsAfterRead(parsed, project.folderUri)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/language-server/src/filesystem/LikeC4ManualLayouts.ts` around lines
142 - 160, The snapshot hash currently skips updates when JSON5.parse fails, so
move the stringHash computation to use the raw file content (and
file.uri.fsPath) before parsing: for each file in the loop compute/update hash =
stringHash(hash, `${file.uri.fsPath}:${content}`) immediately after reading
content (before JSON5.parse), then attempt JSON5.parse and push to manualLayouts
as before; keep the existing catch that logs parse/read errors (logger.warn) but
ensure the hash update happens regardless so corrupted files still affect the
returned hash (refer to the variables/functions stringHash, JSON5.parse, hash,
manualLayouts, and logger.warn in LikeC4ManualLayouts.ts).

126-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hash is order-sensitive — sort files for a stable snapshot hash.

The incremental hash chains file.uri.toString() + content for each file in the order returned by fs.scanDirectory(...). Directory iteration order is filesystem- and platform-dependent (and not guaranteed by scanDirectory's contract), so the same set of manual layouts can produce different hashes across machines or restarts, defeating cache invalidation comparisons that rely on ManualLayoutsSnapshot.hash.

Sort files by URI before iterating so the hash is purely a function of the content set.

🛡️ Proposed fix
       const files = await fs.scanDirectory(outDir, isManualLayoutFile)
       if (files.length === 0) {
         return null
       }
-      for (const file of files) {
+      const sorted = [...files].sort((a, b) => a.uri.toString().localeCompare(b.uri.toString()))
+      for (const file of sorted) {
         try {
           const content = await fs.readFile(file.uri)
           const parsed = JSON5.parse<LayoutedView>(content)
           hash = stringHash(hash + file.uri.toString() + content)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/language-server/src/filesystem/LikeC4ManualLayouts.ts` around lines
126 - 145, The hash is currently order-sensitive because the loop uses the
unsorted array returned by fs.scanDirectory; before iterating and computing the
incremental hash with stringHash(file.uri.toString() + content), sort the files
array by file.uri.toString() (or fsPath) to guarantee a stable order, then
iterate the sorted array and continue using resolveIconPathsAfterRead,
manualLayouts push, and the existing stringHash logic so
ManualLayoutsSnapshot.hash becomes deterministic across runs.
packages/layouts/src/graphviz/DotPrinter.ts (1)

234-284: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Edges referencing filtered-out non-compound nodes will explode.

build() only materializes graphviz nodes for entries returned by selectViewNodes(), but selectViewEdges() is iterated independently. If a subclass override drops a non-compound node while keeping an edge that touches it, addEdgeedgeEndpointgetGraphNode(...) will return null, the invariant(isCompound(element), ...) guard will fail, and rendering throws. The current AiLayoutViewPrinter only reorders so this is latent, but the contract advertised on selectViewNodes/selectViewEdges ("filter") invites future subclasses to hit this.

Either:

  • document that selectViewEdges must be a subset whose endpoints all survive selectViewNodes, and assert it, or
  • in build(), skip edges whose endpoints are not present in the resulting node/subgraph set.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/DotPrinter.ts` around lines 234 - 284, The
build() method can attempt to add edges whose endpoints were filtered out by
selectViewNodes()/selectViewEdges(), causing addEdge → edgeEndpoint →
getGraphNode(...) to return null and blow up; modify build() so that before
calling addEdge (or before storing its result) you check that both endpoints
exist in the materialized graph (use this.getGraphNode(endpointId) or
this.nodes.has(endpointId)/this.subgraphs.has(endpointId) as appropriate) and
skip the edge if either endpoint is missing, ensuring edges are only added when
both endpoints are present; reference functions/classes: build, selectViewNodes,
selectViewEdges, addEdge, edgeEndpoint, getGraphNode, this.nodes,
this.subgraphs.
♻️ Duplicate comments (5)
packages/layouts/src/graphviz/wasm/GraphvizWasmAdapter.ts (1)

61-63: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Retry still bypasses opsCount bookkeeping (unresolved from previous review).

Line 63 calls fn() directly, so a successful retry never increments GraphvizWasmAdapter.opsCount. This means the unload-after-20-ops memory guard will fire 20 operations later than intended for every retry that succeeds. Although the error path already resets opsCount to 0 and unloads WASM, the bookkeeping asymmetry will compound if retries become frequent.

🔧 Proposed fix – extract a shared execution wrapper
   private async attempt<T>(logMessage: string, dot: string, fn: () => Promise<T>): Promise<T> {
     return await limit(async () => {
       const logger = rootLogger.getChild(['layouter', 'wasm', logMessage, '_', randomString(4).toLowerCase()])
+      const runOnce = async () => {
+        logger.trace`execute`
+        const result = await fn()
+        if (++GraphvizWasmAdapter.opsCount >= 20) {
+          GraphvizWasmAdapter.opsCount = 0
+          logger.trace`reached 20 operations, unloading wasm`
+          Graphviz.unload()
+        }
+        return result
+      }
       try {
-        logger.trace`execute`
-        const result = await fn()
-        if (++GraphvizWasmAdapter.opsCount >= 20) {
-          GraphvizWasmAdapter.opsCount = 0
-          // very hacky, but helps
-          logger.trace`reached 20 operations, unloading wasm`
-          Graphviz.unload()
-        }
-        return result
+        return await runOnce()
       } catch (error) {
         logger.trace('FAILED DOT\n' + dot)
         const errorStr = loggable(error)

         // don't retry on syntax errors
         if (errorStr.includes('syntax error')) {
           throw error
         }
         logger.warn(errorStr)

         GraphvizWasmAdapter.opsCount = 0
         Graphviz.unload()
       }
       logger.warn('Retrying...')
       await delay(30, 300)
-      return await fn()
+      return await runOnce()
     })
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/wasm/GraphvizWasmAdapter.ts` around lines 61 -
63, The retry path currently calls fn() directly so successful retries don't
update GraphvizWasmAdapter.opsCount; extract a shared execution wrapper (e.g.,
executeWithOpBookkeeping or wrapCall) that always increments this.opsCount and
performs the same success/error handling, then replace direct calls to fn()
(including the retry branch) with that wrapper so every successful retry
increments GraphvizWasmAdapter.opsCount and the unload-after-20-ops guard
remains correct.
packages/layouts/src/graphviz/ai/llm-input.ts (1)

108-115: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't reject the entire view on compound endpoints.

This unconditional throw still aborts the whole AI path as soon as any single edge touches a non-leaf node. As previously noted, ElementViewPrinter.addEdge() already supports compound endpoints, so container-to-node and container-to-container views lose AI layout entirely here. Prefer per-edge handling: serialize compound endpoints when supported, or skip individual unsupported edges with a logger.warn instead of failing the whole view.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/ai/llm-input.ts` around lines 108 - 115, The
current code finds a single nonLeafEdge and throws, which aborts the entire AI
path; instead, remove the unconditional throw and handle edges per-edge: iterate
view.edges and for each edge check leafs.has(edge.source) and
leafs.has(edge.target); if both are leaves, process/serialize the edge as before
(via ElementViewPrinter.addEdge()), if one or both endpoints are compound but
ElementViewPrinter.addEdge() supports compound endpoints, serialize them
accordingly, otherwise skip that single edge and call logger.warn with view.id
and the edge {source,target} explaining it's unsupported; ensure you no longer
use the single nonLeafEdge throw and that logging is per-edge rather than
aborting the whole view.
packages/layouts/src/graphviz/GraphvizLayoter.ts (1)

158-176: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dynamic-view post-processing still missing in aiLayout.

layout() attaches sequenceLayout for dynamic views via calcSequenceLayout, but aiLayout() returns the parsed diagram directly. AI-enhancing a dynamic view will therefore yield a LayoutResult with a different shape than the standard path, breaking downstream consumers that rely on LayoutedDynamicView.sequenceLayout. Mirror the dynamic-view branch from layout() here.

Proposed fix
       let diagram = parseGraphvizJson(json, view)
+      if (isDynamicView(diagram)) {
+        Object.assign(
+          diagram,
+          {
+            sequenceLayout: calcSequenceLayout(diagram),
+          } satisfies Partial<LayoutedDynamicView<A>>,
+        )
+      }
       dot = normalizeDot(dot)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/GraphvizLayoter.ts` around lines 158 - 176,
aiLayout currently returns dot+diagram without applying the dynamic-view
post-processing that layout() does, so AI-laid-out dynamic views lack the
sequenceLayout field; update aiLayout (inside the AiLayoutLayouter.aiLayout
method) to detect dynamic views the same way layout() does, call
calcSequenceLayout(view, diagram) (or the same helper used by layout()), attach
the returned sequenceLayout onto the LayoutResult (i.e., return a
LayoutedDynamicView with sequenceLayout for dynamic views), and keep the
existing error handling and logger behavior; reference aiLayout, layout,
calcSequenceLayout, sequenceLayout, LayoutResult, and LayoutedDynamicView when
implementing the branch.
packages/layouts/src/graphviz/AiLayoutPrinter.ts (2)

97-98: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unused destructured tuple elements sourceNode/targetNode.

sourceNode and targetNode are bound but never read. Static analysis is consistently flagging both. Drop the leading slot to remove the noise without altering tuple positions.

♻️ Diff
-    const [sourceNode, source, ltail] = this.edgeEndpoint(sourceFqn, nodes => last(nodes))
-    const [targetNode, target, lhead] = this.edgeEndpoint(targetFqn, first)
+    const [, source, ltail] = this.edgeEndpoint(sourceFqn, nodes => last(nodes))
+    const [, target, lhead] = this.edgeEndpoint(targetFqn, first)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts` around lines 97 - 98, The
destructured variables sourceNode and targetNode are unused; update the two
destructuring assignments that call edgeEndpoint to drop the leading slot so
tuple positions stay correct—change const [sourceNode, source, ltail] =
this.edgeEndpoint(sourceFqn, nodes => last(nodes)) to const [, source, ltail] =
this.edgeEndpoint(sourceFqn, nodes => last(nodes)) and change const [targetNode,
target, lhead] = this.edgeEndpoint(targetFqn, first) to const [, target, lhead]
= this.edgeEndpoint(targetFqn, first).

9-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused imports.

nonNullable from @likec4/core (line 9) and isEmptyish from remeda (line 11) are not referenced in this file. SonarCloud and previous review iterations already flagged these.

♻️ Diff
 import {
   type AnyAux,
   type ComputedEdge,
   type ComputedNode,
   type ComputedView,
   type EdgeId,
   type HexColor,
   type LikeC4Styles,
-  nonNullable,
 } from '@likec4/core'
-import { entries, first, isEmptyish, isNonNullish, isNumber, last } from 'remeda'
+import { entries, first, isNonNullish, isNumber, last } from 'remeda'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts` around lines 9 - 11, Remove
the unused named imports nonNullable (from '@likec4/core') and isEmptyish (from
'remeda') in AiLayoutPrinter.ts: locate the import statements that currently
include nonNullable and isEmptyish, delete those symbols from their respective
import lists, adjust remaining commas/spacing so the imports remain valid, and
then run the TypeScript build/linter to ensure no other references remain.
🧹 Nitpick comments (12)
packages/language-server/src/Rpc.ts (1)

166-169: 💤 Low value

Optional: include hints presence in the debug log.

The current log records viewId, projectId, and layoutType but not whether AI hints were supplied. Adding a small flag (e.g., hints: hints ? 'present' : 'none') would help diagnose issues unique to the AI-enhanced path without leaking the hint payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/language-server/src/Rpc.ts` around lines 166 - 169, The debug log
for the layoutView request currently reports viewId, projectId and layoutType;
update the logger.debug call (the template literal using logger.debug`received
request ${'layoutView'} ...`) to also include a small hints-presence flag such
as "hints: " + (hints ? "present" : "none") so the log shows whether AI hints
were supplied without printing the hint payload; keep the rest of the message
unchanged and reference the existing variables viewId, projectId, layoutType and
hints.
packages/layouts/src/graphviz/ai/llm-output.ts (3)

174-189: 💤 Low value

weight: 1 and minlen: 1 are dropped silently.

When the LLM specifies weight: 1 or minlen: 1 explicitly on an invisibleEdge, the !== 1 checks strip them, treating them as defaults. That matches the destructuring defaults and is probably intentional, but the contract is implicit. Consider documenting this normalization (e.g., a brief comment) so downstream consumers understand undefined means "use default" rather than "LLM didn't provide".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/ai/llm-output.ts` around lines 174 - 189, The
parsed.invisibleEdges mapping drops explicit weight: 1 and minlen: 1 by
converting them to undefined (seen in the invisibleEdges const using
parsed.invisibleEdges, nodeId, and exact to build AIEnforcementEdge), which is
implicit; add a concise comment immediately above the invisibleEdges computation
explaining that weight/minlen are normalized to undefined when equal to 1 to
indicate "use default" (not "value absent"), so downstream consumers understand
that undefined means "apply graphviz default" rather than "property not
provided."

143-157: 💤 Low value

Use mapToNonEmpty consistently for excludeFromRanking and reverseRank.

mapToNonEmpty (defined at lines 138-141) already encapsulates "map → filter undefined → return undefined when empty". Both excludeFromRanking and reverseRank re-implement essentially the same flow inline (with an added unique()). Extracting a small helper or extending mapToNonEmpty to optionally dedupe would remove the duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/ai/llm-output.ts` around lines 143 - 157, The
two blocks computing excludeFromRanking and reverseRank duplicate map → filter
undefined → empty-to-undefined logic already implemented by mapToNonEmpty;
refactor them to call mapToNonEmpty(parsed.excludeFromRanking, edgeId) and
mapToNonEmpty(parsed.reverseRank, edgeId) and handle deduplication by either
extending mapToNonEmpty to accept an optional { unique: true } flag or by
wrapping its result with unique() only when non-undefined; update references to
excludeFromRanking and reverseRank accordingly and preserve the existing
unique() behavior.

191-200: 💤 Low value

Replace dynamic RegExp with literal replaceAll for reasoning normalization.

new RegExp(\[${id}\], 'g') constructs regexes from variable input on every iteration. Since you're matching exact [id] substrings (where id is n1/c1/e1-style), a plain string replaceAll is simpler, faster, and sidesteps ReDoS concerns flagged by static analysis.

♻️ Proposed refactor
     entries(params.mapping.nodes).forEach(([id, nd]) => {
-      reasoning = reasoning.replaceAll(new RegExp(`\\[${id}\\]`, 'g'), '`' + nd.id + '`')
+      reasoning = reasoning.replaceAll(`[${id}]`, '`' + nd.id + '`')
     })
     entries(params.mapping.edges).forEach(([id, e]) => {
-      reasoning = reasoning.replaceAll(new RegExp(`\\[${id}\\]`, 'g'), '`' + e.source + ' -> ' + e.target + '`')
+      reasoning = reasoning.replaceAll(`[${id}]`, '`' + e.source + ' -> ' + e.target + '`')
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/ai/llm-output.ts` around lines 191 - 200, The
code currently builds RegExp objects inside the loops when replacing occurrences
like `[${id}]` in the reasoning string; change those to use literal string
replaceAll calls instead: in the loop over entries(params.mapping.nodes) replace
the RegExp-based replaceAll with reasoning.replaceAll(`[${id}]`, '`' + nd.id +
'`'), and in the loop over entries(params.mapping.edges) replace the
RegExp-based replaceAll with reasoning.replaceAll(`[${id}]`, '`' + e.source + '
-> ' + e.target + '`'), preserving the initial reasoning.replaceAll('][', '] [')
normalization.
packages/layouts/src/graphviz/ai/llm-input.ts (2)

96-97: ⚡ Quick win

Avoid O(N²) lookups in findNodeById.

findNodeById runs view.nodes.find(...) once per node and once per edge endpoint, giving O(N²) behavior on every view passed to the LLM. Pre-build an index Map once and reuse it.

♻️ Proposed refactor
-  const findNodeById = (id: NodeId) => view.nodes.find(n => n.id === id)!
-  const nodeId = (id: NodeId) => nodeIds.get(findNodeById(id))
+  const nodeById = new Map(view.nodes.map(n => [n.id, n] as const))
+  const findNodeById = (id: NodeId) => {
+    const node = nodeById.get(id)
+    if (!node) throw new Error(`Node ${id} not found in view ${view.id}`)
+    return node
+  }
+  const nodeId = (id: NodeId) => nodeIds.get(findNodeById(id))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/ai/llm-input.ts` around lines 96 - 97, The
current findNodeById (used by nodeId which calls nodeIds.get(findNodeById(id)))
performs view.nodes.find(...) repeatedly causing O(N²) lookups; instead, build a
Map index of node id -> node (e.g., nodeById) once for the given view before any
nodeId calls and use that Map in findNodeById/nodeId to look up nodes in O(1),
ensuring you populate nodeById from view.nodes and replace all view.nodes.find
usages in this module with nodeById.get(id).

120-136: 💤 Low value

Stale comment: edges are not actually filtered.

The comment says "Filter edges between leaf nodes and create serialized edges", but view.edges.map(...) only maps — no filter. The non-leaf rejection happens earlier via throw. Either remove "Filter" from the comment or, preferably, replace the throw with a real per-edge filter (see related comment on lines 108-115).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/ai/llm-input.ts` around lines 120 - 136, The
comment is stale because view.edges.map(...) doesn't filter; change to
explicitly filter out non-leaf-edge entries before mapping: replace
view.edges.map(...) with view.edges.filter(e => /* check both source and target
are leaf nodes using the same predicate used where a throw currently occurs
*/).map(...) and remove the earlier throw-based rejection (or convert it to a
non-fatal check) so only valid leaf-to-leaf edges are serialized; keep the rest
of the mapping logic (nodeId, edgeIds.get, edgeLabel, handling e.dir and
e.parent) intact.
packages/layouts/src/graphviz/ai/types.ts (1)

63-97: 💤 Low value

Optional: Consider consistency between required/optional collections.

ranks, edgeWeight, and edgeMinlen are required (potentially empty), while other collection-style fields use optional NonEmptyReadonlyArray. Aligning all collection fields to either "optional + NonEmpty" or "required + Readonly" would simplify consumer code (e.g., AiLayoutPrinter, parseOutput). This is purely a shape-consistency suggestion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/ai/types.ts` around lines 63 - 97, The
AILayoutHints type mixes required-empty collections (ranks: ReadonlyArray,
edgeWeight: Record, edgeMinlen: Record) with optional NonEmptyReadonlyArray
fields (reverseRank, excludeFromRanking, edgeOrder, nodeOrder, invisibleEdges),
which complicates consumers like AiLayoutPrinter and parseOutput; pick a
consistent shape and update the type and callers accordingly — either make
ranks/edgeWeight/edgeMinlen optional and use
NonEmptyReadonlyArray/NonEmptyRecord variants (or nullable) to indicate
presence-only semantics, or convert the optional NonEmptyReadonlyArray fields to
required ReadonlyArray/Record (possibly empty) so all collection fields share
the same required-or-optional convention; update AILayoutHints and then adjust
parseOutput and AiLayoutPrinter to handle the chosen convention consistently.
packages/layouts/src/graphviz/GraphvizLayoter.ts (2)

97-105: 💤 Low value

Doc string is now inconsistent with behavior.

The JSDoc states the method does not perform unflattening or any other post-processing on the DOT output, but the implementation now applies normalizeDot(...) to the result, which is a post-processing step. Update the doc to reflect that DOT-level normalization is applied (only unflatten is skipped), or remove the misleading sentence.

📝 Suggested doc update
   /**
    * Generates DOT source for the given view and styles.
    * If `hints` are provided, they will be used to influence the layout (e.g. by specifying node/edge order).
-   * This method does not perform unflattening or any other post-processing on the DOT output, so it may be used for debugging or to generate DOT for external processing.
+   * The DOT is normalized (graph-level margin attribute stripped) but not run through `unflatten`,
+   * so the result is suitable for debugging or external processing.
    */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/GraphvizLayoter.ts` around lines 97 - 105, The
JSDoc for printToDot is misleading because the method does apply DOT
post-processing via normalizeDot; update the comment for printToDot to state
that the output is normalized (normalizeDot is applied) but that unflattening
and other higher-level post-processing are not performed; reference the
printToDot method and mention normalizeDot, AiLayoutViewPrinter and getPrinter
so reviewers can locate the relevant code to change.

64-69: 💤 Low value

Fragile string-based filter for graph margin.

normalizeDot uses substring matching (l.includes('margin') && l.includes('${GraphClusterSpace}')) to strip a line set in DotPrinter.createGraph ([_.margin]: GraphClusterSpace). This is brittle:

  • If GraphClusterSpace (currently 50.1) is ever used as a numeric value elsewhere in DOT (e.g., a node margin or padding that happens to render as 50.1), the filter will incorrectly drop those lines.
  • The trailing comment // see DotPrinter.ts#L175 no longer points at the relevant line in the refactored DotPrinter.ts (see line 326).

Consider emitting a sentinel/marker comment in the printer or removing the attribute on the graph after rendering instead of substring-grepping the output. At minimum, anchor the match (e.g., regex matching ^\s*margin\s*=\s*"?50.1"? on the graph attribute line only) and update the line reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/GraphvizLayoter.ts` around lines 64 - 69,
normalizeDot currently removes lines by substring matching which is brittle;
update it to only remove the specific graph attribute line (not any occurrence
of the number) by either (preferred) having DotPrinter.createGraph emit a
sentinel comment next to the attribute (e.g., append a node like `/*
__GRAPH_CLUSTER_MARGIN__ */` when setting [_.margin]: GraphClusterSpace) and
then have normalizeDot filter only the line containing that sentinel, or
(alternative) change normalizeDot to use an anchored regex that only matches a
graph attribute line like /^\s*margin\s*=\s*"?${GraphClusterSpace}"?\s*;?$/ so
unrelated occurrences of the numeric value are not removed; also update the
trailing comment reference to point to the new location in
DotPrinter.createGraph. Ensure references: normalizeDot, DotPrinter.createGraph,
and GraphClusterSpace are used to find the code to change.
packages/layouts/src/graphviz/AiLayoutPrinter.ts (2)

74-75: 💤 Low value

Dead code: reduceDefaultRankSeparation is defined but never invoked.

The only call site in postBuild is commented out (line 74), so reduceDefaultRankSeparation() (lines 185–211) is unreachable. Either wire it into the build pipeline (and drop the comment) or remove it until it's needed; leaving disabled logic in place tends to drift from the rest of the printer.

Also applies to: 185-211

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts` around lines 74 - 75, The
function reduceDefaultRankSeparation is dead code because its only call in
postBuild is commented out; either re-enable the call by uncommenting and wiring
reduceDefaultRankSeparation() into the postBuild pipeline (ensuring any required
inputs/state in postBuild are available) or remove the
reduceDefaultRankSeparation method entirely. Locate postBuild and the
reduceDefaultRankSeparation definition in AiLayoutPrinter and either restore the
invocation (remove the comment and run tests/formatting) or delete the unused
method to keep the class clean.

37-67: 💤 Low value

O(N·M) reordering can be simplified with a Map.

Both selectViewNodes and selectViewEdges repeatedly findIndex + splice on the working array, which is O(N·M) and mutates a copy on each iteration. For large diagrams this is wasteful; an index map keeps it linear and is easier to read.

♻️ Suggested refactor
 protected override selectViewNodes(): Iterable<ComputedNode> {
-  if (!this.aiHints.nodeOrder) {
-    return this.view.nodes
-  }
-  const viewnodes = [...this.view.nodes]
-  const sorted: ComputedNode[] = []
-  for (const nodeId of this.aiHints.nodeOrder) {
-    const index = viewnodes.findIndex(n => n.id === nodeId)
-    if (index !== -1) {
-      sorted.push(...viewnodes.splice(index, 1))
-    }
-  }
-  sorted.push(...viewnodes)
-  return sorted
+  if (!this.aiHints.nodeOrder) {
+    return this.view.nodes
+  }
+  const byId = new Map(this.view.nodes.map(n => [n.id, n]))
+  const sorted: ComputedNode[] = []
+  for (const nodeId of this.aiHints.nodeOrder) {
+    const node = byId.get(nodeId)
+    if (node) {
+      sorted.push(node)
+      byId.delete(nodeId)
+    }
+  }
+  sorted.push(...byId.values())
+  return sorted
 }

Apply the same shape to selectViewEdges.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts` around lines 37 - 67, The
current selectViewNodes and selectViewEdges implementations repeatedly call
findIndex and splice on a copy of this.view.nodes/edges, causing O(N·M)
complexity; replace this with a Map-based approach: build a map from node/edge
id to the original item (using this.view.nodes and this.view.edges), then
iterate aiHints.nodeOrder / aiHints.edgeOrder to push items in that order from
the map (removing matched ids from the map as needed), and finally append any
remaining items from the map to preserve unspecified items; update symbols:
selectViewNodes, selectViewEdges, aiHints.nodeOrder, aiHints.edgeOrder,
this.view.nodes, this.view.edges, ComputedNode and ComputedEdge to implement the
O(N) reordering without mutating arrays via splice.
packages/layouts/src/graphviz/DotPrinter.ts (1)

357-380: 💤 Low value

Prefer string | undefined over string | false for reserveNodeId.

reserveNodeId returns string | false and generateGraphvizId does let id = reserve...; while (id === false) .... Using false as a sentinel mixes a boolean into a string-typed pipeline and is mildly confusing in a TypeScript-first codebase; string | undefined (or returning null) reads more naturally and aligns with the rest of the file (e.g., getGraphNode/getSubgraph returning T | null).

♻️ Suggested shape
-private reserveNodeId(name: string, isCompound = false): string | false {
+private reserveNodeId(name: string, isCompound = false): string | undefined {
   if (isCompound) {
     name = 'cluster_' + name
   } else if (name.toLowerCase().startsWith('cluster')) {
     name = 'nd_' + name
   }
   if (this.ids.has(name)) {
-    return false
+    return undefined
   }
   this.ids.add(name)
   return name
 }

 protected generateGraphvizId(node: NodeOf<V>) {
   const _compound = isCompound(node)
   const name = nameFromFqn(node.id).toLowerCase()
   let id = this.reserveNodeId(name, _compound)
   let i = 1
-  while (id === false) {
+  while (id === undefined) {
     id = this.reserveNodeId(name + '_' + i++, _compound)
   }
   return id
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/layouts/src/graphviz/DotPrinter.ts` around lines 357 - 380, Change
reserveNodeId to return string | undefined instead of string | false: update its
signature and return undefined where it currently returns false, and keep adding
to this.ids and returning the name on success. Update generateGraphvizId to
handle undefined by initializing let id = this.reserveNodeId(name, _compound)
and change the loop to while (id === undefined) { id = this.reserveNodeId(name +
'_' + i++, _compound) } (or use id == null) so the sentinel is undefined/null
rather than false; adjust any type annotations or callers expecting boolean to
match the new signature (reserveNodeId, generateGraphvizId, and references to
this.ids).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/language-server/src/lsp/CompletionProvider.ts`:
- Around line 92-95: The import snippet currently builds a choice placeholder
from project IDs which may include '|' or ',' and will break VS Code snippet
syntax; in CompletionProvider (acceptSnippet call) sanitize the values from
this.services.shared.workspace.ProjectsManager.all by escaping pipe and comma
characters (or replace them with safe equivalents) before joining into the
choice list, or if any ID contains unsafe characters fall back to a plain
tabstop placeholder (e.g., use '${1}' instead of '${1|...|}') so the insertText
string remains a valid snippet.

In `@packages/language-server/src/views/LikeC4Views.ts`:
- Around line 460-468: clearView currently deletes cached layout keys but leaves
the per-view error sentinel `error:${viewId}` in storage, which keeps later
failures suppressed; update clearView (and the other cache-invalidation site
around the same area) to also remove the `error:${viewId}` entry from `#storage`
when invalidating a view so that reportViewError will not see a stale error
marker after manual-layout updates.

In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts`:
- Around line 153-164: The loop over this.aiHints.invisibleEdges drops edges
when getGraphNode(source) or getGraphNode(target) returns falsy (which happens
for compound/cluster nodes); update the invisible-edge creation in
AiLayoutPrinter to resolve endpoints via the same edgeEndpoint(...) logic used
by addEdge (so clusters are mapped to representative leaf nodes and lhead/ltail
are set) and, if you still must skip an edge, emit a trace or warn with the
source/target and reason instead of silently continuing; touch the logic around
getGraphNode, the invisible edgeId creation, and the logger.trace/warn calls so
skipped cases are observable.

In `@packages/layouts/src/graphviz/DotPrinter.ts`:
- Line 4: Remove the unused import ComputedEdge from the import list in
DotPrinter.ts; locate the import statement that includes "ComputedEdge"
(alongside other imports like EdgeOf<V>) and delete only the ComputedEdge token
so typing continues to flow via EdgeOf<V> and no other behavior changes.

In `@packages/mcp/src/tools/apply-semantic-layout.ts`:
- Around line 61-72: The branch that handles missing hints currently logs an
error via mcpServer.sendLoggingMessage but returns a normal tool result; modify
the returned object in applySemanticLayout (the branch where hints is falsy) to
mark the result as an MCP error by adding isError: true to the returned value
alongside the content, e.g. return { isError: true, content: [{ type: 'text',
text: 'Failed to generate layout hints' }] }; keep the existing logging call to
mcpServer.sendLoggingMessage(ctx.sessionId) unchanged.

---

Outside diff comments:
In `@packages/language-server/src/filesystem/LikeC4ManualLayouts.ts`:
- Around line 142-160: The snapshot hash currently skips updates when
JSON5.parse fails, so move the stringHash computation to use the raw file
content (and file.uri.fsPath) before parsing: for each file in the loop
compute/update hash = stringHash(hash, `${file.uri.fsPath}:${content}`)
immediately after reading content (before JSON5.parse), then attempt JSON5.parse
and push to manualLayouts as before; keep the existing catch that logs
parse/read errors (logger.warn) but ensure the hash update happens regardless so
corrupted files still affect the returned hash (refer to the variables/functions
stringHash, JSON5.parse, hash, manualLayouts, and logger.warn in
LikeC4ManualLayouts.ts).
- Around line 126-145: The hash is currently order-sensitive because the loop
uses the unsorted array returned by fs.scanDirectory; before iterating and
computing the incremental hash with stringHash(file.uri.toString() + content),
sort the files array by file.uri.toString() (or fsPath) to guarantee a stable
order, then iterate the sorted array and continue using
resolveIconPathsAfterRead, manualLayouts push, and the existing stringHash logic
so ManualLayoutsSnapshot.hash becomes deterministic across runs.

In `@packages/language-server/src/lsp/DocumentSymbolProvider.ts`:
- Around line 164-173: getElementsSymbol currently accepts parentFqn but doesn't
forward it, so nested symbols lose parent context; update the call sites in
getElementsSymbol to pass parentFqn into getExtendElementSymbol and
getElementSymbol (and update those functions' signatures to accept optional
parentFqn), and then inside getExtendElementSymbol and getElementSymbol
propagate that parentFqn into any recursive calls to getElementsSymbol or when
building child symbol FQNs so the parent context is preserved.

In `@packages/layouts/src/graphviz/DotPrinter.ts`:
- Around line 234-284: The build() method can attempt to add edges whose
endpoints were filtered out by selectViewNodes()/selectViewEdges(), causing
addEdge → edgeEndpoint → getGraphNode(...) to return null and blow up; modify
build() so that before calling addEdge (or before storing its result) you check
that both endpoints exist in the materialized graph (use
this.getGraphNode(endpointId) or
this.nodes.has(endpointId)/this.subgraphs.has(endpointId) as appropriate) and
skip the edge if either endpoint is missing, ensuring edges are only added when
both endpoints are present; reference functions/classes: build, selectViewNodes,
selectViewEdges, addEdge, edgeEndpoint, getGraphNode, this.nodes,
this.subgraphs.

---

Duplicate comments:
In `@packages/layouts/src/graphviz/ai/llm-input.ts`:
- Around line 108-115: The current code finds a single nonLeafEdge and throws,
which aborts the entire AI path; instead, remove the unconditional throw and
handle edges per-edge: iterate view.edges and for each edge check
leafs.has(edge.source) and leafs.has(edge.target); if both are leaves,
process/serialize the edge as before (via ElementViewPrinter.addEdge()), if one
or both endpoints are compound but ElementViewPrinter.addEdge() supports
compound endpoints, serialize them accordingly, otherwise skip that single edge
and call logger.warn with view.id and the edge {source,target} explaining it's
unsupported; ensure you no longer use the single nonLeafEdge throw and that
logging is per-edge rather than aborting the whole view.

In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts`:
- Around line 97-98: The destructured variables sourceNode and targetNode are
unused; update the two destructuring assignments that call edgeEndpoint to drop
the leading slot so tuple positions stay correct—change const [sourceNode,
source, ltail] = this.edgeEndpoint(sourceFqn, nodes => last(nodes)) to const [,
source, ltail] = this.edgeEndpoint(sourceFqn, nodes => last(nodes)) and change
const [targetNode, target, lhead] = this.edgeEndpoint(targetFqn, first) to const
[, target, lhead] = this.edgeEndpoint(targetFqn, first).
- Around line 9-11: Remove the unused named imports nonNullable (from
'@likec4/core') and isEmptyish (from 'remeda') in AiLayoutPrinter.ts: locate the
import statements that currently include nonNullable and isEmptyish, delete
those symbols from their respective import lists, adjust remaining
commas/spacing so the imports remain valid, and then run the TypeScript
build/linter to ensure no other references remain.

In `@packages/layouts/src/graphviz/GraphvizLayoter.ts`:
- Around line 158-176: aiLayout currently returns dot+diagram without applying
the dynamic-view post-processing that layout() does, so AI-laid-out dynamic
views lack the sequenceLayout field; update aiLayout (inside the
AiLayoutLayouter.aiLayout method) to detect dynamic views the same way layout()
does, call calcSequenceLayout(view, diagram) (or the same helper used by
layout()), attach the returned sequenceLayout onto the LayoutResult (i.e.,
return a LayoutedDynamicView with sequenceLayout for dynamic views), and keep
the existing error handling and logger behavior; reference aiLayout, layout,
calcSequenceLayout, sequenceLayout, LayoutResult, and LayoutedDynamicView when
implementing the branch.

In `@packages/layouts/src/graphviz/wasm/GraphvizWasmAdapter.ts`:
- Around line 61-63: The retry path currently calls fn() directly so successful
retries don't update GraphvizWasmAdapter.opsCount; extract a shared execution
wrapper (e.g., executeWithOpBookkeeping or wrapCall) that always increments
this.opsCount and performs the same success/error handling, then replace direct
calls to fn() (including the retry branch) with that wrapper so every successful
retry increments GraphvizWasmAdapter.opsCount and the unload-after-20-ops guard
remains correct.

---

Nitpick comments:
In `@packages/language-server/src/Rpc.ts`:
- Around line 166-169: The debug log for the layoutView request currently
reports viewId, projectId and layoutType; update the logger.debug call (the
template literal using logger.debug`received request ${'layoutView'} ...`) to
also include a small hints-presence flag such as "hints: " + (hints ? "present"
: "none") so the log shows whether AI hints were supplied without printing the
hint payload; keep the rest of the message unchanged and reference the existing
variables viewId, projectId, layoutType and hints.

In `@packages/layouts/src/graphviz/ai/llm-input.ts`:
- Around line 96-97: The current findNodeById (used by nodeId which calls
nodeIds.get(findNodeById(id))) performs view.nodes.find(...) repeatedly causing
O(N²) lookups; instead, build a Map index of node id -> node (e.g., nodeById)
once for the given view before any nodeId calls and use that Map in
findNodeById/nodeId to look up nodes in O(1), ensuring you populate nodeById
from view.nodes and replace all view.nodes.find usages in this module with
nodeById.get(id).
- Around line 120-136: The comment is stale because view.edges.map(...) doesn't
filter; change to explicitly filter out non-leaf-edge entries before mapping:
replace view.edges.map(...) with view.edges.filter(e => /* check both source and
target are leaf nodes using the same predicate used where a throw currently
occurs */).map(...) and remove the earlier throw-based rejection (or convert it
to a non-fatal check) so only valid leaf-to-leaf edges are serialized; keep the
rest of the mapping logic (nodeId, edgeIds.get, edgeLabel, handling e.dir and
e.parent) intact.

In `@packages/layouts/src/graphviz/ai/llm-output.ts`:
- Around line 174-189: The parsed.invisibleEdges mapping drops explicit weight:
1 and minlen: 1 by converting them to undefined (seen in the invisibleEdges
const using parsed.invisibleEdges, nodeId, and exact to build
AIEnforcementEdge), which is implicit; add a concise comment immediately above
the invisibleEdges computation explaining that weight/minlen are normalized to
undefined when equal to 1 to indicate "use default" (not "value absent"), so
downstream consumers understand that undefined means "apply graphviz default"
rather than "property not provided."
- Around line 143-157: The two blocks computing excludeFromRanking and
reverseRank duplicate map → filter undefined → empty-to-undefined logic already
implemented by mapToNonEmpty; refactor them to call
mapToNonEmpty(parsed.excludeFromRanking, edgeId) and
mapToNonEmpty(parsed.reverseRank, edgeId) and handle deduplication by either
extending mapToNonEmpty to accept an optional { unique: true } flag or by
wrapping its result with unique() only when non-undefined; update references to
excludeFromRanking and reverseRank accordingly and preserve the existing
unique() behavior.
- Around line 191-200: The code currently builds RegExp objects inside the loops
when replacing occurrences like `[${id}]` in the reasoning string; change those
to use literal string replaceAll calls instead: in the loop over
entries(params.mapping.nodes) replace the RegExp-based replaceAll with
reasoning.replaceAll(`[${id}]`, '`' + nd.id + '`'), and in the loop over
entries(params.mapping.edges) replace the RegExp-based replaceAll with
reasoning.replaceAll(`[${id}]`, '`' + e.source + ' -> ' + e.target + '`'),
preserving the initial reasoning.replaceAll('][', '] [') normalization.

In `@packages/layouts/src/graphviz/ai/types.ts`:
- Around line 63-97: The AILayoutHints type mixes required-empty collections
(ranks: ReadonlyArray, edgeWeight: Record, edgeMinlen: Record) with optional
NonEmptyReadonlyArray fields (reverseRank, excludeFromRanking, edgeOrder,
nodeOrder, invisibleEdges), which complicates consumers like AiLayoutPrinter and
parseOutput; pick a consistent shape and update the type and callers accordingly
— either make ranks/edgeWeight/edgeMinlen optional and use
NonEmptyReadonlyArray/NonEmptyRecord variants (or nullable) to indicate
presence-only semantics, or convert the optional NonEmptyReadonlyArray fields to
required ReadonlyArray/Record (possibly empty) so all collection fields share
the same required-or-optional convention; update AILayoutHints and then adjust
parseOutput and AiLayoutPrinter to handle the chosen convention consistently.

In `@packages/layouts/src/graphviz/AiLayoutPrinter.ts`:
- Around line 74-75: The function reduceDefaultRankSeparation is dead code
because its only call in postBuild is commented out; either re-enable the call
by uncommenting and wiring reduceDefaultRankSeparation() into the postBuild
pipeline (ensuring any required inputs/state in postBuild are available) or
remove the reduceDefaultRankSeparation method entirely. Locate postBuild and the
reduceDefaultRankSeparation definition in AiLayoutPrinter and either restore the
invocation (remove the comment and run tests/formatting) or delete the unused
method to keep the class clean.
- Around line 37-67: The current selectViewNodes and selectViewEdges
implementations repeatedly call findIndex and splice on a copy of
this.view.nodes/edges, causing O(N·M) complexity; replace this with a Map-based
approach: build a map from node/edge id to the original item (using
this.view.nodes and this.view.edges), then iterate aiHints.nodeOrder /
aiHints.edgeOrder to push items in that order from the map (removing matched ids
from the map as needed), and finally append any remaining items from the map to
preserve unspecified items; update symbols: selectViewNodes, selectViewEdges,
aiHints.nodeOrder, aiHints.edgeOrder, this.view.nodes, this.view.edges,
ComputedNode and ComputedEdge to implement the O(N) reordering without mutating
arrays via splice.

In `@packages/layouts/src/graphviz/DotPrinter.ts`:
- Around line 357-380: Change reserveNodeId to return string | undefined instead
of string | false: update its signature and return undefined where it currently
returns false, and keep adding to this.ids and returning the name on success.
Update generateGraphvizId to handle undefined by initializing let id =
this.reserveNodeId(name, _compound) and change the loop to while (id ===
undefined) { id = this.reserveNodeId(name + '_' + i++, _compound) } (or use id
== null) so the sentinel is undefined/null rather than false; adjust any type
annotations or callers expecting boolean to match the new signature
(reserveNodeId, generateGraphvizId, and references to this.ids).

In `@packages/layouts/src/graphviz/GraphvizLayoter.ts`:
- Around line 97-105: The JSDoc for printToDot is misleading because the method
does apply DOT post-processing via normalizeDot; update the comment for
printToDot to state that the output is normalized (normalizeDot is applied) but
that unflattening and other higher-level post-processing are not performed;
reference the printToDot method and mention normalizeDot, AiLayoutViewPrinter
and getPrinter so reviewers can locate the relevant code to change.
- Around line 64-69: normalizeDot currently removes lines by substring matching
which is brittle; update it to only remove the specific graph attribute line
(not any occurrence of the number) by either (preferred) having
DotPrinter.createGraph emit a sentinel comment next to the attribute (e.g.,
append a node like `/* __GRAPH_CLUSTER_MARGIN__ */` when setting [_.margin]:
GraphClusterSpace) and then have normalizeDot filter only the line containing
that sentinel, or (alternative) change normalizeDot to use an anchored regex
that only matches a graph attribute line like
/^\s*margin\s*=\s*"?${GraphClusterSpace}"?\s*;?$/ so unrelated occurrences of
the numeric value are not removed; also update the trailing comment reference to
point to the new location in DotPrinter.createGraph. Ensure references:
normalizeDot, DotPrinter.createGraph, and GraphClusterSpace are used to find the
code to change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f735de0c-ae1d-44b7-98ac-bf2b83a6741c

📥 Commits

Reviewing files that changed from the base of the PR and between 76e9e69 and 232f0e3.

⛔ Files ignored due to path filters (10)
  • packages/generators/src/drawio/__snapshots__/parse-drawio.spec.ts.snap is excluded by !**/*.snap
  • packages/layouts/layout2.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/DeploymentViewPrinter-index.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-amazon.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-cloud.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-cloud3levels.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-index.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-no-relationships.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-two-directions.dot is excluded by !**/*.dot
  • packages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-with-relationship.dot is excluded by !**/*.dot
📒 Files selected for processing (46)
  • .changeset/ai-layout-experimental.md
  • .vscode/launch.json
  • dprint.json
  • packages/config/src/node/load-config.ts
  • packages/core/src/utils/index.ts
  • packages/core/src/utils/unsafe.ts
  • packages/language-server/src/Rpc.ts
  • packages/language-server/src/common-exports.ts
  • packages/language-server/src/filesystem/LikeC4ManualLayouts.ts
  • packages/language-server/src/lsp/CompletionProvider.ts
  • packages/language-server/src/lsp/DocumentSymbolProvider.ts
  • packages/language-server/src/protocol.ts
  • packages/language-server/src/shared/NodeKindProvider.ts
  • packages/language-server/src/views/LikeC4Views.ts
  • packages/language-server/src/views/index.ts
  • packages/language-server/src/workspace/ProjectsManager.ts
  • packages/layouts/.gitignore
  • packages/layouts/build.config.ts
  • packages/layouts/model.json
  • packages/layouts/package.json
  • packages/layouts/scripts/generate.ts
  • packages/layouts/scripts/tsconfig.json
  • packages/layouts/src/graphviz/AiLayoutPrinter.ts
  • packages/layouts/src/graphviz/DeploymentViewPrinter.ts
  • packages/layouts/src/graphviz/DotPrinter.ts
  • packages/layouts/src/graphviz/DynamicViewPrinter.ts
  • packages/layouts/src/graphviz/ElementViewPrinter.ts
  • packages/layouts/src/graphviz/GraphvizLayoter.ts
  • packages/layouts/src/graphviz/GraphvizParser.ts
  • packages/layouts/src/graphviz/ProjectsViewPrinter.ts
  • packages/layouts/src/graphviz/ai/index.ts
  • packages/layouts/src/graphviz/ai/llm-input.ts
  • packages/layouts/src/graphviz/ai/llm-output.ts
  • packages/layouts/src/graphviz/ai/logger.ts
  • packages/layouts/src/graphviz/ai/orchestrator.ts
  • packages/layouts/src/graphviz/ai/prompt-system.md
  • packages/layouts/src/graphviz/ai/types.ts
  • packages/layouts/src/graphviz/utils.ts
  • packages/layouts/src/graphviz/wasm/GraphvizWasmAdapter.ts
  • packages/layouts/turbo.json
  • packages/layouts/validate.ts
  • packages/mcp/src/ctx.ts
  • packages/mcp/src/server/StdioLikeC4MCPServer.ts
  • packages/mcp/src/server/StreamableLikeC4MCPServer.ts
  • packages/mcp/src/server/createMCPServer.ts
  • packages/mcp/src/tools/apply-semantic-layout.ts
💤 Files with no reviewable changes (2)
  • packages/config/src/node/load-config.ts
  • packages/layouts/src/graphviz/utils.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/layouts/src/graphviz/ai/prompt-system.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/layouts/scripts/tsconfig.json
  • packages/layouts/src/graphviz/ai/logger.ts
  • .changeset/ai-layout-experimental.md
  • packages/layouts/src/graphviz/ai/orchestrator.ts

Comment thread packages/language-server/src/lsp/CompletionProvider.ts
Comment thread packages/language-server/src/views/LikeC4Views.ts
Comment thread packages/layouts/src/graphviz/AiLayoutPrinter.ts
Comment thread packages/layouts/src/graphviz/AiLayoutPrinter.ts
Comment thread packages/layouts/src/graphviz/DotPrinter.ts Outdated
Comment thread packages/mcp/src/tools/apply-semantic-layout.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/mcp/src/tools/apply-semantic-layout.ts (2)

83-95: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard aiLayout() before dereferencing result.diagram.

This path assumes the layouter always returns a layout. If it returns a falsy value, result.diagram throws and the tool fails with an exception instead of an MCP error response.

Suggested fix
       const result = await languageServices.views.layouter.aiLayout({
         view: view.$view,
         styles: model.$styles,
       }, hints)
+      if (!result) {
+        await mcpServer.sendLoggingMessage({
+          level: 'error',
+          data: 'AI hints were generated but layout could not be applied',
+        }, ctx.sessionId)
+        return toolError('Generated layout hints but failed to apply them')
+      }

       await languageServices.editor.applyChange({
         change: {
           op: 'save-view-snapshot',
           layout: result.diagram,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/tools/apply-semantic-layout.ts` around lines 83 - 95, The
call to languageServices.views.layouter.aiLayout may return a falsy value but
the code immediately dereferences result.diagram; update
apply-semantic-layout.ts to guard the aiLayout result (the variable result from
languageServices.views.layouter.aiLayout) before accessing result.diagram and
before calling languageServices.editor.applyChange: if result is falsy or
result.diagram is missing, short-circuit and return or throw an MCP-friendly
error (instead of calling applyChange with undefined layout), otherwise call
languageServices.editor.applyChange with change.op 'save-view-snapshot' and
layout set to result.diagram.

24-35: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return a structured tool error for unknown projects.

ensureProjectId() can still throw before you reach the findView() guard, so an invalid projectId escapes as a raw MCP exception instead of a normal tool result.

Suggested fix
     async (args, ctx) => {
-      const projectId = languageServices.projectsManager.ensureProjectId(args.projectId)
+      let projectId: string
+      try {
+        projectId = languageServices.projectsManager.ensureProjectId(args.projectId)
+      } catch {
+        return toolError(`Project "${args.projectId}" not found`)
+      }
       if (projectId !== args.projectId) {
         await mcpServer.sendLoggingMessage({
           level: 'notice',
           data: `Using project "${projectId}" instead of "${args.projectId}"`,
         }, ctx.sessionId)
       }
-      const model = await languageServices.computedModel(projectId)
+      const model = await languageServices.computedModel(projectId)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mcp/src/tools/apply-semantic-layout.ts` around lines 24 - 35,
ensureProjectId may throw for invalid project IDs so wrap the call to
languageServices.projectsManager.ensureProjectId(args.projectId) in a try/catch
and convert any thrown error into a structured tool result via toolError instead
of letting the exception escape; specifically, catch errors around
ensureProjectId, return toolError(`Project "${args.projectId}" not found` or
include the caught error.message), and otherwise proceed to call
languageServices.computedModel(projectId) and the existing view lookup
(referencing ensureProjectId, languageServices.computedModel, and toolError).
🧹 Nitpick comments (1)
packages/vscode/src/node/useTelemetry.ts (1)

113-119: 💤 Low value

logError/logUsage are gated on isDev but not on vscode.env.isTelemetryEnabled.

vscode.env.createTelemetryLogger calls sendErrorData/sendEventData only when the user has telemetry enabled, so the telemetryLogger path is already protected. However, the legacy sendTelemetry and sendTelemetryError (also in the same return object) only check isDev and rely on TelemetryReporter to enforce the isTelemetryEnabled flag internally. This inconsistency is pre-existing and not introduced by this PR, but exposing two parallel telemetry APIs (logError/logUsage vs sendTelemetry/sendTelemetryError) with subtly different enforcement models may surprise callers.

Consider documenting the distinction (or unifying the guard logic) before these methods are adopted more broadly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vscode/src/node/useTelemetry.ts` around lines 113 - 119, The
returned telemetry methods expose two inconsistent guards: logError/logUsage
(bound to telemetryLogger) are only gated by isDev while
sendTelemetry/sendTelemetryError only rely on TelemetryReporter, so unify
behavior by checking vscode.env.isTelemetryEnabled (and preserving the existing
isDev short-circuit) for all four exports: wrap
telemetryLogger.logError/logUsage and the legacy
sendTelemetry/sendTelemetryError with a no-op when
!vscode.env.isTelemetryEnabled, or alternatively add a comment above the return
explaining the distinction between telemetryLogger (which internally respects
vscode.env.isTelemetryEnabled) and the legacy TelemetryReporter APIs; update
references to telemetryLogger, logError, logUsage, sendTelemetry,
sendTelemetryError and isDev accordingly so callers see consistent behavior or
clear documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/mcp/src/server/createMCPServer.ts`:
- Around line 114-123: The viewId completer currently returns no suggestions
when ctx?.arguments?.['projectId'] is undefined because it directly reads the
raw argument and casts it; instead, parse the raw argument through
projectIdSchema so any default value is applied and the unchecked cast is
removed — e.g., inside the completable for viewId, replace the current projectId
lookup with something like const projectId =
projectIdSchema.parse(ctx?.arguments?.['projectId']); if parsing fails or yields
no projectId return []; then call services.builder.parseModel(projectId) as
before to produce suggestions.

In `@packages/vscode/src/commands/semanticLayoutWithAI.ts`:
- Around line 66-85: The catch around enhanceLayoutWithAI currently maps all
errors to null and causes a misleading warning on user cancellation; update the
catch handler so that if the operation was cancelled (check
token.isCancellationRequested and/or detect cancellation via the thrown error)
you rethrow (or return early) instead of returning null, and only convert
non-cancellation errors to null and log them; apply this change around the
enhanceLayoutWithAI(...) call that constructs the VscodeAILayoutProvider so
cancellation is not surfaced as "AI could not generate layout suggestions".

---

Duplicate comments:
In `@packages/mcp/src/tools/apply-semantic-layout.ts`:
- Around line 83-95: The call to languageServices.views.layouter.aiLayout may
return a falsy value but the code immediately dereferences result.diagram;
update apply-semantic-layout.ts to guard the aiLayout result (the variable
result from languageServices.views.layouter.aiLayout) before accessing
result.diagram and before calling languageServices.editor.applyChange: if result
is falsy or result.diagram is missing, short-circuit and return or throw an
MCP-friendly error (instead of calling applyChange with undefined layout),
otherwise call languageServices.editor.applyChange with change.op
'save-view-snapshot' and layout set to result.diagram.
- Around line 24-35: ensureProjectId may throw for invalid project IDs so wrap
the call to languageServices.projectsManager.ensureProjectId(args.projectId) in
a try/catch and convert any thrown error into a structured tool result via
toolError instead of letting the exception escape; specifically, catch errors
around ensureProjectId, return toolError(`Project "${args.projectId}" not found`
or include the caught error.message), and otherwise proceed to call
languageServices.computedModel(projectId) and the existing view lookup
(referencing ensureProjectId, languageServices.computedModel, and toolError).

---

Nitpick comments:
In `@packages/vscode/src/node/useTelemetry.ts`:
- Around line 113-119: The returned telemetry methods expose two inconsistent
guards: logError/logUsage (bound to telemetryLogger) are only gated by isDev
while sendTelemetry/sendTelemetryError only rely on TelemetryReporter, so unify
behavior by checking vscode.env.isTelemetryEnabled (and preserving the existing
isDev short-circuit) for all four exports: wrap
telemetryLogger.logError/logUsage and the legacy
sendTelemetry/sendTelemetryError with a no-op when
!vscode.env.isTelemetryEnabled, or alternatively add a comment above the return
explaining the distinction between telemetryLogger (which internally respects
vscode.env.isTelemetryEnabled) and the legacy TelemetryReporter APIs; update
references to telemetryLogger, logError, logUsage, sendTelemetry,
sendTelemetryError and isDev accordingly so callers see consistent behavior or
clear documentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 85b09eaa-a404-4163-a84e-a573aea459ef

📥 Commits

Reviewing files that changed from the base of the PR and between 232f0e3 and ba2eb33.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • packages/mcp/package.json
  • packages/mcp/src/ctx.ts
  • packages/mcp/src/index.ts
  • packages/mcp/src/server/createMCPServer.ts
  • packages/mcp/src/tools/_common.ts
  • packages/mcp/src/tools/apply-semantic-layout.ts
  • packages/vscode/src/commands/semanticLayoutWithAI.ts
  • packages/vscode/src/node/useLikeC4ChatParticipant.ts
  • packages/vscode/src/node/useMcpRegistration.ts
  • packages/vscode/src/node/useTelemetry.ts
  • packages/vscode/src/useWorkspaceId.ts
  • pnpm-workspace.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/vscode/src/node/useLikeC4ChatParticipant.ts

Comment thread packages/mcp/src/server/createMCPServer.ts Outdated
Comment thread packages/vscode/src/commands/semanticLayoutWithAI.ts
Comment thread packages/vite-plugin/src/plugin.ts Fixed
Comment thread packages/layouts/src/graphviz/GraphvizParser.ts Fixed
davydkov and others added 5 commits May 6, 2026 23:26
…tion or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Signed-off-by: Denis Davydkov <denis@davydkov.com>
Comment thread packages/diagram/src/likec4diagram/state/DiagramActorProvider.tsx Fixed
Comment thread packages/diagram/src/likec4diagram/state/DiagramActorProvider.tsx Fixed
Comment thread packages/likec4-spa/src/routes/__root.tsx Fixed
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Signed-off-by: Denis Davydkov <denis@davydkov.com>
useDiagramSnapshot,
useOnDiagramEvent,
} from '../../hooks/useDiagram'
import { useEditorActorRef } from '../../hooks/useEditorActor'
@davydkov davydkov merged commit 0e34eb8 into main May 8, 2026
@davydkov davydkov deleted the ai-layout branch May 8, 2026 17:01
@likec4-ci likec4-ci Bot mentioned this pull request May 8, 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