diff --git a/.gitignore b/.gitignore index 286df82..e44e5df 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ test-results.xml *.vsix *.log *.interp -*.tokens \ No newline at end of file +*.tokens +ecl-sample/.vscode/launch-secure.json diff --git a/package-lock.json b/package-lock.json index a8251c4..730d5ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1707,9 +1707,9 @@ } }, "node_modules/@fluentui/react-window-provider": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.3.1.tgz", - "integrity": "sha512-yzKYYnPEimNXU2YvgY/aCru997DnD6O4fg+NXVwtxHaOq/hSaAlSugs4elVzlwbYOBbdEcELL5Rx1bUhRMvq9Q==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.3.2.tgz", + "integrity": "sha512-T15zFPIWr9De8hNkapne7YyvcxclyTK2bMXXHZwbWLkVeH/lGHRG0CIy/calNGKa86wuzMJhq8iqFW2W6+EwVQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1740,22 +1740,22 @@ "dependencies": { "@fluentui/merge-styles": "^8.6.14", "@fluentui/set-version": "^8.2.24", - "@fluentui/theme": "^2.7.1", - "@fluentui/utilities": "^8.17.1", + "@fluentui/theme": "^2.6.67", + "@fluentui/utilities": "^8.15.23", "@microsoft/load-themed-styles": "^1.10.26", "tslib": "^2.1.0" } }, "node_modules/@fluentui/theme": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.1.tgz", - "integrity": "sha512-r3DONB0UtrfHx8qGaKdPT/kUOGUx3Rdfj5DNa7CuUbzKuHaI8LDd7MkmG2FBzL2iA0m3hdQ6j9bfn4Ya64wwNw==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.7.2.tgz", + "integrity": "sha512-UXGNfGa/1bLmYrOpmHXdvyc7CzlNSKUQAADweTncbNoMF1DvscWEjPj5kxFgCmOU8wVtvvn4GraNNUSWtNxeeA==", "dev": true, "license": "MIT", "dependencies": { "@fluentui/merge-styles": "^8.6.14", "@fluentui/set-version": "^8.2.24", - "@fluentui/utilities": "^8.17.1", + "@fluentui/utilities": "^8.17.2", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1764,15 +1764,15 @@ } }, "node_modules/@fluentui/utilities": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.1.tgz", - "integrity": "sha512-foFe3QPDY+qaVX2kqJT6f4oWfGF0Hc4VOedr1e+j0RnMm4YL1DEeQwVl0tAqkbu0VZlcVNnjINjb77ATRprA8g==", + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.17.2.tgz", + "integrity": "sha512-TmeWVtGN+Lk0mch7tuRcbkeMdrBwltI68fvQbPwcNLo4igFtTInMmjEnVJGa7pBQN5lQAmHYqB9IJI6RZU/t6w==", "dev": true, "license": "MIT", "dependencies": { "@fluentui/dom-utilities": "^2.3.10", "@fluentui/merge-styles": "^8.6.14", - "@fluentui/react-window-provider": "^2.3.1", + "@fluentui/react-window-provider": "^2.3.2", "@fluentui/set-version": "^8.2.24", "tslib": "^2.1.0" }, @@ -1961,6 +1961,7 @@ "@xmldom/xmldom": "0.9.8", "abort-controller": "3.0.0", "node-fetch": "3.3.2", + "tmp": "0.2.5", "undici": "7.16.0" } }, @@ -3462,6 +3463,102 @@ } } }, + "node_modules/@langchain/community/node_modules/langchain": { + "version": "0.3.37", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.37.tgz", + "integrity": "sha512-1jPsZ6xsxkcQPUvqRjvfuOLwZLLyt49hzcOK7OYAJovIkkOxd5gzK4Yw6giPUQ8g4XHyvULNlWBz+subdkcokw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@langchain/openai": ">=0.1.0 <0.7.0", + "@langchain/textsplitters": ">=0.0.0 <0.2.0", + "js-tiktoken": "^1.0.12", + "js-yaml": "^4.1.0", + "jsonpointer": "^5.0.1", + "langsmith": "^0.3.67", + "openapi-types": "^12.1.3", + "p-retry": "4", + "uuid": "^10.0.0", + "yaml": "^2.2.1", + "zod": "^3.25.32" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/anthropic": "*", + "@langchain/aws": "*", + "@langchain/cerebras": "*", + "@langchain/cohere": "*", + "@langchain/core": ">=0.3.58 <0.4.0", + "@langchain/deepseek": "*", + "@langchain/google-genai": "*", + "@langchain/google-vertexai": "*", + "@langchain/google-vertexai-web": "*", + "@langchain/groq": "*", + "@langchain/mistralai": "*", + "@langchain/ollama": "*", + "@langchain/xai": "*", + "axios": "*", + "cheerio": "*", + "handlebars": "^4.7.8", + "peggy": "^3.0.2", + "typeorm": "*" + }, + "peerDependenciesMeta": { + "@langchain/anthropic": { + "optional": true + }, + "@langchain/aws": { + "optional": true + }, + "@langchain/cerebras": { + "optional": true + }, + "@langchain/cohere": { + "optional": true + }, + "@langchain/deepseek": { + "optional": true + }, + "@langchain/google-genai": { + "optional": true + }, + "@langchain/google-vertexai": { + "optional": true + }, + "@langchain/google-vertexai-web": { + "optional": true + }, + "@langchain/groq": { + "optional": true + }, + "@langchain/mistralai": { + "optional": true + }, + "@langchain/ollama": { + "optional": true + }, + "@langchain/xai": { + "optional": true + }, + "axios": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "handlebars": { + "optional": true + }, + "peggy": { + "optional": true + }, + "typeorm": { + "optional": true + } + } + }, "node_modules/@langchain/community/node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -6478,9 +6575,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz", - "integrity": "sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA==", + "version": "2.9.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.8.tgz", + "integrity": "sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6616,9 +6713,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -6636,11 +6733,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -6921,9 +7018,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001755", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", - "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", "dev": true, "funding": [ { @@ -8061,9 +8158,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.255", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.255.tgz", - "integrity": "sha512-Z9oIp4HrFF/cZkDPMpz2XSuVpc1THDpT4dlmATFlJUIBVCy9Vap5/rIXsASP1CscBacBqhabwh8vLctqBwEerQ==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -10992,13 +11089,13 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dev": true, "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -13438,9 +13535,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -15773,9 +15870,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 195c044..c1ea1d5 100644 --- a/package.json +++ b/package.json @@ -159,24 +159,61 @@ "contributes": { "languageModelTools": [ { - "name": "ecl-extension-syntaxCheck", + "name": "ecl-extension-findWorkunits", "tags": [ - "editors", - "syntax check", + "workunits", + "wus", + "search", + "query", "ecl-extension" ], - "toolReferenceName": "syntaxCheck", - "displayName": "Syntax Check", - "modelDescription": "Check the syntax of ECL code", + "toolReferenceName": "findWorkunits", + "displayName": "Find Workunits (wus)", + "modelDescription": "Search for workunits (WU or WUs) on the active HPCC Platform session. Can filter by cluster, owner, state, WUID pattern, limit the number of results, and choose the sort column and order.", "canBeReferencedInPrompt": true, - "icon": "$(files)", + "icon": "$(search)", "inputSchema": { "type": "object", "properties": { - "ecl": { + "count": { + "type": "number", + "description": "Maximum number of workunits to return (default: 20, upper bound: 2000)" + }, + "cluster": { "type": "string", - "description": "The ECL code to check.", - "default": "" + "description": "Filter by cluster name" + }, + "owner": { + "type": "string", + "description": "Filter by owner/username" + }, + "state": { + "type": "string", + "description": "Filter by workunit state (e.g., completed, failed, running, blocked, etc.)" + }, + "wuidPattern": { + "type": "string", + "description": "Search pattern for workunit ID (WUID)" + }, + "sortOrder": { + "type": "string", + "enum": [ + "Protection", + "Wuid", + "Owner", + "Jobname", + "Cluster", + "State", + "ClusterTime", + "Compile Cost", + "Execution Cost", + "File Access Cost" + ], + "description": "Sort results by a specific workunit field" + }, + "descending": { + "type": "boolean", + "description": "Convenience flag to request descending order (overrides sortOrder when true)." } } } @@ -186,11 +223,13 @@ "tags": [ "logical files", "search", - "ecl-extension" + "ecl-extension", + "ecl", + "data" ], "toolReferenceName": "findLogicalFiles", "displayName": "Find Logical Files", - "modelDescription": "Search for logical files that match the wildcard pattern on the active HPCC Platform session", + "modelDescription": "When user mentions data files, datasets, or logical files in ECL context, use this tool to search for logical files on the HPCC Platform. Supports wildcard patterns. Essential for ECL data exploration and when user asks 'what files are available' or references file names.", "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", @@ -204,6 +243,31 @@ "pattern" ] } + }, + { + "name": "ecl-extension-syntaxCheck", + "tags": [ + "editors", + "syntax check", + "ecl-extension", + "ecl", + "validation" + ], + "toolReferenceName": "syntaxCheck", + "displayName": "Syntax Check", + "modelDescription": "Validates ECL code syntax using the HPCC Platform compiler. Use this tool before submitting code or when user asks about code correctness. Essential for ECL development workflow.", + "canBeReferencedInPrompt": true, + "icon": "$(files)", + "inputSchema": { + "type": "object", + "properties": { + "ecl": { + "type": "string", + "description": "The ECL code to check.", + "default": "" + } + } + } } ], "languages": [ diff --git a/src/ecl/lm/tools.ts b/src/ecl/lm/tools.ts index 3e4aa24..4aa56a9 100644 --- a/src/ecl/lm/tools.ts +++ b/src/ecl/lm/tools.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode"; +import { FindWorkunitsTool } from "./tools/findWorkunits"; import { FindLogicalFilesTool } from "./tools/findLogicalFiles"; import { SyntaxCheckTool } from "./tools/syntaxCheck"; @@ -7,8 +8,9 @@ let eclLMTools: ECLLMTools; export class ECLLMTools { protected constructor(ctx: vscode.ExtensionContext) { - ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-syntaxCheck", new SyntaxCheckTool())); + ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-findWorkunits", new FindWorkunitsTool())); ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-findLogicalFiles", new FindLogicalFilesTool())); + ctx.subscriptions.push(vscode.lm.registerTool("ecl-extension-syntaxCheck", new SyntaxCheckTool())); } static attach(ctx: vscode.ExtensionContext): ECLLMTools { diff --git a/src/ecl/lm/tools/findWorkunits.ts b/src/ecl/lm/tools/findWorkunits.ts new file mode 100644 index 0000000..20e6d9b --- /dev/null +++ b/src/ecl/lm/tools/findWorkunits.ts @@ -0,0 +1,294 @@ +import * as vscode from "vscode"; +import { WsWorkunits } from "@hpcc-js/comms"; +import { isPlatformConnected } from "../../../hpccplatform/session"; +import { reporter } from "../../../telemetry"; +import localize from "../../../util/localize"; +import { logToolEvent, requireConnectedSession, throwIfCancellationRequested } from "../utils"; + +enum SortBy { + protection = "Protection", + wuid = "Wuid", + owner = "Owner", + jobname = "Jobname", + cluster = "Cluster", + state = "State", + clustertime = "ClusterTime", + compilecost = "Compile Cost", + executioncost = "Execution Cost", + fileaccesscost = "File Access Cost" +} + +export interface IFindWorkunitsParameters { + /** + * Maximum number of workunits to return (default: 20) + */ + count?: number; + /** + * Filter by cluster name + */ + cluster?: string; + /** + * Filter by owner/username + */ + owner?: string; + /** + * Filter by state (e.g., "completed", "failed", "running") + */ + state?: string; + /** + * Search pattern for workunit ID (WUID) + */ + wuidPattern?: string; + /** + * Field to sort by (e.g. "wuid", "owner", "cluster", "state", "jobname"). + */ + sortOrder?: SortBy; + + /** + * Convenience flag to request descending order (overrides sortOrder when provided). + */ + descending?: boolean; +} + +export class FindWorkunitsTool implements vscode.LanguageModelTool { + async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken) { + reporter?.sendTelemetryEvent("lmTool.invoke", { tool: "findWorkunits" }); + const params = options.input; + + const sortKeyLookup: Record = { + wuid: { field: "Wuid", labelKey: "workunit ID" }, + id: { field: "Wuid", labelKey: "workunit ID" }, + owner: { field: "Owner", labelKey: "owner" }, + user: { field: "Owner", labelKey: "owner" }, + cluster: { field: "Cluster", labelKey: "cluster" }, + state: { field: "State", labelKey: "state" }, + status: { field: "State", labelKey: "state" }, + jobname: { field: "Jobname", labelKey: "job name" }, + job: { field: "Jobname", labelKey: "job name" }, + protection: { field: "Protection", labelKey: "protection status" }, + clustertime: { field: "ClusterTime", labelKey: "cluster time" }, + "cluster time": { field: "ClusterTime", labelKey: "cluster time" }, + compilecost: { field: "Compile Cost", labelKey: "compile cost" }, + "compile cost": { field: "Compile Cost", labelKey: "compile cost" }, + executioncost: { field: "Execution Cost", labelKey: "execution cost" }, + "execution cost": { field: "Execution Cost", labelKey: "execution cost" }, + fileaccesscost: { field: "File Access Cost", labelKey: "file access cost" }, + "file access cost": { field: "File Access Cost", labelKey: "file access cost" }, + }; + + const sortByRaw = typeof params.sortOrder === "string" ? params.sortOrder.trim().toLowerCase() : undefined; + let sortByField: string | undefined; + let sortLabel: string | undefined; + let unsupportedSortKey: string | undefined; + + if (sortByRaw) { + const mapping = sortKeyLookup[sortByRaw]; + if (mapping) { + sortByField = mapping.field; + sortLabel = localize(mapping.labelKey); + } else { + unsupportedSortKey = params.sortOrder; + } + } + + let descending: boolean | undefined; + let sortOrderLabel: string | undefined; + + if (typeof params.descending === "boolean") { + descending = params.descending; + sortOrderLabel = descending ? localize("descending") : localize("ascending"); + } + + logToolEvent("findWorkunits", "invoke start", { + count: params.count, + cluster: params.cluster, + owner: params.owner, + state: params.state, + wuidPattern: params.wuidPattern, + sortBy: sortByField ?? params.sortOrder, + descending, + unsupportedSortKey, + }); + + const session = requireConnectedSession(); + + const requestCount = Math.min(Math.max(params.count ?? 20, 1), 2000); + + // Build WUQuery request from parameters + const request: Partial = { + Count: requestCount, + }; + + if (params.cluster) { + request.Cluster = params.cluster; + } + if (params.owner) { + request.Owner = params.owner; + } + if (params.state) { + request.State = params.state; + } + if (params.wuidPattern) { + request.Wuid = params.wuidPattern; + } + if (sortByField) { + request.Sortby = sortByField; + } + if (descending !== undefined) { + request.Descending = descending; + } + + throwIfCancellationRequested(token); + + try { + const workunits = await session.wuQuery(request); + + throwIfCancellationRequested(token); + + const parts: vscode.LanguageModelTextPart[] = []; + + if (workunits.length === 0) { + parts.push(new vscode.LanguageModelTextPart(localize("No workunits found matching the specified criteria."))); + } else { + parts.push(new vscode.LanguageModelTextPart(localize("Found {0} workunit(s).", workunits.length.toString()))); + + const list = workunits.map(wu => { + const state = wu.State || localize("unknown state"); + const jobName = wu.Jobname || localize("unnamed job"); + return `- ${wu.Wuid} (${state}) — ${jobName}`; + }).join("\n"); + parts.push(new vscode.LanguageModelTextPart(list)); + + for (const wu of workunits) { + const detailsUrl = session.wuDetailsUrl(wu.Wuid); + const wuInfo = { + Wuid: wu.Wuid, + Owner: wu.Owner, + Cluster: wu.Cluster, + Jobname: wu.Jobname, + StateID: wu.StateID, + State: wu.State, + Protected: !!wu.Protected, + Action: wu.Action, + ActionEx: wu.ActionEx, + DateTimeScheduled: wu.DateTimeScheduled, + IsPausing: !!wu.IsPausing, + ThorLCR: !!wu.ThorLCR, + TotalClusterTime: wu.TotalClusterTime, + ExecuteCost: wu.ExecuteCost, + FileAccessCost: wu.FileAccessCost, + CompileCost: wu.CompileCost, + NoAccess: !!wu.NoAccess, + detailsUrl, + }; + parts.push(new vscode.LanguageModelTextPart(JSON.stringify(wuInfo, null, 2))); + } + } + + if (sortLabel) { + const summary = sortOrderLabel + ? localize("Sorted by {0} ({1}).", sortLabel, sortOrderLabel) + : localize("Sorted by {0}.", sortLabel); + parts.push(new vscode.LanguageModelTextPart(summary)); + } else if (sortOrderLabel) { + parts.push(new vscode.LanguageModelTextPart(localize("Sort order: {0}.", sortOrderLabel))); + } + + if (unsupportedSortKey) { + parts.push(new vscode.LanguageModelTextPart(localize("Sort key \"{0}\" is not supported; using default ordering.", unsupportedSortKey))); + } + + logToolEvent("findWorkunits", "invoke success", { + resultCount: workunits.length, + sortBy: request.Sortby, + descending, + unsupportedSortKey, + }); + + return new vscode.LanguageModelToolResult(parts); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logToolEvent("findWorkunits", "invoke failed", { + error: message, + sortBy: sortByField ?? params.sortOrder, + descending, + unsupportedSortKey, + }); + throw new vscode.LanguageModelError(localize("Failed to query workunits: {0}", message), { cause: error }); + } + } + + async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions, _token: vscode.CancellationToken) { + const connected = isPlatformConnected(); + const params = options.input; + + const filters: string[] = []; + if (params.cluster) filters.push(localize("cluster: {0}", params.cluster)); + if (params.owner) filters.push(localize("owner: {0}", params.owner)); + if (params.state) filters.push(localize("state: {0}", params.state)); + if (params.wuidPattern) filters.push(localize("WUID: {0}", params.wuidPattern)); + + const sortLabelLookup: Record = { + wuid: "workunit ID", + id: "workunit ID", + owner: "owner", + user: "owner", + cluster: "cluster", + state: "state", + status: "state", + jobname: "job name", + job: "job name", + protection: "protection status", + clustertime: "cluster time", + "cluster time": "cluster time", + compilecost: "compile cost", + "compile cost": "compile cost", + executioncost: "execution cost", + "execution cost": "execution cost", + fileaccesscost: "file access cost", + "file access cost": "file access cost", + }; + + const sortByRaw = typeof params.sortOrder === "string" ? params.sortOrder.trim().toLowerCase() : undefined; + const sortLabel = sortByRaw ? sortLabelLookup[sortByRaw] : undefined; + if (sortLabel) { + const localizedSortLabel = localize(sortLabel); + filters.push(localize("sort by {0}", localizedSortLabel)); + } else if (params.sortOrder) { + filters.push(localize("sort by {0}", params.sortOrder)); + } + + const sortOrderRaw = typeof params.sortOrder === "string" ? params.sortOrder.trim().toLowerCase() : undefined; + let orderLabel: string | undefined; + if (sortOrderRaw === "asc" || sortOrderRaw === "ascending") { + orderLabel = localize("ascending"); + } else if (sortOrderRaw === "desc" || sortOrderRaw === "descending") { + orderLabel = localize("descending"); + } else if (params.sortOrder) { + orderLabel = params.sortOrder; + } + + if (!orderLabel && typeof params.descending === "boolean") { + orderLabel = params.descending ? localize("descending") : localize("ascending"); + } + + if (orderLabel) { + filters.push(localize("order: {0}", orderLabel)); + } + + let searchDesc = localize("recent workunits"); + if (filters.length > 0) { + searchDesc = filters.join(", "); + } + + return { + invocationMessage: connected + ? localize("Searching for {0}", searchDesc) + : localize("Cannot search: HPCC Platform not connected"), + confirmationMessages: connected ? undefined : { + title: localize("HPCC Platform not connected"), + message: new vscode.MarkdownString(localize("This tool requires an active HPCC connection.")), + } + }; + } +}