This repository was archived by the owner on Oct 31, 2025. It is now read-only.
forked from continuedev/continue
-
Notifications
You must be signed in to change notification settings - Fork 3
Feat: Add a validating process to inject missing virtual references #71
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import { graniteCodeModelSlugs } from "@continuedev/config-yaml"; | ||
| import fs from "fs"; | ||
| import * as YAML from "yaml"; | ||
| import { IDE } from "../.."; | ||
| import { getConfigYamlPath } from "../../util/paths"; | ||
| import { parseUsesSlug } from "../utils/parseUsesSlug"; | ||
|
|
||
| const keyOrder = ["name", "version", "schema", "models"]; | ||
|
|
||
| function extractSlugsFromComment(comment: string | undefined | null): string[] { | ||
| const res: string[] = []; | ||
|
|
||
| if (!comment) { | ||
| return res; | ||
| } | ||
|
|
||
| const lines = comment.split("\n"); | ||
| for (const line of lines) { | ||
| let slug = parseUsesSlug(line); | ||
| if (slug) { | ||
| res.push(slug); | ||
| } | ||
| } | ||
| return res; | ||
| } | ||
|
|
||
| /** | ||
| * Determines which model slugs need to be added. | ||
| * | ||
| * This function identifies which model slugs from the predefined `graniteCodeModelSlugs` array are not currently in use within the provided YAML sequence of models. It considers two sources of "in-use" slugs: | ||
| * 1. An active model entry (e.g. `- uses: $granite-code/models/chat`). | ||
| * 2. Any comment within the `models` block (checked via a deep search). | ||
| * | ||
| * @param models The YAML sequence of models to check against. | ||
| * @returns An array of strings representing the model slugs that are not in use. | ||
| */ | ||
| function slugsToAdd(models: YAML.YAMLSeq): string[] { | ||
| const graniteCodeModelSlugsSet = new Set(graniteCodeModelSlugs); | ||
|
|
||
| // Trailing comments of models section | ||
| const slugs = extractSlugsFromComment(models.comment); | ||
| for (const slug of slugs) { | ||
| graniteCodeModelSlugsSet.delete(slug); | ||
| } | ||
|
|
||
| // Models in use | ||
| for (const model of models.items) { | ||
| if (YAML.isMap(model)) { | ||
| const usesSlug = model.get("uses"); | ||
| if (usesSlug && typeof usesSlug === "string") { | ||
| graniteCodeModelSlugsSet.delete(usesSlug); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Commented out models | ||
| const extractedSlugs = extractYamlCommentsDFS(models); | ||
| extractedSlugs.forEach((slug) => graniteCodeModelSlugsSet.delete(slug)); | ||
|
|
||
| return [...graniteCodeModelSlugsSet]; | ||
| } | ||
|
|
||
| /** | ||
| * Recursively extracts "slugs" from all comments within a YAML document structure. | ||
| * | ||
| * This function performs a depth-first search (DFS) through a given YAML Map (`{...}`) or | ||
| * Sequence (`[...]`). It inspects the `comment` (inline) and `commentBefore` (on the preceding line) | ||
| * properties of every key, value, and item. It then uses a helper function, `extractSlugsFromComment`, | ||
| * to parse slugs from those comment strings. | ||
| * | ||
| * The final result is a deduplicated array of all unique slugs found throughout the entire | ||
| * nested structure. | ||
| * | ||
| * @param {YAML.YAMLMap | YAML.YAMLSeq} yamlNode - The root YAML node (Map or Sequence) to start the search from. | ||
| * @returns {string[]} A unique, flat array of all slugs extracted from the comments. | ||
| */ | ||
| function extractYamlCommentsDFS( | ||
| yamlNode: YAML.YAMLMap | YAML.YAMLSeq, | ||
| ): string[] { | ||
| let extractedSlugs: string[] = []; | ||
|
|
||
| function pushSlugs(node: YAML.Node) { | ||
| extractedSlugs.push(...extractSlugsFromComment(node.comment)); | ||
| extractedSlugs.push(...extractSlugsFromComment(node.commentBefore)); | ||
| } | ||
|
|
||
| // Comments of the current node | ||
| pushSlugs(yamlNode); | ||
|
|
||
| for (const item of yamlNode.items) { | ||
| if (YAML.isPair(item)) { | ||
| // Key Section | ||
| // If it's not a scalar, ignore it | ||
| if (YAML.isScalar(item.key)) { | ||
| pushSlugs(item.key); | ||
| } | ||
|
|
||
| // Value Section | ||
| // Only consider Scalar, Map, and Seq | ||
| if (YAML.isScalar(item.value)) { | ||
| pushSlugs(item.value); | ||
| } else if (YAML.isMap(item.value)) { | ||
| extractedSlugs.push(...extractYamlCommentsDFS(item.value)); | ||
| } else if (YAML.isSeq(item.value)) { | ||
| extractedSlugs.push(...extractYamlCommentsDFS(item.value)); | ||
| } | ||
| } else if (YAML.isMap(item)) { | ||
| extractedSlugs.push(...extractYamlCommentsDFS(item)); | ||
| } else if (YAML.isSeq(item)) { | ||
| extractedSlugs.push(...extractYamlCommentsDFS(item)); | ||
| } | ||
| } | ||
|
|
||
| return [...new Set(extractedSlugs)]; | ||
| } | ||
|
|
||
| /** | ||
| * Validates and injects missing virtual references into the global configuration YAML file. | ||
| * | ||
| * This function reads the global YAML config, ensures a `models` sequence (an array) exists, | ||
| * and then populates it with any required virtual model references ("slugs") that are missing. | ||
| * The changes are then written back to the file. | ||
| * | ||
| * It operates safely by: | ||
| * - Aborting without changes if the YAML file has parsing errors. | ||
| * - Catching any exceptions during file I/O or processing and displaying a warning toast in the IDE. | ||
| * | ||
| * @param {IDE} ide - The IDE instance, used to display a warning notification if the process fails. | ||
| * @param {string} configFilePath - The path to the config YAML file to validate. If not provided, the default path will be used. | ||
| */ | ||
| export async function validateVirtualReferences( | ||
| ide: IDE, | ||
| configFilePath?: string, | ||
| ): Promise<void> { | ||
| try { | ||
| const filePath = configFilePath || getConfigYamlPath("vscode"); | ||
| const rawContent = (await fs.promises.readFile(filePath)).toString(); | ||
| const yamlDoc = YAML.parseDocument(rawContent); | ||
|
|
||
| // Don't do anything, if there is a parsing error | ||
| if (yamlDoc.errors.length !== 0) { | ||
| return; | ||
| } | ||
|
|
||
| // Add an empty models section if there is no models section | ||
| if (yamlDoc.get("models") === undefined) { | ||
| yamlDoc.add(new YAML.Pair("models", [])); | ||
| } | ||
|
|
||
| const models = yamlDoc.get("models"); | ||
|
|
||
| // Models should be a Seq | ||
| if (YAML.isSeq(models)) { | ||
| // Change to block style if models section is empty | ||
| if (models.items.length === 0) { | ||
| models.flow = false; | ||
| } | ||
|
|
||
| const missingRefs = slugsToAdd(models); | ||
| // Only inject missing references if there are any | ||
| if (missingRefs.length > 0) { | ||
| for (const missingRef of missingRefs) { | ||
| const node = yamlDoc.createNode({ uses: missingRef }); | ||
| models.add(node); | ||
| } | ||
| const stringYaml = yamlDoc.toString(); | ||
| await fs.promises.writeFile(filePath, stringYaml); | ||
| } | ||
| } | ||
| } catch { | ||
| void ide.showToast( | ||
| "warning", | ||
| "Failed to inject missing virtual references", | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| import { graniteCodeModelSlugs } from "@continuedev/config-yaml"; | ||
| import path from "path"; | ||
| import { afterAll, beforeAll, describe, expect, it } from "vitest"; | ||
| import { testIde } from "../../test/fixtures"; | ||
| import { | ||
| addToTestDir, | ||
| setUpTestDir, | ||
| tearDownTestDir, | ||
| TEST_DIR_PATH, | ||
| } from "../../test/testDir"; | ||
| import { localPathToUri } from "../../util/pathToUri"; | ||
| import { validateVirtualReferences } from "./virtualReference"; | ||
|
|
||
| describe("Test validateVirtualReferences", () => { | ||
| beforeAll(async () => { | ||
| setUpTestDir(); | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| tearDownTestDir(); | ||
| }); | ||
|
|
||
| it("injects all references when the models section is empty", async () => { | ||
| const injectedReferences = graniteCodeModelSlugs | ||
| .map((slug) => ` - uses: ${slug}`) | ||
| .join("\n"); | ||
| const rawConfig = `name: Local Assistant | ||
| version: 1.0.0 | ||
| schema: v1 | ||
| models: []`; | ||
| const expectedResult = `name: Local Assistant | ||
| version: 1.0.0 | ||
| schema: v1 | ||
| models: | ||
| ${injectedReferences} | ||
| `; | ||
| addToTestDir([["config.yaml", rawConfig]]); | ||
|
|
||
| const configUri = localPathToUri(path.join(TEST_DIR_PATH, "config.yaml")); | ||
| await validateVirtualReferences( | ||
| testIde, | ||
| path.join(TEST_DIR_PATH, "config.yaml"), | ||
| ); | ||
|
|
||
| const res = await testIde.readFile(configUri); | ||
|
|
||
| expect(res).toBe(expectedResult); | ||
| }); | ||
|
|
||
| it("don't inject the references that are commented out", async () => { | ||
| const commentedOutRef = graniteCodeModelSlugs[0]; | ||
| const injectedReferences = graniteCodeModelSlugs | ||
| .slice(1) | ||
| .map((slug) => ` - uses: ${slug}`) | ||
| .join("\n"); | ||
| const rawConfig = `name: Local Assistant | ||
| version: 1.0.0 | ||
| schema: v1 | ||
| models: | ||
| # - uses: ${commentedOutRef} | ||
| - name: Local Chat Model | ||
| provider: ollama | ||
| model: granite3.3:8b | ||
| roles: | ||
| - chat`; | ||
|
|
||
| const expectedResult = `name: Local Assistant | ||
| version: 1.0.0 | ||
| schema: v1 | ||
| models: | ||
| # - uses: ${commentedOutRef} | ||
| - name: Local Chat Model | ||
| provider: ollama | ||
| model: granite3.3:8b | ||
| roles: | ||
| - chat | ||
| ${injectedReferences} | ||
| `; | ||
|
|
||
| addToTestDir([["config.yaml", rawConfig]]); | ||
|
|
||
| const configUri = localPathToUri(path.join(TEST_DIR_PATH, "config.yaml")); | ||
| await validateVirtualReferences( | ||
| testIde, | ||
| path.join(TEST_DIR_PATH, "config.yaml"), | ||
| ); | ||
|
|
||
| const res = await testIde.readFile(configUri); | ||
|
|
||
| expect(res).toBe(expectedResult); | ||
| }); | ||
|
|
||
| it("don't inject the references that are commented out (flow style)", async () => { | ||
| const commentedOutRef = graniteCodeModelSlugs[0]; | ||
| const injectedReferences = graniteCodeModelSlugs | ||
| .slice(1) | ||
| .map((slug) => ` { uses: ${slug} }`) | ||
| .join(",\n"); | ||
| const rawConfig = `name: Local Assistant | ||
| version: 1.0.0 | ||
| schema: v1 | ||
| models: [ | ||
| # {uses: ${commentedOutRef}, override: { name: new name }}, | ||
| { name: Local Chat Model, | ||
| provider: ollama, | ||
| model: granite3.3:8b, | ||
| roles: [chat], | ||
| } | ||
| ]`; | ||
|
|
||
| const expectedResult = `name: Local Assistant | ||
| version: 1.0.0 | ||
| schema: v1 | ||
| models: | ||
| [ | ||
| # {uses: ${commentedOutRef}, override: { name: new name }}, | ||
| { | ||
| name: Local Chat Model, | ||
| provider: ollama, | ||
| model: granite3.3:8b, | ||
| roles: [ chat ] | ||
| }, | ||
| ${injectedReferences} | ||
| ] | ||
| `; | ||
|
|
||
| addToTestDir([["config.yaml", rawConfig]]); | ||
|
|
||
| const configUri = localPathToUri(path.join(TEST_DIR_PATH, "config.yaml")); | ||
| await validateVirtualReferences( | ||
| testIde, | ||
| path.join(TEST_DIR_PATH, "config.yaml"), | ||
| ); | ||
|
|
||
| const res = await testIde.readFile(configUri); | ||
|
|
||
| expect(res).toBe(expectedResult); | ||
| }); | ||
|
|
||
| it("don't rewtire the config file if there are no missing references", async () => { | ||
| const commentedOutRefsWithIncreasingIndent = graniteCodeModelSlugs | ||
| .map((slug, ind) => `#${" ".repeat(ind + 1)}{ uses: ${slug} }`) | ||
| .join(",\n"); | ||
| const rawConfig = `name: Local Assistant | ||
| version: 1.0.0 | ||
| schema: v1 | ||
| models: [ | ||
| ${commentedOutRefsWithIncreasingIndent} | ||
| ]`; | ||
|
|
||
| // The expected result should keep the increasing indentation because we didn't rewrite the file | ||
| const expectedResult = rawConfig; | ||
|
|
||
| addToTestDir([["config.yaml", rawConfig]]); | ||
|
|
||
| const configUri = localPathToUri(path.join(TEST_DIR_PATH, "config.yaml")); | ||
| await validateVirtualReferences( | ||
| testIde, | ||
| path.join(TEST_DIR_PATH, "config.yaml"), | ||
| ); | ||
|
|
||
| const res = await testIde.readFile(configUri); | ||
|
|
||
| expect(res).toBe(expectedResult); | ||
| }); | ||
| }); | ||
Jazzcort marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
2 changes: 1 addition & 1 deletion
2
extensions/vscode/src/extension/ConfigYamlDefinitionProvider.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
extensions/vscode/src/extension/ConfigYamlDiagnosticsProvider.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.