completers/qwen_completer/qwen_completer.ps1
|
<# .SYNOPSIS Argument completer for qwen / qwen.cmd / qwen.ps1. .DESCRIPTION Provides tab completion for qwen subcommands, flags, flag values, and the --flag=value inline syntax. Dot-source this file from your $PROFILE to enable completion for all three invocation forms. Safe to source multiple times (re-registration simply replaces the completer). .EXAMPLE . "$PSScriptRoot\qwen_completer.ps1" #> Set-StrictMode -Version Latest function Initialize-QwenCompleterData { if (Get-Variable -Name QwenTopCommands -Scope Script -ErrorAction Ignore) { return } # Top-level subcommands, in upstream registration order (config.ts; 'review' is # registered lazily whenever argv contains it, so it is always reachable). $script:QwenTopCommands = @('mcp', 'extensions', 'auth', 'hooks', 'hook', 'channel', 'board', 'serve', 'sessions', 'batch', 'update', 'sandbox', 'review') # Level-2 subcommands per top-level command. $script:QwenSubSubcommands = @{ mcp = @('add', 'remove', 'list', 'reconnect', 'approve', 'reject') extensions = @('install', 'uninstall', 'list', 'update', 'disable', 'enable', 'link', 'new', 'settings', 'sources') auth = @('status', 'coding-plan', 'openrouter', 'requesty', 'api-key', 'qwen-oauth') channel = @('start', 'daemon-worker', 'stop', 'status', 'reload', 'set', 'pairing', 'configure-weixin') board = @('show', 'task', 'claim', 'done', 'ask', 'answer', 'decline', 'prune') sessions = @('list', 'ps', 'controllers') batch = @('run', 'collect', 'retry', 'cancel', 'list', 'check', 'clean') review = @('run', 'parse-args', 'match-remote', 'meta', 'issue-context', 'fetch-diff', 'comment-body', 'fetch-pr', 'capture-local', 'capture-tui', 'plan-diff', 'cache-commit', 'repo-context', 'pr-context', 'comment-status', 'load-rules', 'agent-prompt', 'emit-workflow', 'build-test', 'base-tree', 'scratch-tree', 'test-delta', 'fix-delta', 'drive', 'ab-drive', 'mock-provider', 'extract-step', 'script-lint', 'dedup-candidates', 'revert-hunk', 'resolve-anchors', 'check-coverage', 'cost-ledger', 'presubmit', 'test-efficacy', 'test-plan', 'findings', 'recover-findings', 'publish-assets', 'compose-review', 'save-artifact', 'submit', 'cleanup') hooks = @() hook = @() serve = @() update = @() sandbox = @() } # Level-3 subcommands: "cmd.subcmd" -> @(subsubcmds). $script:QwenL3Subcommands = @{ 'extensions.settings' = @('set', 'list') 'extensions.sources' = @('add', 'remove', 'list', 'update') 'channel.pairing' = @('list', 'approve') 'sessions.controllers' = @('add', 'list', 'remove') } # Commands that are parsed but never offered: 'auth' and its legacy subcommands # only print "qwen auth has been removed.", and 'channel daemon-worker' is # declared with describe: false (internal). $script:QwenHiddenCommands = [System.Collections.Generic.HashSet[string]]::new( [string[]]@('auth', 'channel.daemon-worker'), [System.StringComparer]::Ordinal ) foreach ($legacy in $script:QwenSubSubcommands.auth) { $null = $script:QwenHiddenCommands.Add("auth.$legacy") } # ---- Flag categories ----------------------------------------------------------------------- # Global enum flags with fixed choice sets. $script:QwenEnumFlags = @{ '--telemetry-target' = @('local', 'gcp') '--telemetry-otlp-protocol' = @('grpc', 'http') # 'auto' precedes 'auto-edit' so Tab on a typed 'auto' keeps it. '--approval-mode' = @('plan', 'default', 'auto', 'auto-edit', 'yolo') '--channel' = @('VSCode', 'ACP', 'SDK', 'CI', 'desktop', 'daemon') '--input-format' = @('text', 'stream-json') '--output-format' = @('text', 'json', 'stream-json') '--auth-type' = @('openai', 'openai-responses', 'anthropic', 'qwen-oauth', 'gemini', 'vertex-ai') '--sandbox' = @('true', 'false', 'docker', 'podman', 'sandbox-exec') # [string], NOT a switch } # Boolean / switch flags (accept no value). $script:QwenBoolFlags = @( '--telemetry' '--telemetry-log-prompts' '--debug' '--bare' '--safe-mode' '--insecure' '--chat-recording' '--yolo' '--acp' '--experimental-lsp' '--restore-ask-user-question' '--openai-logging' '--screen-reader' '--include-partial-messages' '--list-extensions' '--continue' '--fork-session' '--help' '--version' ) # Free-form string flags (single value, no fixed choices). $script:QwenStringFlags = @( '--telemetry-otlp-endpoint' '--proxy' '--model' '--advisor' '--prompt' '--prompt-interactive' '--system-prompt' '--append-system-prompt' '--output-style' '--sandbox-image' '--openai-api-key' '--openai-base-url' '--json-schema' '--resume' '--session-id' '--worktree' '--max-wall-time' ) # Numeric flags. $script:QwenNumberFlags = @( '--max-session-turns' '--json-fd' '--max-tool-calls' '--max-subagent-depth' ) # Array / multi-value flags. $script:QwenArrayFlags = @( '--fallback-model' '--allowed-mcp-server-names' '--allowed-tools' '--extensions' '--include-directories' '--add-dir' # alias for --include-directories (help: --include-directories, --add-dir) '--core-tools' '--exclude-tools' '--disabled-slash-commands' ) # Flags that produce file-path completion. $script:QwenPathFlags = @( '--telemetry-outfile' '--mcp-config' '--json-file' '--input-file' ) # Flags that produce directory-path completion. $script:QwenDirFlags = @( '--openai-logging-dir' '--include-directories' '--add-dir' ) # ---- Context tables ------------------------------------------------------------------------ # A context is a command path: 'serve', 'mcp.add', 'sessions.controllers.add'. yargs # options are global, so a command's options also apply to its subcommands: lookups walk # from the deepest context up to its top-level command, and the first declaration wins # (it also shadows a global flag of the same name, e.g. 'review run --resume'). # Value-taking options declared by a command: context -> @(flags). $script:QwenContextFlags = @{ 'mcp.add' = @('--scope', '--transport', '--env', '--header', '--timeout', '--description', '--include-tools', '--exclude-tools', '--oauth-client-id', '--oauth-client-secret', '--oauth-redirect-uri', '--oauth-authorization-url', '--oauth-token-url', '--oauth-scopes') 'extensions.install' = @('--ref', '--registry', '--scope') 'extensions.disable' = @('--scope') 'extensions.enable' = @('--scope') 'extensions.settings.set' = @('--scope') 'channel.daemon-worker' = @('--channel') 'channel.reload' = @('--daemon-url', '--token', '--timeout') 'channel.set' = @('--daemon-url', '--token', '--timeout') 'board' = @('--board', '--as') 'board.task' = @('--owner') 'board.done' = @('--note') 'board.ask' = @('--about', '--timeout', '--ttl') 'board.prune' = @('--older-than') 'sessions.list' = @('--limit') 'sessions.controllers.add' = @('--label') 'batch.run' = @('--expect') 'batch.collect' = @('--timeout') 'batch.retry' = @('--max-output-tokens') 'sandbox' = @('--sandbox') 'serve' = @('--port', '--hostname', '--profile', '--hosted-harness-capability-digest', '--token', '--max-sessions', '--max-total-sessions', '--max-pending-prompts-per-session', '--workspace', '--memory-project-scope', '--max-connections', '--tls-cert', '--tls-key', '--channel', '--local-control-address', '--join', '--agent-host-server', '--agent-host-workspace-id', '--agent-host-name', '--event-ring-size', '--compacted-replay-max-bytes', '--max-journal-events', '--max-journal-bytes', '--memory-budget-mb', '--memory-pressure-mode', '--child-heap-mode', '--mcp-client-budget', '--mcp-budget-mode', '--allow-origin', '--prompt-deadline-ms', '--experimental-managed-runtime-url', '--experimental-managed-runtime-token', '--managed-runtime-broker-url', '--managed-runtime-broker-token', '--writer-idle-timeout-ms', '--channel-idle-timeout-ms', '--initialize-timeout-ms', '--session-restore-timeout-ms', '--session-reap-interval-ms', '--session-idle-timeout-ms', '--session-prompt-settled-close-grace-ms', '--permission-response-timeout-ms', '--external-tool-guard-mode', '--external-tool-guard-endpoint', '--external-tool-guard-timeout-ms', '--rate-limit-prompt', '--rate-limit-mutation', '--rate-limit-read', '--rate-limit-window-ms') 'review.ab-drive' = @('--script', '--arm-a', '--arm-b', '--ready', '--ready-timeout', '--timeout', '--shared', '--shared-ready', '--shared-ready-timeout', '--shared-cwd', '--server', '--out') 'review.agent-prompt' = @('--plan', '--role', '--chunk', '--file', '--rules', '--findings', '--hunks', '--round') 'review.base-tree' = @('--plan', '--worktree', '--out', '--timeout') 'review.build-test' = @('--plan', '--worktree', '--out', '--timeout', '--budget') 'review.cache-commit' = @('--candidate', '--ledger', '--out', '--state-id') 'review.capture-local' = @('--out', '--file', '--target', '--effort', '--deadline', '--cache') 'review.capture-tui' = @('--command', '--cwd', '--cols', '--rows', '--settle-ms', '--until', '--ready', '--keys', '--out', '--timeout-ms') 'review.check-coverage' = @('--plan', '--out') 'review.comment-body' = @('--kind', '--pr', '--repo', '--host', '--out') 'review.comment-status' = @('--out', '--host') 'review.compose-review' = @('--input', '--comments', '--out', '--host', '--pr', '--repo', '--skill-args') 'review.cost-ledger' = @('--plan', '--out') 'review.dedup-candidates' = @('--plan', '--candidates') 'review.drive' = @('--script', '--cwd', '--ready', '--ready-timeout', '--timeout', '--server', '--capture', '--out') 'review.emit-workflow' = @('--plan', '--rules', '--batch') 'review.extract-step' = @('--workflow', '--job', '--step', '--out') 'review.fetch-diff' = @('--repo', '--host', '--out') 'review.fetch-pr' = @('--remote', '--out', '--host', '--max-chunk-lines', '--effort', '--deadline', '--since', '--since-model') 'review.findings' = @('--input', '--out', '--outcomes', '--test-delta', '--to-anchors') 'review.fix-delta' = @('--since', '--out') 'review.issue-context' = @('--repo', '--host', '--issue', '--out') 'review.load-rules' = @('--out') 'review.match-remote' = @('--owner', '--repo', '--host', '--group-path') 'review.meta' = @('--repo', '--host') 'review.mock-provider' = @('--responder', '--log', '--ttl', '--out') 'review.parse-args' = @('--out') 'review.plan-diff' = @('--out', '--pr', '--repo', '--host', '--max-chunk-lines', '--effort', '--deadline') 'review.pr-context' = @('--out', '--host') 'review.presubmit' = @('--host', '--new-findings') 'review.publish-assets' = @('--pr', '--reviewed-repo', '--files', '--findings', '--findings-out', '--out', '--host', '--skill-args') 'review.recover-findings' = @('--plan', '--out') 'review.repo-context' = @('--plan', '--worktree', '--out') 'review.resolve-anchors' = @('--diff', '--input', '--out') 'review.revert-hunk' = @('--diff', '--hunk', '--tree', '--out') 'review.run' = @('--effort', '--fail-on', '--timeout-minutes', '--approval-mode') 'review.save-artifact' = @('--findings', '--composed', '--report', '--target', '--effort', '--out', '--workspace-root') 'review.scratch-tree' = @('--worktree', '--label', '--fetched-sha', '--out') 'review.script-lint' = @('--plan', '--worktree', '--out') 'review.submit' = @('--pr', '--repo', '--review', '--skill-args', '--host') 'review.test-delta' = @('--report', '--baseline', '--pr-worktree', '--out', '--timeout') 'review.test-efficacy' = @('--worktree', '--base', '--out') 'review.test-plan' = @('--plan', '--pr', '--repo', '--worktree', '--build-test', '--out', '--host') } # Boolean options declared by a command (take no value): context -> @(flags). $script:QwenContextBoolFlags = @{ 'mcp.add' = @('--trust') 'mcp.reconnect' = @('--all') 'mcp.approve' = @('--all') 'mcp.reject' = @('--all') 'extensions.install' = @('--auto-update', '--pre-release', '--consent') 'extensions.update' = @('--all') 'board' = @('--json') 'board.ask' = @('--wait') 'sessions.list' = @('--json') 'sessions.ps' = @('--json') 'sessions.controllers.add' = @('--json') 'sessions.controllers.list' = @('--json') 'batch.run' = @('--dry-run') 'batch.collect' = @('--wait') 'batch.clean' = @('--force') 'sandbox' = @('--verify') 'serve' = @('--require-auth', '--enable-session-shell', '--experimental-lsp', '--restore-ask-user-question', '--web', '--open', '--open-with-auth', '--token-qr', '--local-control', '--agent-host-allow-http', '--http-bridge', '--allow-private-auth-base-url', '--experimental-paired-engines', '--experimental-managed-agents', '--experimental-managed-runtime-worker', '--experimental-managed-runtime-auto-local', '--rate-limit') 'review.ab-drive' = @('--shared-once') 'review.agent-prompt' = @('--all-chunks', '--roster', '--batch', '--whole-diff') 'review.base-tree' = @('--install') 'review.build-test' = @('--install', '--build-only', '--resume') 'review.capture-local' = @('--untracked') 'review.fetch-pr' = @('--resume') 'review.findings' = @('--print') 'review.fix-delta' = @('--snapshot') 'review.parse-args' = @('--stdin') 'review.publish-assets' = @('--user-authorized') 'review.revert-hunk' = @('--list') 'review.run' = @('--comment', '--resume', '--json', '--quiet') 'review.scratch-tree' = @('--standalone') 'review.submit' = @('--user-authorized', '--dry-run') } # Context-specific enum values: "context.--flag" -> @(choices). $script:QwenContextEnumFlags = @{ 'mcp.add.--scope' = @('user', 'project') 'mcp.add.--transport' = @('stdio', 'sse', 'http') 'extensions.install.--scope' = @('user', 'project', 'workspace') 'extensions.settings.set.--scope' = @('user', 'workspace') 'sandbox.--sandbox' = @('true', 'false', 'docker', 'podman', 'sandbox-exec') 'serve.--profile' = @('default', 'hosted-harness') 'serve.--memory-project-scope' = @('git-root', 'workspace') 'serve.--memory-pressure-mode' = @('off', 'observe') 'serve.--child-heap-mode' = @('off', 'observe', 'admit', 'enforce') 'serve.--mcp-budget-mode' = @('enforce', 'warn', 'off') 'serve.--external-tool-guard-mode' = @('off', 'required') 'review.run.--effort' = @('low', 'medium', 'high') 'review.run.--fail-on' = @('none', 'request-changes') 'review.run.--approval-mode' = @('plan', 'default', 'auto', 'auto-edit', 'yolo') 'review.capture-local.--effort' = @('low', 'medium', 'high') 'review.fetch-pr.--effort' = @('low', 'medium', 'high') 'review.plan-diff.--effort' = @('low', 'medium', 'high') 'review.save-artifact.--effort' = @('medium', 'high') 'review.comment-body.--kind' = @('review', 'inline', 'issue') 'review.agent-prompt.--role' = @('docs-nav', '0', '1a', '1b', '1c', '1d', '1e', '2', '3a', '3b', '3c', '4', '5', '6a', '6b', '6c', '6d', '7', 'prose-exec', 'test-matrix', 'invariant-a', 'invariant-b', 'invariant-c', 'verify', 'reverse-audit', 'fix-audit') } # Subcommand paths (L2) whose first positional argument accepts path completion. $script:QwenPathPositionalL2 = [System.Collections.Generic.HashSet[string]]::new( [string[]]@('extensions.link', 'extensions.new', 'extensions.install', 'batch.run', 'review.plan-diff', 'review.test-efficacy'), [System.StringComparer]::Ordinal ) # Positional enum values for specific subcommand paths. $script:QwenPositionalEnums = @{ 'channel.configure-weixin' = @('clear') 'channel.set' = @('all') } # Placeholder values for positional slots that are free-form and should not # fall back to noisy filesystem completion. $script:QwenPositionalPlaceholders = @{ 'mcp.add' = @('<name>', '<commandOrUrl>', '<arg>') 'mcp.remove' = @('<name>') 'mcp.reconnect' = @('<server-name>') 'mcp.approve' = @('<name>') 'mcp.reject' = @('<name>') 'extensions.install' = @('<source>') 'extensions.uninstall' = @('<name>') 'extensions.update' = @('<name>') 'extensions.disable' = @('<name>') 'extensions.enable' = @('<name>') 'extensions.new' = @('<path>', '<template>') 'extensions.settings.set' = @('<name>', '<setting>') 'extensions.settings.list' = @('<name>') 'extensions.sources.add' = @('<source>') 'extensions.sources.remove' = @('<name>') 'extensions.sources.update' = @('<name>') 'channel.start' = @('<name>') 'channel.set' = @('<name>') 'channel.pairing.list' = @('<name>') 'channel.pairing.approve' = @('<name>', '<code>') 'board.task' = @('<subject>') 'board.claim' = @('<id>') 'board.done' = @('<id>') 'board.ask' = @('<to>', '<question>') 'board.answer' = @('<id>', '<answer>') 'board.decline' = @('<id>', '<reason>') 'sessions.controllers.remove' = @('<id>') 'batch.collect' = @('<task-id>') 'batch.retry' = @('<task-id>') 'batch.cancel' = @('<task-id>') 'batch.clean' = @('<task-id>') 'sandbox' = @('<cmd>') 'review.run' = @('<target>') 'review.parse-args' = @('<raw>') 'review.meta' = @('<pr_number>') 'review.issue-context' = @('<pr_number>') 'review.fetch-diff' = @('<pr_number>') 'review.comment-body' = @('<id>') 'review.fetch-pr' = @('<pr_number>', '<owner_repo>') 'review.pr-context' = @('<pr_number>', '<owner_repo>') 'review.comment-status' = @('<pr_number>', '<owner_repo>') 'review.load-rules' = @('<base_ref>') 'review.presubmit' = @('<pr_number>', '<commit_sha>', '<owner_repo>', '<out_path>') 'review.cleanup' = @('<target>') } # ---- Short alias maps (ordinal: '-H' and '-h' are different flags) -------------------------- $script:QwenShortAliases = ConvertTo-QwenOrdinalMap @{ '-d' = '--debug' '-m' = '--model' '-p' = '--prompt' '-i' = '--prompt-interactive' '-s' = '--sandbox' '-y' = '--yolo' '-e' = '--extensions' '-l' = '--list-extensions' '-o' = '--output-format' '-c' = '--continue' '-r' = '--resume' '-v' = '--version' '-h' = '--help' } # --sandbox (-s) is declared by the default command ('$0') builder and by 'sandbox' # only: inside 'mcp', 'extensions', ... neither form means --sandbox. $script:QwenRootOnlyFlags = [System.Collections.Generic.HashSet[string]]::new( [string[]]@('--sandbox'), [System.StringComparer]::Ordinal ) $script:QwenContextShortAliases = @{ 'mcp.add' = ConvertTo-QwenOrdinalMap @{ '-s' = '--scope' '-t' = '--transport' '-e' = '--env' '-H' = '--header' } 'mcp.reconnect' = ConvertTo-QwenOrdinalMap @{ '-a' = '--all' } 'sandbox' = ConvertTo-QwenOrdinalMap @{ '-s' = '--sandbox' } } # Reverse map (long -> short) built once at load time. $script:QwenAliasReverse = @{} $script:QwenShortAliases.GetEnumerator() | ForEach-Object { $script:QwenAliasReverse[$_.Value] = $_.Key } $script:QwenContextAliasReverse = @{} foreach ($entry in $script:QwenContextShortAliases.GetEnumerator()) { $reverse = @{} foreach ($alias in $entry.Value.GetEnumerator()) { $reverse[$alias.Value] = $alias.Key } $script:QwenContextAliasReverse[$entry.Key] = $reverse } # ---- Descriptions -------------------------------------------------------------------------- $script:QwenCmdDesc = @{ mcp = 'Manage MCP servers' extensions = 'Manage extensions' hooks = 'Manage hooks (use /hooks in interactive mode)' hook = 'Alias for hooks' channel = 'Manage messaging channels (Telegram, Discord, etc.)' board = 'Share work with other agents through a board' serve = 'Run Qwen Code as a local HTTP daemon' sessions = 'Manage Qwen Code sessions' batch = 'Run many independent requests through the DashScope Batch API' update = 'Check for Qwen Code updates and install if available' sandbox = 'Inspect tool confinement, verify it, or run one confined command' review = 'Run a review non-interactively (run), plus the /review skill helpers' } $script:QwenSubcmdDesc = @{ 'mcp.add' = 'Add an MCP server' 'mcp.remove' = 'Remove an MCP server' 'mcp.list' = 'List configured MCP servers' 'mcp.reconnect' = 'Reconnect MCP server(s)' 'mcp.approve' = 'Approve a gated MCP server (.mcp.json or workspace settings)' 'mcp.reject' = 'Reject a gated MCP server (.mcp.json or workspace settings)' 'extensions.install' = 'Install an extension (URL, local path, or npm package)' 'extensions.uninstall' = 'Uninstall an extension' 'extensions.list' = 'List installed extensions' 'extensions.update' = 'Update extensions' 'extensions.disable' = 'Disable an extension' 'extensions.enable' = 'Enable an extension' 'extensions.link' = 'Link a local development extension (live)' 'extensions.new' = 'Scaffold a new extension from boilerplate' 'extensions.settings' = 'Manage extension settings' 'extensions.sources' = 'Manage marketplace sources for discovering extensions' 'extensions.settings.set' = 'Set a specific extension setting' 'extensions.settings.list' = 'List all settings for an extension' 'extensions.sources.add' = 'Add a marketplace source (Claude format)' 'extensions.sources.remove' = 'Remove a marketplace source' 'extensions.sources.list' = 'List configured marketplace sources' 'extensions.sources.update' = 'Re-fetch a marketplace source and its plugin listing' 'channel.start' = 'Start channels (all or named)' 'channel.stop' = 'Stop the channel service' 'channel.status' = 'Show channel service status' 'channel.reload' = 'Reload the daemon-managed channel worker (re-reads settings.json)' 'channel.set' = 'Set the channel selection for a running qwen serve daemon' 'channel.pairing' = 'Manage DM pairing requests' 'channel.configure-weixin' = 'Configure WeChat channel (login or "clear")' 'channel.pairing.list' = 'List pending pairing requests for a channel' 'channel.pairing.approve' = 'Approve a pending pairing request' 'board.show' = 'Print the board once' 'board.task' = 'Create a task' 'board.claim' = 'Take ownership of a task' 'board.done' = 'Complete a task you own' 'board.ask' = 'Ask another actor a question' 'board.answer' = 'Answer an ask addressed to you' 'board.decline' = 'Decline an ask addressed to you' 'board.prune' = 'Remove settled items older than a cutoff' 'sessions.list' = 'List sessions' 'sessions.ps' = 'List registered and managed Qwen Code sessions' 'sessions.controllers' = 'Manage the controller tokens that may drive your sessions' 'sessions.controllers.add' = 'Mint a controller token and print it once' 'sessions.controllers.list' = 'List the controllers this Qwen home trusts' 'sessions.controllers.remove' = 'Revoke a controller by id' 'batch.run' = 'Run an agent-prepared batch plan: assemble, submit, record the task' 'batch.collect' = 'Collect a workflow task: reconcile, download, validate, deliver' 'batch.retry' = 'Resubmit only the failed items of a workflow task' 'batch.cancel' = "Cancel a workflow task's active batch" 'batch.list' = 'List all recorded workflow tasks with their project' 'batch.check' = 'Verify credentials and the Batch route (no billed request)' 'batch.clean' = "Delete a workflow task's local record" 'review.run' = 'Run a full /review non-interactively and print the verdict' 'review.parse-args' = 'Parse the /review skill argument string' 'review.match-remote' = 'Print the git remote whose URL matches an owner/repo' 'review.meta' = 'Print the review platform identity facts for this repository' 'review.issue-context' = "Fetch a PR's closing issues and render them as context" 'review.fetch-diff' = "Write a PR's full unified diff to a file" 'review.comment-body' = 'Print one comment body' 'review.fetch-pr' = 'Prepare a PR review worktree and write a review plan' 'review.capture-local' = 'Capture local changes as one diff and partition it into chunks' 'review.capture-tui' = 'Run a command in a private tmux server and capture what it rendered' 'review.plan-diff' = 'Partition a captured diff file into review chunks (JSON plan)' 'review.cache-commit' = "Promote a capture's cache candidate into .qwen/review-cache/" 'review.repo-context' = 'Attach bounded repository-specific context to a review plan' 'review.pr-context' = 'Fetch PR metadata and comments as a Markdown context file' 'review.comment-status' = "Emit a per-thread status index for a PR's inline comments" 'review.load-rules' = 'Read project review rules from the base branch' 'review.agent-prompt' = "Build a review agent's launch prompt from the plan" 'review.emit-workflow' = 'Emit a review roster or batch as a runnable workflow script' 'review.build-test' = 'Build and test the workspaces the diff changes' 'review.base-tree' = "Build the PR's merge base in a sibling worktree" 'review.scratch-tree' = 'Create a throwaway worktree at the commit under review' 'review.test-delta' = "Rerun the PR side's failed tests on the base tree" 'review.fix-delta' = 'Snapshot the tree before --fix, then diff it afterwards' 'review.drive' = 'Start something, wait until it is up, drive it, capture it' 'review.ab-drive' = 'Drive the same script against the PR and base trees' 'review.mock-provider' = 'Serve mock OpenAI and Anthropic endpoints for the review' 'review.extract-step' = "Extract one workflow step's run: script into an executable" 'review.script-lint' = 'Run shellcheck/actionlint/hadolint over changed scripts' 'review.dedup-candidates' = 'Drop pooled candidates the carried ledger already holds' 'review.revert-hunk' = 'List the diff hunks, or apply one in reverse in a tree' 'review.resolve-anchors' = "Compute each finding's diff line from its quoted snippet" 'review.check-coverage' = "Prove the diff was read, from the harness's transcripts" 'review.cost-ledger' = "Aggregate this review's model-call cost" 'review.presubmit' = 'Pre-submission checks for /review Step 7' 'review.test-efficacy' = "Check whether the diff's new tests gate its new behaviour" 'review.test-plan' = "Rule on the PR Test Plan's checkable claims" 'review.findings' = "Validate and canonicalize the review's findings" 'review.recover-findings' = 'Recover the certified agent results of an interrupted review' 'review.publish-assets' = 'Publish review evidence images to the assets repository' 'review.compose-review' = 'Compute the review event and body from the drafted comments' 'review.save-artifact' = 'Create a durable versioned code-review JSON document' 'review.submit' = 'Post the review to the pull request' 'review.cleanup' = 'Post-review cleanup: remove worktree, branch ref, temp files' } # Global flag descriptions (by flag name). $script:QwenFlagDesc = @{ '--telemetry' = '[boolean] Enable telemetry (deprecated: use settings.json)' '--telemetry-target' = '[string] Telemetry target: local|gcp (deprecated)' '--telemetry-otlp-endpoint' = '[string] OTLP collector endpoint URL (deprecated)' '--telemetry-otlp-protocol' = '[string] OTLP protocol: grpc|http (deprecated)' '--telemetry-log-prompts' = '[boolean] Log prompts for telemetry (deprecated)' '--telemetry-outfile' = '[path] Write telemetry to file (deprecated)' '--debug' = '[boolean] Run in debug mode' '--bare' = '[boolean] Minimal mode: skip startup auto-discovery' '--safe-mode' = '[boolean] Disable all customizations for troubleshooting' '--insecure' = '[boolean] Skip TLS certificate verification for API connections' '--proxy' = '[string] HTTP proxy URL (deprecated)' '--chat-recording' = '[boolean] Enable chat recording to disk' '--model' = '[string] Model to use' '--advisor' = '[string] Advisor model selector ("off" disables)' '--fallback-model' = '[array] Fallback model(s) for capacity errors (max 3)' '--prompt' = '[string] Prompt text (deprecated; use positional)' '--prompt-interactive' = '[string] Prompt and continue in interactive mode' '--system-prompt' = '[string] Override session system prompt' '--append-system-prompt' = '[string] Append to session system prompt' '--output-style' = '[string] Output style for this run (e.g. Concise, Explanatory)' '--sandbox' = '[string] Run in a sandbox: true|false|docker|podman|sandbox-exec' '--sandbox-image' = '[string] Sandbox container image URI (deprecated)' '--yolo' = '[boolean] Auto-accept all actions (YOLO mode)' '--approval-mode' = '[string] Tool approval mode: plan|default|auto|auto-edit|yolo' '--acp' = '[boolean] Start in ACP mode' '--experimental-lsp' = '[boolean] Enable experimental LSP support' '--restore-ask-user-question' = '[boolean] Re-hang an unanswered ask_user_question on daemon resume' '--channel' = '[string] Channel identifier: VSCode|ACP|SDK|CI|desktop|daemon' '--allowed-mcp-server-names' = '[array] Allowed MCP server names' '--mcp-config' = '[path] MCP server config: JSON file path or inline JSON' '--allowed-tools' = '[array] Tools to allow (bypass confirmation)' '--extensions' = '[array] Extensions to use (-e)' '--list-extensions' = '[boolean] List available extensions and exit' '--include-directories' = '[array] Additional workspace directories (--add-dir)' '--add-dir' = '[array] Additional workspace directories (--include-directories)' '--openai-logging' = '[boolean] Enable OpenAI API call logging' '--openai-logging-dir' = '[path] Directory for OpenAI API logs' '--openai-api-key' = '[string] OpenAI API key' '--openai-base-url' = '[string] OpenAI base URL override' '--screen-reader' = '[boolean] Enable screen reader accessibility mode' '--input-format' = '[string] Input format: text|stream-json' '--output-format' = '[string] Output format: text|json|stream-json' '--include-partial-messages' = '[boolean] Include partial assistant messages (stream-json)' '--json-fd' = '[number] File descriptor for structured JSON event output' '--json-file' = '[path] File for structured JSON event output' '--json-schema' = '[string] JSON Schema for the final output (JSON or @path)' '--input-file' = '[path] File for receiving remote input commands (JSONL)' '--continue' = '[boolean] Resume the most recent session' '--resume' = '[string] Resume a specific session by ID' '--session-id' = '[string] Specify session ID for this run' '--fork-session' = '[boolean] Fork a new session from the resumed one' '--worktree' = '[string] Start the session inside a git worktree (slug or PR)' '--max-session-turns' = '[number] Maximum session turns' '--max-wall-time' = '[string] Run-level wall-clock budget (e.g. 90, 30s, 5m, 1h)' '--max-tool-calls' = '[number] Maximum cumulative tool calls for the run' '--max-subagent-depth' = '[number] Maximum sub-agent nesting depth' '--core-tools' = '[array] Core tool paths' '--exclude-tools' = '[array] Tools to exclude' '--disabled-slash-commands' = '[array] Slash command names to hide/disable' '--auth-type' = '[string] Authentication type' '--help' = '[boolean] Show help' '--version' = '[boolean] Show version' } # Context flag descriptions: "context.--flag" -> text, found through the same # deepest-first walk as the flags ('mcp.--all' covers every mcp subcommand). A # context flag without an entry is described by its kind ([boolean], choices, [value]). $script:QwenContextFlagDesc = @{ 'mcp.--scope' = '[string] Configuration scope: user|project' 'mcp.--transport' = '[string] MCP transport: stdio|sse|http' 'mcp.--env' = '[array] Environment variable: KEY=value' 'mcp.--header' = '[array] HTTP header: "Name: value"' 'mcp.--timeout' = '[number] Connection timeout in milliseconds' 'mcp.--trust' = '[boolean] Trust server (bypass all tool confirmations)' 'mcp.--description' = '[string] Server description' 'mcp.--include-tools' = '[array] Tools to include (comma-separated)' 'mcp.--exclude-tools' = '[array] Tools to exclude (comma-separated)' 'mcp.--oauth-client-id' = '[string] OAuth client ID for MCP server authentication' 'mcp.--oauth-client-secret' = '[string] OAuth client secret for MCP server authentication' 'mcp.--oauth-redirect-uri' = '[string] OAuth redirect URI' 'mcp.--oauth-authorization-url' = '[string] OAuth authorization URL' 'mcp.--oauth-token-url' = '[string] OAuth token URL' 'mcp.--oauth-scopes' = '[array] OAuth scopes (comma-separated)' 'mcp.--all' = '[boolean] Apply to all servers' 'extensions.--scope' = '[string] Configuration scope' 'extensions.--ref' = '[string] Git ref to install from' 'extensions.--auto-update' = '[boolean] Enable auto-update for this extension' 'extensions.--pre-release' = '[boolean] Include pre-release versions' 'extensions.--registry' = '[string] Custom npm registry URL' 'extensions.--consent' = '[boolean] Acknowledge risks and skip confirmation' 'extensions.--all' = '[boolean] Update all extensions' 'channel.--daemon-url' = '[string] Daemon base URL (default: $QWEN_DAEMON_URL)' 'channel.--token' = '[string] Bearer token (default: $QWEN_SERVER_TOKEN)' 'channel.--timeout' = '[number] Request timeout in milliseconds' 'channel.--channel' = '[array] Internal daemon-managed channel selection' 'board.--board' = '[string] Board name' 'board.--as' = '[string] Declared actor name' 'board.--json' = '[boolean] Emit JSON' 'board.--owner' = '[string] Task owner' 'board.--note' = '[string] Completion note' 'board.--about' = '[string] What the question is about' 'board.--wait' = '[boolean] Wait for the answer' 'board.--timeout' = '[number] Seconds to wait for the answer (default 30)' 'board.--ttl' = '[number] Seconds the ask stays open (default 900)' 'board.--older-than' = '[number] Cutoff in days (default 7)' 'sessions.--json' = '[boolean] Output as JSON' 'sessions.--limit' = '[number] Maximum number of sessions to show (default 20)' 'sessions.--label' = '[string] What to call this controller, for later revocation' 'batch.--dry-run' = '[boolean] Assemble and show items without uploading' 'batch.--expect' = '[string] Submit only if the batch matches this --dry-run digest' 'batch.--wait' = '[boolean] Poll until the batch settles, then collect' 'batch.--timeout' = '[number] Seconds to wait with --wait before giving up' 'batch.--max-output-tokens' = '[number] Output limit for the new attempt' 'batch.--force' = '[boolean] Delete even if a batch may still be running' 'sandbox.--verify' = '[boolean] Verify the kernel boundary' 'sandbox.--sandbox' = '[string] Sandbox to inspect: true|false|docker|podman|sandbox-exec' 'serve.--port' = '[number] TCP port to bind (default 4170; 0 = ephemeral)' 'serve.--hostname' = '[string] Interface to bind (non-loopback requires a token)' 'serve.--token' = '[string] Bearer token required on every request' 'serve.--workspace' = '[array] Absolute workspace path to register (repeatable)' 'serve.--channel' = '[array] Start a daemon-managed channel worker (or "all")' 'serve.--experimental-lsp' = '[boolean] Forward the experimental LSP opt-in to sessions' 'serve.--restore-ask-user-question' = '[boolean] Re-hang an unanswered ask_user_question on resume' 'serve.--web' = '[boolean] Serve the Web Shell UI (default true; --no-web)' 'serve.--open' = '[boolean] Open the Web Shell in a browser once listening' 'serve.--require-auth' = '[boolean] Refuse to start without a bearer token' 'review.--resume' = '[boolean] Continue an interrupted review' 'review.--worktree' = '[string] Review worktree path' 'review.--timeout' = '[number] Timeout' 'review.--effort' = '[string] Review effort: low|medium|high' 'review.run.--comment' = '[boolean] Authorise posting the review to GitHub (PR targets)' 'review.run.--json' = '[boolean] Print the full result as JSON on stdout' 'review.run.--fail-on' = '[string] Exit 3 on this outcome: none|request-changes' 'review.run.--timeout-minutes' = '[number] Terminate the review after this long (default 120)' 'review.run.--approval-mode' = '[string] Approval mode for the child CLI (default yolo)' 'review.run.--quiet' = '[boolean] Suppress the child CLI progress stream on stderr' } } #region -- Helpers ------------------------------------------------------------------------------ function ConvertTo-QwenOrdinalMap { param([hashtable]$Map) $ordinal = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::Ordinal) foreach ($entry in $Map.GetEnumerator()) { $ordinal[$entry.Key] = $entry.Value } Write-Output -InputObject $ordinal -NoEnumerate } function New-QwenCompletion { param( [Parameter(Mandatory)] [string]$CompletionText, [string]$ListItemText = '', [string]$Tooltip = '', [System.Management.Automation.CompletionResultType] $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue ) if ([string]::IsNullOrEmpty($ListItemText)) { $ListItemText = $CompletionText } if ([string]::IsNullOrEmpty($Tooltip)) { $Tooltip = $ListItemText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $ListItemText, $ResultType, $Tooltip ) } # The typed word under the cursor. PowerShell hands a quoted word over re-quoted # ('ti arrives as 'ti') and drops the quote inside an attached --flag='ti, so the # word is read from the element the cursor is in: all of it, because PowerShell # replaces the whole element, including any text after the cursor. function Get-QwenCursorWord { param($CommandAst, [int]$CursorPosition) foreach ($element in ($CommandAst.CommandElements | Select-Object -Skip 1)) { $extent = $element.Extent if ($extent.StartOffset -lt $CursorPosition -and $CursorPosition -le $extent.EndOffset) { return $extent.Text } } '' } # The value of a typed word. A word opened with a quote (ASCII or typographic) is # read by the PowerShell tokenizer, which drops the quotes and undoes that quote # style's escapes. function ConvertFrom-QwenTypedWord { param([string]$Value) if ($Value -notmatch '^[''"\u2018-\u201E]') { return $Value } $tokens = $null $parseErrors = $null [void][System.Management.Automation.Language.Parser]::ParseInput($Value, [ref]$tokens, [ref]$parseErrors) $tokens[0].Value } # Renders a value as one PowerShell argument: bare when safe and no quote was typed, # otherwise in the quote the user typed (single by default). PowerShell reads ' and # U+2018-U+201B as single quotes and " and U+201C-U+201E as double quotes. function ConvertTo-QwenQuotedValue { param([string]$Value, [string]$QuoteChar = '') if ([string]::IsNullOrEmpty($Value)) { return $Value } if (-not $QuoteChar) { if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]') { return $Value } $QuoteChar = "'" } if ($QuoteChar -match '^[''\u2018-\u201B]$') { return $QuoteChar + ($Value -replace '([''\u2018-\u201B])', '$1$1') + $QuoteChar } $QuoteChar + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + $QuoteChar } # File-system candidates for a typed word, quoted once in the style the user typed. # CompleteFilename quotes and wildcard-escapes its results (tick``x.txt), so each is # unwrapped by the parser and unescaped first. CompleteFilename strips a leading quote # from its input, so a value that starts with one is handed over single-quoted. A # typed ./ or ../ keeps its separator. $Prefix is an attached '--flag=' part. function Get-QwenPathCompletions { param([string]$TypedWord, [string]$Prefix = '') $quoteChar = if ($TypedWord -match '^[''"\u2018-\u201E]') { $TypedWord.Substring(0, 1) } else { '' } $value = ConvertFrom-QwenTypedWord -Value $TypedWord $search = if ($value -match '^[''"\u2018-\u201E]') { "'" + ($value -replace '([''\u2018-\u201B])', '$1$1') } else { $value } foreach ($item in [System.Management.Automation.CompletionCompleters]::CompleteFilename($search)) { $path = $item.CompletionText if ($path -match '^[''"\u2018-\u201E]') { $ast = [System.Management.Automation.Language.Parser]::ParseInput($path, [ref]$null, [ref]$null) $constant = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true) if ($constant) { $path = $constant.Value } } $path = [System.Management.Automation.WildcardPattern]::Unescape($path) if ($value -match '^\.{1,2}/') { $path = $path -replace '^(\.{1,2})\\', '$1/' } New-QwenCompletion ($Prefix + (ConvertTo-QwenQuotedValue -Value $path -QuoteChar $quoteChar)) ` -ListItemText $item.ListItemText ` -ResultType $item.ResultType ` -Tooltip $item.ToolTip } } # Context keys for the current command path, deepest first ('board.ask', 'board'); # empty at the root. function Get-QwenContextChain { param([string]$Sub, [string]$SubSub, [string]$SubSubSub) if (-not $Sub) { return } if ($SubSub) { if ($SubSubSub) { "$Sub.$SubSub.$SubSubSub" } "$Sub.$SubSub" } $Sub } # Returns 'bool' or 'value' when a context declares the flag, else $null. function Get-QwenContextFlagKind { param([string]$FlagName, [string[]]$ContextKeys) foreach ($key in $ContextKeys) { if ($script:QwenContextBoolFlags[$key] -ccontains $FlagName) { return 'bool' } if ($script:QwenContextFlags[$key] -ccontains $FlagName) { return 'value' } } return $null } function Resolve-QwenFlagName { param([string]$FlagName, [string[]]$ContextKeys) foreach ($key in $ContextKeys) { $ctxMap = $script:QwenContextShortAliases[$key] if ($ctxMap -and $ctxMap.ContainsKey($FlagName)) { return $ctxMap[$FlagName] } } $long = $script:QwenShortAliases[$FlagName] if (-not $long -or ($ContextKeys.Count -gt 0 -and $script:QwenRootOnlyFlags.Contains($long))) { return $FlagName } $long } function Get-QwenShortAlias { param([string]$FlagName, [string[]]$ContextKeys) foreach ($key in $ContextKeys) { $ctxReverse = $script:QwenContextAliasReverse[$key] if ($ctxReverse -and $ctxReverse.ContainsKey($FlagName)) { return $ctxReverse[$FlagName] } } return $script:QwenAliasReverse[$FlagName] } # Returns $true when the flag consumes the next token as its value. function Test-QwenFlagTakesValue { param([string]$FlagName, [string[]]$ContextKeys) $r = Resolve-QwenFlagName -FlagName $FlagName -ContextKeys $ContextKeys # A command's own declaration wins over a global flag of the same name. $kind = Get-QwenContextFlagKind -FlagName $r -ContextKeys $ContextKeys if ($kind) { return $kind -eq 'value' } if ($ContextKeys.Count -gt 0 -and $script:QwenRootOnlyFlags.Contains($r)) { return $false } # Global value-taking flag categories. if ($null -ne $script:QwenEnumFlags[$r]) { return $true } if ($script:QwenStringFlags -contains $r) { return $true } if ($script:QwenNumberFlags -contains $r) { return $true } if ($script:QwenArrayFlags -contains $r) { return $true } if ($script:QwenPathFlags -contains $r) { return $true } if ($script:QwenDirFlags -contains $r) { return $true } # Global boolean flags and unknown flags take no value. return $false } # Returns the known enum choices for a flag in the given context, or $null. function Get-QwenEnumValues { param([string]$FlagName, [string[]]$ContextKeys) $r = Resolve-QwenFlagName -FlagName $FlagName -ContextKeys $ContextKeys foreach ($key in $ContextKeys) { $vals = $script:QwenContextEnumFlags["$key.$r"] if ($vals) { return $vals } # Declared here without choices: a global enum of the same name does not apply. if (Get-QwenContextFlagKind -FlagName $r -ContextKeys $key) { return $null } } if ($ContextKeys.Count -gt 0 -and $script:QwenRootOnlyFlags.Contains($r)) { return $null } return $script:QwenEnumFlags[$r] } function Get-QwenFlagDescription { param([string]$FlagName, [string[]]$ContextKeys) $kind = Get-QwenContextFlagKind -FlagName $FlagName -ContextKeys $ContextKeys if (-not $kind) { return $script:QwenFlagDesc[$FlagName] ?? '' } foreach ($key in $ContextKeys) { $desc = $script:QwenContextFlagDesc["$key.$FlagName"] if ($desc) { return $desc } } if ($kind -eq 'bool') { return '[boolean]' } $choices = Get-QwenEnumValues -FlagName $FlagName -ContextKeys $ContextKeys if ($choices) { return '[string] ' + ($choices -join '|') } return '[value]' } # Builds the full flag set for the current command context. function Get-QwenFlagSet { param([string[]]$ContextKeys) $set = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::Ordinal ) $script:QwenEnumFlags.Keys | ForEach-Object { $null = $set.Add($_) } $script:QwenBoolFlags | ForEach-Object { $null = $set.Add($_) } $script:QwenStringFlags | ForEach-Object { $null = $set.Add($_) } $script:QwenNumberFlags | ForEach-Object { $null = $set.Add($_) } $script:QwenArrayFlags | ForEach-Object { $null = $set.Add($_) } $script:QwenPathFlags | ForEach-Object { $null = $set.Add($_) } $script:QwenDirFlags | ForEach-Object { $null = $set.Add($_) } if ($ContextKeys.Count -gt 0) { $set.ExceptWith($script:QwenRootOnlyFlags) } foreach ($key in $ContextKeys) { foreach ($table in @($script:QwenContextFlags, $script:QwenContextBoolFlags)) { $ctxFlags = $table[$key] if ($ctxFlags) { $ctxFlags | ForEach-Object { $null = $set.Add($_) } } } } return $set } function Get-QwenPositionalPlaceholder { param( [string]$ContextKey, [int]$PositionIndex ) $placeholders = $script:QwenPositionalPlaceholders[$ContextKey] if (-not $placeholders) { return $null } if ($PositionIndex -lt $placeholders.Count) { return $placeholders[$PositionIndex] } return $placeholders[-1] } function Write-QwenLongFlagResults { param( [string]$WordToComplete, [string[]]$ContextKeys ) $flagSet = Get-QwenFlagSet -ContextKeys $ContextKeys foreach ($flag in $flagSet) { if ($flag -notlike "$WordToComplete*") { continue } $desc = Get-QwenFlagDescription -FlagName $flag -ContextKeys $ContextKeys $short = Get-QwenShortAlias -FlagName $flag -ContextKeys $ContextKeys $tip = if ($short) { "$flag (alias: $short): $desc" } else { "${flag}: $desc" } New-QwenCompletion $flag -ResultType ParameterName -Tooltip $tip } } #endregion #region -- Completer scriptblock ---------------------------------------------------------------- function Complete-QwenNative { param( [string]$WordToComplete, $CommandAst, [int]$CursorPosition ) Initialize-QwenCompleterData if ($null -eq $CommandAst) { return } $allElements = @($CommandAst.CommandElements) if ($allElements.Count -eq 0) { return } # ------------------------------------------------------------------------- # The word under the cursor is the engine's $WordToComplete (empty in an # empty slot). Committed arguments are the elements that end before the # cursor; extents and the cursor are both absolute offsets in the input, # so this holds when the command follows another statement. A string # constant contributes its unquoted value, as the shell passes "mcp" to # qwen as mcp; every other element (--flag parameters included) keeps its # Extent.Text, which is safe under StrictMode. # ------------------------------------------------------------------------- $committedArgs = @(foreach ($el in ($allElements | Select-Object -Skip 1)) { if ($el.Extent.EndOffset -lt $CursorPosition) { if ($el -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $el.Value } else { $el.Extent.Text } } }) # ------------------------------------------------------------------------- # State machine: walk committed args to determine context. # Tracks up to 3 command levels plus expectingValue / currentFlag. # ------------------------------------------------------------------------- $sub = $null # e.g. 'mcp' $subsub = $null # e.g. 'add' $sub3 = $null # e.g. 'set' (extensions settings set / sessions controllers add) $ctxKeys = [string[]]@() $expectingValue = $false $currentFlag = $null $positionalCount = 0 # non-flag positionals seen after the deepest command foreach ($token in $committedArgs) { # yargs stops parsing at '--': everything after it is passed through. if ($token -ceq '--') { return } if ($expectingValue) { $expectingValue = $false $currentFlag = $null # yargs never takes a dash-led word other than a negative number as # a value, so in '--sandbox -p' the -p is a flag of its own. if ($token -notlike '-*' -or $token -match '^-\d') { continue } } if ($token -like '-*') { if ($token -like '*=*') { continue } # inline --flag=value; value already consumed if (Test-QwenFlagTakesValue -FlagName $token -ContextKeys $ctxKeys) { $expectingValue = $true $currentFlag = Resolve-QwenFlagName -FlagName $token -ContextKeys $ctxKeys } continue } # Positional token. A command name only counts before the current # level's first positional: in 'qwen fix mcp' the 'mcp' is prompt text. $matched = $false if ($positionalCount -eq 0) { if ($null -eq $sub) { if ($script:QwenTopCommands -ccontains $token) { $sub = $token $matched = $true } } elseif ($null -eq $subsub) { if ($script:QwenSubSubcommands[$sub] -ccontains $token) { $subsub = $token $matched = $true } } elseif ($null -eq $sub3) { if ($script:QwenL3Subcommands["$sub.$subsub"] -ccontains $token) { $sub3 = $token $matched = $true } } } if ($matched) { $ctxKeys = [string[]]@(Get-QwenContextChain -Sub $sub -SubSub $subsub -SubSubSub $sub3) } else { $positionalCount++ } } $isWordPrefix = { param($candidate) $candidate -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } # ========================================================================= # 1. Inline --flag=value completion # ========================================================================= if ($WordToComplete -like '*=*') { $eqIdx = $WordToComplete.IndexOf('=') $flagPart = $WordToComplete.Substring(0, $eqIdx) $valPfx = $WordToComplete.Substring($eqIdx + 1) $resolved = Resolve-QwenFlagName -FlagName $flagPart -ContextKeys $ctxKeys $enumVals = Get-QwenEnumValues -FlagName $resolved -ContextKeys $ctxKeys if ($enumVals) { $tip = Get-QwenFlagDescription -FlagName $resolved -ContextKeys $ctxKeys $enumVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($valPfx) + '*') } | ForEach-Object { New-QwenCompletion "$flagPart=$_" -ListItemText $_ -Tooltip $tip } return } # Path / dir flags with inline syntax (global flags only). if (-not (Get-QwenContextFlagKind -FlagName $resolved -ContextKeys $ctxKeys) -and ($script:QwenPathFlags -contains $resolved -or $script:QwenDirFlags -contains $resolved)) { $typedWord = Get-QwenCursorWord -CommandAst $CommandAst -CursorPosition $CursorPosition Get-QwenPathCompletions -TypedWord $typedWord.Substring($typedWord.IndexOf('=') + 1) -Prefix "$flagPart=" } return } # ========================================================================= # 2. Value completion after space-separated value-taking flag # ========================================================================= if ($expectingValue -and ($WordToComplete -notlike '-*' -or $WordToComplete -match '^-\d')) { $enumVals = Get-QwenEnumValues -FlagName $currentFlag -ContextKeys $ctxKeys if ($enumVals) { $tip = Get-QwenFlagDescription -FlagName $currentFlag -ContextKeys $ctxKeys $enumVals | Where-Object { & $isWordPrefix $_ } | ForEach-Object { New-QwenCompletion $_ -Tooltip $tip } return } $isGlobal = -not (Get-QwenContextFlagKind -FlagName $currentFlag -ContextKeys $ctxKeys) # Path / dir completion. if ($isGlobal -and ($script:QwenPathFlags -contains $currentFlag -or $script:QwenDirFlags -contains $currentFlag)) { Get-QwenPathCompletions -TypedWord (Get-QwenCursorWord -CommandAst $CommandAst -CursorPosition $CursorPosition) return } # Number placeholder — suppresses filesystem fallback. if ($isGlobal -and $script:QwenNumberFlags -contains $currentFlag) { if (& $isWordPrefix '') { New-QwenCompletion '<n>' -Tooltip "Numeric value for $currentFlag" } return } # Generic placeholder — suppresses filesystem fallback for string/array flags. if (& $isWordPrefix '') { New-QwenCompletion '<value>' -Tooltip "Value for $currentFlag" } return } # ========================================================================= # 3. Flag completion (word starts with - or --) # ========================================================================= if ($WordToComplete -like '-*') { $flagSet = Get-QwenFlagSet -ContextKeys $ctxKeys $isShort = $WordToComplete -notlike '--*' foreach ($flag in $flagSet) { $short = Get-QwenShortAlias -FlagName $flag -ContextKeys $ctxKeys if ($isShort) { if (-not $short) { continue } if ($short -notlike "$WordToComplete*") { continue } $desc = Get-QwenFlagDescription -FlagName $flag -ContextKeys $ctxKeys New-QwenCompletion $short ` -ResultType ParameterName ` -Tooltip "$short -> ${flag}: $desc" } else { if ($flag -notlike "$WordToComplete*") { continue } $desc = Get-QwenFlagDescription -FlagName $flag -ContextKeys $ctxKeys $tip = if ($short) { "$flag (alias: $short): $desc" } else { "${flag}: $desc" } New-QwenCompletion $flag -ResultType ParameterName -Tooltip $tip } } return } # ========================================================================= # 4. Subcommand / positional completion # A positional already typed where a subcommand was due (unknown word) # ends completion here, so PowerShell's own fallback applies. # ========================================================================= # --- 4a. Have sub + subsub → offer L3 subcommands (or positional values). --- if ($sub -and $subsub -and $null -eq $sub3) { $l3k = "$sub.$subsub" $l3cs = $script:QwenL3Subcommands[$l3k] if ($l3cs) { if ($positionalCount -gt 0) { return } $l3cs | Where-Object { & $isWordPrefix $_ } | ForEach-Object { $tip = $script:QwenSubcmdDesc["$sub.$subsub.$_"] ?? $_ New-QwenCompletion $_ -Tooltip $tip } return } # No L3 commands → offer positional enum / path if applicable. $posVals = $script:QwenPositionalEnums[$l3k] if ($posVals) { $posVals | Where-Object { & $isWordPrefix $_ } | ForEach-Object { New-QwenCompletion $_ -Tooltip "Positional: $_" } } if ($script:QwenPathPositionalL2.Contains($l3k) -and $positionalCount -eq 0) { Get-QwenPathCompletions -TypedWord (Get-QwenCursorWord -CommandAst $CommandAst -CursorPosition $CursorPosition) } $placeholder = Get-QwenPositionalPlaceholder -ContextKey $l3k -PositionIndex $positionalCount if ($placeholder -and (& $isWordPrefix $placeholder)) { New-QwenCompletion $placeholder -Tooltip "Positional value for $l3k" } if ([string]::IsNullOrEmpty($WordToComplete)) { Write-QwenLongFlagResults -WordToComplete '' -ContextKeys $ctxKeys } return } # --- 4b. Have sub + subsub + sub3 → positional enum values only. --- if ($sub -and $subsub -and $sub3) { $posKey = "$sub.$subsub.$sub3" $posVals = $script:QwenPositionalEnums[$posKey] if ($posVals) { $posVals | Where-Object { & $isWordPrefix $_ } | ForEach-Object { New-QwenCompletion $_ -Tooltip "Positional: $_" } } $placeholder = Get-QwenPositionalPlaceholder -ContextKey $posKey -PositionIndex $positionalCount if ($placeholder -and (& $isWordPrefix $placeholder)) { New-QwenCompletion $placeholder -Tooltip "Positional value for $posKey" } if ([string]::IsNullOrEmpty($WordToComplete)) { Write-QwenLongFlagResults -WordToComplete '' -ContextKeys $ctxKeys } return } # --- 4c. Have sub only → offer L2 subcommands, or the command's own positional. --- if ($sub -and $null -eq $subsub) { $subs = $script:QwenSubSubcommands[$sub] if ($subs) { if ($positionalCount -gt 0) { return } $subs | Where-Object { -not $script:QwenHiddenCommands.Contains("$sub.$_") -and (& $isWordPrefix $_) } | ForEach-Object { $tip = $script:QwenSubcmdDesc["$sub.$_"] ?? $_ New-QwenCompletion $_ -Tooltip $tip } return } $placeholder = Get-QwenPositionalPlaceholder -ContextKey $sub -PositionIndex $positionalCount if ($placeholder -and (& $isWordPrefix $placeholder)) { New-QwenCompletion $placeholder -Tooltip "Positional value for $sub" } if ([string]::IsNullOrEmpty($WordToComplete)) { Write-QwenLongFlagResults -WordToComplete '' -ContextKeys $ctxKeys } return } # --- 4d. Top level → offer L1 subcommands (not after a prompt word). --- if ($positionalCount -gt 0) { return } $script:QwenTopCommands | Where-Object { -not $script:QwenHiddenCommands.Contains($_) -and (& $isWordPrefix $_) } | ForEach-Object { $tip = $script:QwenCmdDesc[$_] ?? $_ New-QwenCompletion $_ -Tooltip $tip } } #endregion #region -- Registration ------------------------------------------------------------------------- Register-ArgumentCompleter -CommandName @('qwen', 'qwen.cmd', 'qwen.ps1') -Native -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-QwenNative -WordToComplete $wordToComplete -CommandAst $commandAst -CursorPosition $cursorPosition } #endregion |