basant307/AI_Governance_Project
045
1/**2 * @license3 * Copyright 2026 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7/**8 * @fileoverview Discovers saved workflow scripts under `.qwen/workflows/`9 * (project) and `~/.qwen/workflows/` (user) and exposes each as a `/<name>`10 * slash command that dispatches the `workflow` tool with the file's path.11 * The script is read at execution time (by the tool), so edits to a saved12 * workflow take effect on the next invocation.13 *14 * Enumeration, project-over-user precedence, and the name constraint all live15 * in core's `listSavedWorkflows` — the single source of truth shared with the16 * `workflow('<name>')` in-script global. This loader only adapts the17 * discovered entries into `SlashCommand` objects.18 */19 20import type { Config, SavedWorkflowEntry } from '@qwen-code/qwen-code-core';21import {22 listSavedWorkflows,23 ToolNames,24 createDebugLogger,25} from '@qwen-code/qwen-code-core';26import type { ICommandLoader } from './types.js';27import type {28 CommandContext,29 SlashCommand,30 SlashCommandActionReturn,31} from '../ui/commands/types.js';32import { CommandKind } from '../ui/commands/types.js';33 34const debugLogger = createDebugLogger('SavedWorkflowLoader');35 36export class SavedWorkflowLoader implements ICommandLoader {37 constructor(private readonly config: Config | null) {}38 39 async loadCommands(signal: AbortSignal): Promise<SlashCommand[]> {40 if (!this.config) return [];41 // Feature gate: the `workflow` tool is only registered when the feature42 // flag is on, so without this guard the commands would dispatch a tool43 // that doesn't exist.44 if (!this.config.isWorkflowsEnabled?.()) return [];45 // Mirror FileCommandLoader: saved workflows execute project-local code, so46 // skip discovery in bare mode and in untrusted folders.47 if (this.config.getBareMode?.()) return [];48 const folderTrustEnabled = !!this.config.getFolderTrustFeature?.();49 const folderTrust = !!this.config.getFolderTrust?.();50 if (folderTrustEnabled && !folderTrust) return [];51 52 let entries: SavedWorkflowEntry[];53 try {54 entries = await listSavedWorkflows(this.config);55 } catch (e) {56 debugLogger.debug(`listSavedWorkflows failed: ${e}`);57 return [];58 }59 if (signal.aborted) return [];60 return entries.map((entry) => this.toCommand(entry));61 }62 63 private toCommand(entry: SavedWorkflowEntry): SlashCommand {64 return {65 name: entry.name,66 description: `Run the "${entry.name}" saved workflow (${entry.source})`,67 // File-derived command (all execution modes via commandUtils fallback);68 // `source` carries the distinct workflow identity for display/telemetry.69 kind: CommandKind.FILE,70 source: 'workflow-command',71 sourceLabel: 'Workflow',72 sourceDetail: entry.source, // 'project' | 'user'73 // Interactive only: the action returns a `{type:'tool'}` dispatch, which74 // the non-interactive command adapter converts to `unsupported`. Listing75 // these in headless / ACP modes would advertise a command that then fails76 // to run, so restrict them until those paths can execute a tool return.77 supportedModes: ['interactive'],78 acceptsInput: true,79 argumentHint: '[json-args]',80 action: (81 _context: CommandContext,82 args: string,83 ): SlashCommandActionReturn => {84 const toolArgs: Record<string, unknown> = {85 // The tool reads the file fresh at execution time (hot reload).86 scriptPath: entry.scriptPath,87 };88 const trimmed = (args ?? '').trim();89 if (trimmed.length > 0) {90 // Forward user-supplied text to the script's `args` global. Parse as91 // JSON when valid (objects / arrays / numbers — matching the tool's92 // "actual JSON value" contract), else pass the raw string so plain93 // text still reaches the script.94 toolArgs['args'] = tryParseJson(trimmed);95 }96 return {97 type: 'tool',98 toolName: ToolNames.WORKFLOW,99 toolArgs,100 };101 },102 };103 }104}105 106function tryParseJson(raw: string): unknown {107 try {108 return JSON.parse(raw);109 } catch {110 return raw;111 }112}113 