Conversation
🦋 Changeset detectedLatest commit: 0e34eb8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 22 packages
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 |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesAI-assisted Semantic Layout (end-to-end)
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 | 🟡 MinorMake snapshot hash deterministic by sorting scanned files first.
Current hash accumulation depends on
scanDirectoryiteration 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 | 🟠 MajorMulti-root workspaces are collapsed to the first folder here.
LIKEC4_WORKSPACEis parsed as an array, but Line 58 immediately narrows it tofirst(workspacePaths)before callingfromWorkspace(). 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 | 🟡 MinorUse
runtimeArgsfor the Node condition flag.The
--conditions=sourcesflag is a Node runtime option and should be inruntimeArgs, notargs. With"runtimeExecutable": "tsx",argsare passed to the program being debugged, whileruntimeArgsare 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
parentFqnis not actually propagated, andfqnis redundantly computed.Line 166 adds
parentFqn, but it is never consumed ingetElementsSymbol. Also, Line 187 computesfqnwhile Line 195 recomputes the same value inline. Please either wireparentFqnthrough downstream symbol naming logic or remove it for now, and at minimum reusefqnto 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 | 🟡 MinorHandle empty project lists before building the import choice snippet.
If
ProjectsManager.allis empty, this produces\${1||}, which is not a valid choice placeholder. In an empty or not-yet-indexed workspace, theimportcompletion 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 | 🟡 MinorAdd an error state to communicate failed AI layout operations to the webview.
The
stateunion 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.tsanduseLikeC4ChatParticipant.tsneed 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 | 🟡 MinorRemove unused
onMountimport (Line 12).
onMountis 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 | 🟡 MinorFix typo in rule field name.
excludeFromRanking arrashould beexcludeFromRanking 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 | 🟡 MinorTighten 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 | 🟡 MinorKeep 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 | 🟡 MinorClean 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 | 🟡 MinorRemove the stray quote in the predicate-types list.
The trailing
'after ``includeonlymakes 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 | 🟡 MinorHandle
CancellationErrorseparately to avoid showing error toast on user cancellation.When
sendRequest()withinenhanceLayoutWithAI()throwsvscode.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.CancellationErrorwithout 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 usingtrue as constfor consistent type narrowing.The error case uses
false as constfor precise type narrowing, but the success case usesres.successwhich 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 addingdependsOnto thegeneratetask.This helps Turbo orchestrate generation order predictably when multiple packages define
generate.Based on learnings: Configure Turbo task dependencies in `turbo.json` to orchestrate build order across the monorepo.🔁 Suggested update
"tasks": { "generate": { + "dependsOn": [ + "^generate" + ], "inputs": [ "scripts/**", "src/**/*.md", "package.json" ],🤖 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 byBroadcastAILayoutStateUpdate. 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: Avoidas anyfor 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
anyand type casts withas".🤖 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 throughany.
origin: 'invisible' as anyhides that these injected edges do not matchGraphologyEdgeAttributes. 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
anyand type casts withas."🤖 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
chatParticipantAdditionsAPI 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
⛔ Files ignored due to path filters (11)
packages/generators/src/drawio/__snapshots__/parse-drawio.spec.ts.snapis excluded by!**/*.snappackages/layouts/layout2.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/DeploymentViewPrinter-index.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-amazon.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-cloud.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-cloud3levels.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-index.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-no-relationships.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-two-directions.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-with-relationship.dotis excluded by!**/*.dotpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (92)
.changeset/ai-layout-experimental.md.tool-versions.vscode/launch.jsondprint.jsone2e/package.jsonpackage.jsonpackages/config/src/node/load-config.tspackages/core/src/utils/index.tspackages/core/src/utils/unsafe.tspackages/language-server/src/Rpc.tspackages/language-server/src/common-exports.tspackages/language-server/src/filesystem/LikeC4ManualLayouts.tspackages/language-server/src/lsp/CompletionProvider.tspackages/language-server/src/lsp/DocumentSymbolProvider.tspackages/language-server/src/protocol.tspackages/language-server/src/shared/NodeKindProvider.tspackages/language-server/src/views/LikeC4Views.tspackages/language-server/src/views/index.tspackages/language-server/src/workspace/ProjectsManager.tspackages/language-server/src/workspace/WorkspaceManager.tspackages/language-services/src/node/index.tspackages/layouts/.gitignorepackages/layouts/build.config.tspackages/layouts/model.jsonpackages/layouts/package.jsonpackages/layouts/scripts/generate.tspackages/layouts/scripts/tsconfig.jsonpackages/layouts/src/graphviz/AiLayoutPrinter.tspackages/layouts/src/graphviz/DeploymentViewPrinter.tspackages/layouts/src/graphviz/DotPrinter.tspackages/layouts/src/graphviz/DynamicViewPrinter.tspackages/layouts/src/graphviz/ElementViewPrinter.tspackages/layouts/src/graphviz/GraphvizLayoter.tspackages/layouts/src/graphviz/GraphvizParser.tspackages/layouts/src/graphviz/ProjectsViewPrinter.tspackages/layouts/src/graphviz/ai/index.tspackages/layouts/src/graphviz/ai/llm-input.tspackages/layouts/src/graphviz/ai/llm-output.tspackages/layouts/src/graphviz/ai/logger.tspackages/layouts/src/graphviz/ai/orchestrator.tspackages/layouts/src/graphviz/ai/prompt-system.mdpackages/layouts/src/graphviz/ai/types.tspackages/layouts/src/graphviz/utils.tspackages/layouts/src/graphviz/wasm/GraphvizWasmAdapter.tspackages/layouts/turbo.jsonpackages/layouts/validate.tspackages/vscode-preview/protocol.tspackages/vscode-preview/src/main.tsxpackages/vscode-preview/src/state.tspackages/vscode-preview/src/vscode.tspackages/vscode/.gitignorepackages/vscode/data/skills/likec4-dsl/SKILL.mdpackages/vscode/data/skills/likec4-dsl/evals/evals-public.jsonpackages/vscode/data/skills/likec4-dsl/evals/grading-spec.jsonpackages/vscode/data/skills/likec4-dsl/references/cli.mdpackages/vscode/data/skills/likec4-dsl/references/configuration.mdpackages/vscode/data/skills/likec4-dsl/references/deployment.mdpackages/vscode/data/skills/likec4-dsl/references/dynamic-views.mdpackages/vscode/data/skills/likec4-dsl/references/examples.mdpackages/vscode/data/skills/likec4-dsl/references/identifier-validity.mdpackages/vscode/data/skills/likec4-dsl/references/include-predicates-wildcards.mdpackages/vscode/data/skills/likec4-dsl/references/model.mdpackages/vscode/data/skills/likec4-dsl/references/predicates.mdpackages/vscode/data/skills/likec4-dsl/references/relationships-bidirectional.mdpackages/vscode/data/skills/likec4-dsl/references/specification.mdpackages/vscode/data/skills/likec4-dsl/references/style-tokens-colors.mdpackages/vscode/data/skills/likec4-dsl/references/troubleshooting.mdpackages/vscode/data/skills/likec4-dsl/references/views.mdpackages/vscode/package.jsonpackages/vscode/package.nls.jsonpackages/vscode/src/browser/useTelemetry.tspackages/vscode/src/commands/index.tspackages/vscode/src/commands/locate.tspackages/vscode/src/commands/semanticLayoutWithAI.tspackages/vscode/src/node/extension.tspackages/vscode/src/node/language-server.tspackages/vscode/src/node/mcp-server.tspackages/vscode/src/node/useLanguageClient.tspackages/vscode/src/node/useLikeC4ChatParticipant.tspackages/vscode/src/node/useMcpRegistration.tspackages/vscode/src/node/useTelemetry.tspackages/vscode/src/panel/activateMessenger.tspackages/vscode/src/panel/useDiagramPanel.tspackages/vscode/src/useExtensionLogger.tspackages/vscode/src/useMessenger.tspackages/vscode/src/useRpc.tspackages/vscode/tsconfig.jsonpackages/vscode/tsdown.config.tspackages/vscode/turbo.jsonpackages/vscode/vscode.proposed.chatParticipantAdditions.d.tspnpm-workspace.yamltsconfig.json
💤 Files with no reviewable changes (3)
- packages/config/src/node/load-config.ts
- packages/layouts/src/graphviz/utils.ts
- tsconfig.json
| 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') | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (7)
packages/layouts/scripts/generate.tspackages/layouts/src/graphviz/ai/llm-output.tspackages/layouts/src/graphviz/ai/orchestrator.tspackages/vscode-preview/protocol.tspackages/vscode/src/commands/semanticLayoutWithAI.tspackages/vscode/src/node/useLikeC4ChatParticipant.tspackages/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
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
packages/vscode/src/panel/activateMessenger.ts (1)
138-148: LGTM — conversion correctly centralized inuseRpc.Removing the panel-side
protocol2CodeConverter.asLocationcall is consistent withpackages/vscode/src/useRpc.ts(lines 91-103), wherechangeViewalready returns avscode.Location | null. The null-guard at line 139 keeps the subsequentshowEditorNextToPreviewcall (which requires a non-nullvscode.Locationperpackages/vscode/src/utils.ts) andvscode.workspace.save(location.uri)safe.Optional nit: if
result.locationis typed asvscode.Location | nullat the RPC boundary, the?? nullon 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...sharedspread placement across build targets.The node-CJS build spreads
...sharedat 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 sosharedwins. Today there are no overlapping keys so behavior is equivalent, but any future addition toshared(e.g.,sourcemap,define,external) will silently apply differently per target and be easy to miss. Consider normalizing placement (preferably...sharedfirst 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: Unusedresourcebinding.The return value of
mcp.registerResource(...)is never referenced. Drop the assignment to avoid the dead variable (also fixes SonarCloud'suseless assignmentwarning).🧹 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 theas ProjectIdcast.Coding guidelines discourage
ascasts.ctx?.arguments?.['projectId']is typed asunknown/string; ifparseModelrequires a brandedProjectId, prefer going throughservices.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 usinganyand type casts withas".🤖 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-layoutmissing from theinstructionstool 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 innerconst 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-outcompletableblock.Dead/commented code should not land in main. If the
completablevariant 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() }) }), soresponse.content.type !== 'text'is unreachable. The ternary reduces toresponse.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, butserver.close(...)completes later inside the Promise. In-flight requests on/mcpthat calluseMcpServer()between the unset and the actual close will throw (unctx.use()throws on unset). Consider deferring the unset until afterserver.closeresolves, or gating it on anisStoppingflag. Sincestop()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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (22)
package.jsonpackages/language-server/src/common-exports.tspackages/layouts/package.jsonpackages/likec4/src/cli/build/index.tspackages/mcp/README.mdpackages/mcp/bin/likec4-mcp.mjspackages/mcp/package.jsonpackages/mcp/src/ctx.tspackages/mcp/src/server/StdioLikeC4MCPServer.tspackages/mcp/src/server/StreamableLikeC4MCPServer.tspackages/mcp/src/server/createMCPServer.tspackages/mcp/src/tools/apply-semantic-layout.tspackages/vscode-preview/protocol.tspackages/vscode-preview/src/main.tsxpackages/vscode-preview/src/vscode.tspackages/vscode/package.jsonpackages/vscode/src/node/mcp-server.tspackages/vscode/src/panel/activateMessenger.tspackages/vscode/src/useMessenger.tspackages/vscode/tsconfig.jsonpackages/vscode/tsdown.config.tspnpm-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
- 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
…mprove cache logging
…and deleting obsolete cache files
…s; create turbo.json for task management
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>
There was a problem hiding this comment.
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
parentFqnis 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 winHash leaks unparseable-file state silently.
When
JSON5.parse(orreadFile) throws on a particular file, the catch on line 142 logs a warning and continues, buthashwas not yet updated for that file (thestringHashcall 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.parseis on line 135 andhash = 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
contentandfile.uribefore 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 winHash is order-sensitive — sort files for a stable snapshot hash.
The incremental hash chains
file.uri.toString() + contentfor each file in the order returned byfs.scanDirectory(...). Directory iteration order is filesystem- and platform-dependent (and not guaranteed byscanDirectory's contract), so the same set of manual layouts can produce different hashes across machines or restarts, defeating cache invalidation comparisons that rely onManualLayoutsSnapshot.hash.Sort
filesby 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 winEdges referencing filtered-out non-compound nodes will explode.
build()only materializes graphviz nodes for entries returned byselectViewNodes(), butselectViewEdges()is iterated independently. If a subclass override drops a non-compound node while keeping an edge that touches it,addEdge→edgeEndpoint→getGraphNode(...)will returnnull, theinvariant(isCompound(element), ...)guard will fail, and rendering throws. The currentAiLayoutViewPrinteronly reorders so this is latent, but the contract advertised onselectViewNodes/selectViewEdges("filter") invites future subclasses to hit this.Either:
- document that
selectViewEdgesmust be a subset whose endpoints all surviveselectViewNodes, 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 winRetry still bypasses
opsCountbookkeeping (unresolved from previous review).Line 63 calls
fn()directly, so a successful retry never incrementsGraphvizWasmAdapter.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 resetsopsCountto 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 liftDon't reject the entire view on compound endpoints.
This unconditional
throwstill 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 alogger.warninstead 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 winDynamic-view post-processing still missing in
aiLayout.
layout()attachessequenceLayoutfor dynamic views viacalcSequenceLayout, butaiLayout()returns the parsed diagram directly. AI-enhancing a dynamic view will therefore yield aLayoutResultwith a different shape than the standard path, breaking downstream consumers that rely onLayoutedDynamicView.sequenceLayout. Mirror the dynamic-view branch fromlayout()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 winUnused destructured tuple elements
sourceNode/targetNode.
sourceNodeandtargetNodeare 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 winRemove unused imports.
nonNullablefrom@likec4/core(line 9) andisEmptyishfromremeda(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 valueOptional: include hints presence in the debug log.
The current log records
viewId,projectId, andlayoutTypebut 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: 1andminlen: 1are dropped silently.When the LLM specifies
weight: 1orminlen: 1explicitly on aninvisibleEdge, the!== 1checks 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 understandundefinedmeans "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 valueUse
mapToNonEmptyconsistently forexcludeFromRankingandreverseRank.
mapToNonEmpty(defined at lines 138-141) already encapsulates "map → filter undefined → return undefined when empty". BothexcludeFromRankingandreverseRankre-implement essentially the same flow inline (with an addedunique()). Extracting a small helper or extendingmapToNonEmptyto 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 valueReplace dynamic
RegExpwith literalreplaceAllfor reasoning normalization.
new RegExp(\[${id}\], 'g')constructs regexes from variable input on every iteration. Since you're matching exact[id]substrings (whereidisn1/c1/e1-style), a plain stringreplaceAllis 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 winAvoid O(N²) lookups in
findNodeById.
findNodeByIdrunsview.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 valueStale 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 viathrow. 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 valueOptional: Consider consistency between required/optional collections.
ranks,edgeWeight, andedgeMinlenare required (potentially empty), while other collection-style fields use optionalNonEmptyReadonlyArray. 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 valueDoc 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 appliesnormalizeDot(...)to the result, which is a post-processing step. Update the doc to reflect that DOT-level normalization is applied (onlyunflattenis 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 valueFragile string-based filter for graph margin.
normalizeDotuses substring matching (l.includes('margin') && l.includes('${GraphClusterSpace}')) to strip a line set inDotPrinter.createGraph([_.margin]: GraphClusterSpace). This is brittle:
- If
GraphClusterSpace(currently50.1) is ever used as a numeric value elsewhere in DOT (e.g., a node margin or padding that happens to render as50.1), the filter will incorrectly drop those lines.- The trailing comment
// see DotPrinter.ts#L175no longer points at the relevant line in the refactoredDotPrinter.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 valueDead code:
reduceDefaultRankSeparationis defined but never invoked.The only call site in
postBuildis commented out (line 74), soreduceDefaultRankSeparation()(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 valueO(N·M) reordering can be simplified with a Map.
Both
selectViewNodesandselectViewEdgesrepeatedlyfindIndex+spliceon 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 valuePrefer
string | undefinedoverstring | falseforreserveNodeId.
reserveNodeIdreturnsstring | falseandgenerateGraphvizIddoeslet id = reserve...; while (id === false) .... Usingfalseas a sentinel mixes a boolean into a string-typed pipeline and is mildly confusing in a TypeScript-first codebase;string | undefined(or returningnull) reads more naturally and aligns with the rest of the file (e.g.,getGraphNode/getSubgraphreturningT | 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
⛔ Files ignored due to path filters (10)
packages/generators/src/drawio/__snapshots__/parse-drawio.spec.ts.snapis excluded by!**/*.snappackages/layouts/layout2.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/DeploymentViewPrinter-index.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-amazon.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-cloud.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-cloud3levels.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ElementViewPrinter-index.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-no-relationships.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-two-directions.dotis excluded by!**/*.dotpackages/layouts/src/graphviz/__snapshots__/ProjectsViewPrinter-with-relationship.dotis excluded by!**/*.dot
📒 Files selected for processing (46)
.changeset/ai-layout-experimental.md.vscode/launch.jsondprint.jsonpackages/config/src/node/load-config.tspackages/core/src/utils/index.tspackages/core/src/utils/unsafe.tspackages/language-server/src/Rpc.tspackages/language-server/src/common-exports.tspackages/language-server/src/filesystem/LikeC4ManualLayouts.tspackages/language-server/src/lsp/CompletionProvider.tspackages/language-server/src/lsp/DocumentSymbolProvider.tspackages/language-server/src/protocol.tspackages/language-server/src/shared/NodeKindProvider.tspackages/language-server/src/views/LikeC4Views.tspackages/language-server/src/views/index.tspackages/language-server/src/workspace/ProjectsManager.tspackages/layouts/.gitignorepackages/layouts/build.config.tspackages/layouts/model.jsonpackages/layouts/package.jsonpackages/layouts/scripts/generate.tspackages/layouts/scripts/tsconfig.jsonpackages/layouts/src/graphviz/AiLayoutPrinter.tspackages/layouts/src/graphviz/DeploymentViewPrinter.tspackages/layouts/src/graphviz/DotPrinter.tspackages/layouts/src/graphviz/DynamicViewPrinter.tspackages/layouts/src/graphviz/ElementViewPrinter.tspackages/layouts/src/graphviz/GraphvizLayoter.tspackages/layouts/src/graphviz/GraphvizParser.tspackages/layouts/src/graphviz/ProjectsViewPrinter.tspackages/layouts/src/graphviz/ai/index.tspackages/layouts/src/graphviz/ai/llm-input.tspackages/layouts/src/graphviz/ai/llm-output.tspackages/layouts/src/graphviz/ai/logger.tspackages/layouts/src/graphviz/ai/orchestrator.tspackages/layouts/src/graphviz/ai/prompt-system.mdpackages/layouts/src/graphviz/ai/types.tspackages/layouts/src/graphviz/utils.tspackages/layouts/src/graphviz/wasm/GraphvizWasmAdapter.tspackages/layouts/turbo.jsonpackages/layouts/validate.tspackages/mcp/src/ctx.tspackages/mcp/src/server/StdioLikeC4MCPServer.tspackages/mcp/src/server/StreamableLikeC4MCPServer.tspackages/mcp/src/server/createMCPServer.tspackages/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
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
packages/mcp/src/tools/apply-semantic-layout.ts (2)
83-95:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard
aiLayout()before dereferencingresult.diagram.This path assumes the layouter always returns a layout. If it returns a falsy value,
result.diagramthrows 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 winReturn a structured tool error for unknown projects.
ensureProjectId()can still throw before you reach thefindView()guard, so an invalidprojectIdescapes 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/logUsageare gated onisDevbut not onvscode.env.isTelemetryEnabled.
vscode.env.createTelemetryLoggercallssendErrorData/sendEventDataonly when the user has telemetry enabled, so thetelemetryLoggerpath is already protected. However, the legacysendTelemetryandsendTelemetryError(also in the same return object) only checkisDevand rely onTelemetryReporterto enforce theisTelemetryEnabledflag internally. This inconsistency is pre-existing and not introduced by this PR, but exposing two parallel telemetry APIs (logError/logUsagevssendTelemetry/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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (12)
packages/mcp/package.jsonpackages/mcp/src/ctx.tspackages/mcp/src/index.tspackages/mcp/src/server/createMCPServer.tspackages/mcp/src/tools/_common.tspackages/mcp/src/tools/apply-semantic-layout.tspackages/vscode/src/commands/semanticLayoutWithAI.tspackages/vscode/src/node/useLikeC4ChatParticipant.tspackages/vscode/src/node/useMcpRegistration.tspackages/vscode/src/node/useTelemetry.tspackages/vscode/src/useWorkspaceId.tspnpm-workspace.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/vscode/src/node/useLikeC4ChatParticipant.ts
…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>
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>
Summary
AiLayoutPrinterin@likec4/layoutsthat applies AI-suggested hints to DOT outputTest plan
pnpm testinpackages/layouts)🤖 Generated with Claude Code