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.
    $script:QwenTopCommands = @('mcp', 'extensions', 'auth', 'hooks', 'hook', 'channel')

    # Level-2 subcommands per top-level command.
    $script:QwenSubSubcommands = @{
        mcp        = @('add', 'remove', 'list', 'reconnect')
        extensions = @('install', 'uninstall', 'list', 'update', 'disable', 'enable', 'link', 'new', 'settings')
        auth       = @('qwen-oauth', 'coding-plan', 'status')
        channel    = @('start', 'stop', 'status', 'pairing', 'configure-weixin')
        hooks      = @()
        hook       = @()
    }

    # Level-3 subcommands: "cmd.subcmd" -> @(subsubcmds).
    # Covers: extensions settings set|list and channel pairing list|approve.
    $script:QwenL3Subcommands = @{
        'extensions.settings' = @('set', 'list')
        'channel.pairing'     = @('list', 'approve')
    }

    # ---- Flag categories -----------------------------------------------------------------------

    # Global enum flags with fixed choice sets.
    $script:QwenEnumFlags = @{
        '--telemetry-target'        = @('local', 'gcp')
        '--telemetry-otlp-protocol' = @('grpc', 'http')
        '--approval-mode'           = @('plan', 'default', 'auto-edit', 'yolo')
        '--channel'                 = @('VSCode', 'ACP', 'SDK', 'CI')
        '--input-format'            = @('text', 'stream-json')
        '--output-format'           = @('text', 'json', 'stream-json')
        '--auth-type'               = @('openai', 'anthropic', 'qwen-oauth', 'gemini', 'vertex-ai')
        '--web-search-default'      = @('dashscope', 'tavily', 'google')   # [string], NOT a switch
    }

    # Boolean / switch flags (accept no value).
    $script:QwenBoolFlags = @(
        '--telemetry'
        '--telemetry-log-prompts'
        '--debug'
        '--chat-recording'
        '--sandbox'
        '--yolo'
        '--acp'
        '--experimental-lsp'
        '--openai-logging'
        '--screen-reader'
        '--include-partial-messages'
        '--checkpointing'
        '--list-extensions'
        '--continue'
        '--help'
        '--version'
    )

    # Free-form string flags (single value, no fixed choices).
    $script:QwenStringFlags = @(
        '--telemetry-otlp-endpoint'
        '--proxy'
        '--model'
        '--prompt'
        '--prompt-interactive'
        '--system-prompt'
        '--append-system-prompt'
        '--sandbox-image'
        '--openai-api-key'
        '--openai-base-url'
        '--tavily-api-key'
        '--google-api-key'
        '--google-search-engine-id'
        '--resume'
        '--session-id'
    )

    # Numeric flags.
    $script:QwenNumberFlags = @(
        '--max-session-turns'
    )

    # Array / multi-value flags.
    $script:QwenArrayFlags = @(
        '--allowed-mcp-server-names'
        '--allowed-tools'
        '--extensions'
        '--include-directories'
        '--add-dir'          # alias for --include-directories (help: --include-directories, --add-dir)
        '--core-tools'
        '--exclude-tools'
    )

    # Flags that produce file-path completion.
    $script:QwenPathFlags = @(
        '--telemetry-outfile'
    )

    # Flags that produce directory-path completion.
    $script:QwenDirFlags = @(
        '--openai-logging-dir'
        '--include-directories'
        '--add-dir'
    )

    # ---- Context tables ------------------------------------------------------------------------

    # L2 context-specific flags (beyond the global set): "cmd.subcmd" -> @(flags).
    $script:QwenContextFlags = @{
        'mcp.add'            = @('--scope', '--transport', '--env', '--header',
                                  '--timeout', '--trust', '--description',
                                  '--include-tools', '--exclude-tools')
        'mcp.reconnect'      = @('--all')
        'extensions.install' = @('--ref', '--auto-update', '--pre-release',
                                  '--registry', '--consent')
        'extensions.update'  = @('--all')
        'extensions.disable' = @('--scope')
        'extensions.enable'  = @('--scope')
        'auth.coding-plan'   = @('--region', '--key')
    }

    # Context-specific boolean flags (take no value when used in a subcommand).
    # These are checked to avoid incorrectly treating them as value-taking flags.
    $script:QwenContextBoolFlagSet = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::Ordinal
    )
    @('--trust', '--all', '--auto-update', '--pre-release', '--consent') |
        ForEach-Object { $null = $script:QwenContextBoolFlagSet.Add($_) }

    # Context-specific enum values: "cmd.subcmd.--flag" or "cmd.subcmd.subsub.--flag" -> @(choices).
    $script:QwenContextEnumFlags = @{
        'mcp.add.--scope'                  = @('user', 'project')
        'mcp.add.--transport'              = @('stdio', 'sse', 'http')
        'extensions.settings.set.--scope'  = @('user', 'workspace')
    }

    # L3 context-specific flags: "cmd.subcmd.subsubcmd" -> @(flags).
    $script:QwenL3ContextFlags = @{
        'extensions.settings.set' = @('--scope')
    }

    # Subcommand paths (L2) whose first positional argument accepts path completion.
    $script:QwenPathPositionalL2 = [System.Collections.Generic.HashSet[string]]::new(
        [System.StringComparer]::Ordinal
    )
    @('extensions.link', 'extensions.new', 'extensions.install') |
        ForEach-Object { $null = $script:QwenPathPositionalL2.Add($_) }

    # Positional enum values for specific subcommand paths.
    $script:QwenPositionalEnums = @{
        'channel.configure-weixin' = @('clear')
    }

    # 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>')
        '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>')
        'channel.start'            = @('<name>')
        'channel.pairing.list'     = @('<name>')
        'channel.pairing.approve'  = @('<name>', '<code>')
    }

    # ---- Short alias maps -----------------------------------------------------------------------

    $script:QwenShortAliases = @{
        '-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'
    }

    $script:QwenContextShortAliases = @{
        'mcp.add'          = @{
            '-s' = '--scope'
            '-t' = '--transport'
            '-e' = '--env'
            '-H' = '--header'
        }
        'mcp.reconnect'    = @{
            '-a' = '--all'
        }
        'auth.coding-plan' = @{
            '-r' = '--region'
            '-k' = '--key'
        }
    }

    # 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'
        auth       = 'Configure authentication (Qwen-OAuth or Alibaba Cloud Coding Plan)'
        hooks      = 'Manage hooks (use /hooks in interactive mode)'
        hook       = 'Alias for hooks'
        channel    = 'Manage messaging channels (Telegram, Discord, etc.)'
    }

    $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)'
        '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'
        'auth.qwen-oauth'                = 'Authenticate via Qwen OAuth'
        'auth.coding-plan'               = 'Authenticate via Alibaba Cloud Coding Plan'
        'auth.status'                    = 'Show current authentication status'
        'channel.start'                  = 'Start channels (all or named)'
        'channel.stop'                   = 'Stop the channel service'
        'channel.status'                 = 'Show channel service status'
        'channel.pairing'                = 'Manage DM pairing requests'
        'channel.configure-weixin'       = 'Configure WeChat channel (login or "clear")'
        'extensions.settings.set'        = 'Set a specific extension setting'
        'extensions.settings.list'       = 'List all settings for an extension'
        'channel.pairing.list'           = 'List pending pairing requests for a channel'
        'channel.pairing.approve'        = 'Approve a pending pairing request'
    }

    $script:QwenFlagDesc = @{
        # Global flags
        '--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'
        '--proxy'                    = '[string] HTTP proxy URL (deprecated)'
        '--chat-recording'           = '[boolean] Enable chat recording to disk'
        '--model'                    = '[string] Model to use'
        '--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'
        '--sandbox'                  = '[boolean] Enable sandbox mode'
        '--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-edit|yolo'
        '--checkpointing'            = '[boolean] Enable file-edit checkpointing (deprecated)'
        '--acp'                      = '[boolean] Start in ACP mode'
        '--experimental-lsp'         = '[boolean] Enable experimental LSP support'
        '--channel'                  = '[string] Channel identifier: VSCode|ACP|SDK|CI'
        '--allowed-mcp-server-names' = '[array] Allowed MCP server names'
        '--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'
        '--tavily-api-key'           = '[string] Tavily web-search API key'
        '--google-api-key'           = '[string] Google Custom Search API key'
        '--google-search-engine-id'  = '[string] Google Custom Search Engine ID'
        '--web-search-default'       = '[string] Default web search provider: dashscope|tavily|google'
        '--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)'
        '--continue'                 = '[boolean] Resume the most recent session'
        '--resume'                   = '[string] Resume a specific session by ID'
        '--session-id'               = '[string] Specify session ID for this run'
        '--max-session-turns'        = '[number] Maximum session turns'
        '--core-tools'               = '[array] Core tool paths'
        '--exclude-tools'            = '[array] Tools to exclude'
        '--auth-type'                = '[string] Authentication type'
        '--help'                     = '[boolean] Show help'
        '--version'                  = '[boolean] Show version'
        # Context-specific flags (mcp add / mcp reconnect)
        '--scope'                    = '[string] Configuration scope'
        '--transport'                = '[string] MCP transport: stdio|sse|http'
        '--env'                      = '[array] Environment variable: KEY=value'
        '--header'                   = '[array] HTTP header: "Name: value"'
        '--timeout'                  = '[number] Connection timeout in milliseconds'
        '--trust'                    = '[boolean] Trust server (bypass all tool confirmations)'
        '--description'              = '[string] Server description'
        '--include-tools'            = '[array] Tools to include (comma-separated)'
        '--all'                      = '[boolean] Apply to all'
        # Context-specific flags (extensions)
        '--ref'                      = '[string] Git ref to install from'
        '--auto-update'              = '[boolean] Enable auto-update for this extension'
        '--pre-release'              = '[boolean] Include pre-release versions'
        '--registry'                 = '[string] Custom npm registry URL'
        '--consent'                  = '[boolean] Acknowledge risks and skip confirmation'
        # Context-specific flags (auth coding-plan)
        '--region'                   = '[string] Region for Coding Plan (china/global)'
        '--key'                      = '[string] API key for Coding Plan'
    }
}

#region -- Helpers ------------------------------------------------------------------------------

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
    )
}

function Resolve-QwenFlagName {
    param(
        [string]$FlagName,
        [string]$Sub,
        [string]$SubSub,
        [string]$SubSubSub
    )
    foreach ($ctxKey in @(
        if ($Sub -and $SubSub -and $SubSubSub) { "$Sub.$SubSub.$SubSubSub" }
        if ($Sub -and $SubSub) { "$Sub.$SubSub" }
    )) {
        $ctxMap = $script:QwenContextShortAliases[$ctxKey]
        if ($ctxMap -and $ctxMap.ContainsKey($FlagName)) {
            return $ctxMap[$FlagName]
        }
    }
    $script:QwenShortAliases[$FlagName] ?? $FlagName
}

function Get-QwenShortAlias {
    param(
        [string]$FlagName,
        [string]$Sub,
        [string]$SubSub,
        [string]$SubSubSub
    )
    foreach ($ctxKey in @(
        if ($Sub -and $SubSub -and $SubSubSub) { "$Sub.$SubSub.$SubSubSub" }
        if ($Sub -and $SubSub) { "$Sub.$SubSub" }
    )) {
        $ctxReverse = $script:QwenContextAliasReverse[$ctxKey]
        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]$Sub,
        [string]$SubSub,
        [string]$SubSubSub
    )
    $r = Resolve-QwenFlagName -FlagName $FlagName -Sub $Sub -SubSub $SubSub -SubSubSub $SubSubSub

    # Global enum flags always take a value.
    if ($null -ne $script:QwenEnumFlags[$r]) { return $true }

    # Global value-taking flag categories.
    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 never take a value.
    if ($script:QwenBoolFlags   -contains $r) { return $false }

    # Context-specific enum flags.
    if ($Sub -and $SubSubSub) {
        $k3 = "$Sub.$SubSub.$SubSubSub.$r"
        if ($null -ne $script:QwenContextEnumFlags[$k3]) { return $true }
    }
    if ($Sub -and $SubSub) {
        $k2 = "$Sub.$SubSub.$r"
        if ($null -ne $script:QwenContextEnumFlags[$k2]) { return $true }
    }

    # Context-specific boolean flags (trust, all, auto-update, pre-release, consent).
    if ($script:QwenContextBoolFlagSet.Contains($r)) { return $false }

    # Remaining context flags: treat as value-taking unless proven boolean above.
    if ($Sub -and $SubSub) {
        $ctxFlags = $script:QwenContextFlags["$Sub.$SubSub"]
        if ($ctxFlags -and $ctxFlags -contains $r) { return $true }
    }
    if ($Sub -and $SubSub -and $SubSubSub) {
        $l3Flags = $script:QwenL3ContextFlags["$Sub.$SubSub.$SubSubSub"]
        if ($l3Flags -and $l3Flags -contains $r) { return $true }
    }

    return $false
}

# Returns the known enum choices for a flag in the given context, or $null.
function Get-QwenEnumValues {
    param(
        [string]$FlagName,
        [string]$Sub,
        [string]$SubSub,
        [string]$SubSubSub
    )
    $r = Resolve-QwenFlagName -FlagName $FlagName -Sub $Sub -SubSub $SubSub -SubSubSub $SubSubSub

    # Global enum first.
    $vals = $script:QwenEnumFlags[$r]
    if ($vals) { return $vals }

    # L3 context (most specific).
    if ($Sub -and $SubSub -and $SubSubSub) {
        $vals = $script:QwenContextEnumFlags["$Sub.$SubSub.$SubSubSub.$r"]
        if ($vals) { return $vals }
    }

    # L2 context.
    if ($Sub -and $SubSub) {
        $vals = $script:QwenContextEnumFlags["$Sub.$SubSub.$r"]
        if ($vals) { return $vals }
    }

    return $null
}

# Builds the full flag set for the current command context.
function Get-QwenFlagSet {
    param([string]$Sub, [string]$SubSub, [string]$SubSubSub)

    $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 ($Sub -and $SubSub) {
        $ctxFlags = $script:QwenContextFlags["$Sub.$SubSub"]
        if ($ctxFlags) { $ctxFlags | ForEach-Object { $null = $set.Add($_) } }
    }
    if ($Sub -and $SubSub -and $SubSubSub) {
        $l3Flags = $script:QwenL3ContextFlags["$Sub.$SubSub.$SubSubSub"]
        if ($l3Flags) { $l3Flags | 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]$Sub,
        [string]$SubSub,
        [string]$SubSubSub
    )
    $flagSet = Get-QwenFlagSet -Sub $Sub -SubSub $SubSub -SubSubSub $SubSubSub
    foreach ($flag in $flagSet) {
        if ($flag -notlike "$WordToComplete*") { continue }
        $desc = $script:QwenFlagDesc[$flag] ?? ''
        $short = Get-QwenShortAlias -FlagName $flag -Sub $Sub -SubSub $SubSub -SubSubSub $SubSubSub
        $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(
        $CommandName,
        $ParameterName,
        $WordToComplete,
        $CommandAst,
        $FakeBoundParameter
    )

    Initialize-QwenCompleterData

    # -------------------------------------------------------------------------
    # Detect the native ReadLine calling convention vs TabExpansion2.
    #
    # Native convention (ReadLine):
    # A) after trailing space: $CommandName='', $ParameterName='<full line>',
    # $WordToComplete='<cursor col>'
    # B) mid-token: $CommandName='<partial>', $ParameterName='<full line>',
    # $WordToComplete='<cursor col>'
    #
    # TabExpansion2 native path:
    # $CommandName='<wordToComplete>', $ParameterName=<CommandAst object>,
    # $WordToComplete='<cursorPosition int>'
    #
    # Heuristic: $WordToComplete is a pure integer AND $ParameterName coerces
    # to something that looks like a command line (starts with a non-space word).
    # -------------------------------------------------------------------------
    $isNativeConvention = $WordToComplete -match '^\d+$' -and
                          $ParameterName  -match '^\s*\S+'

    if ($isNativeConvention) {
        $nativePartialWord = $CommandName
        $cursorCol         = [int]$WordToComplete

        # $ParameterName is a CommandAst (TabExpansion2) or a string (ReadLine).
        # In both cases we need a plain string for parsing and indexing.
        if ($ParameterName -is [System.Management.Automation.Language.CommandAst]) {
            # TabExpansion2: we already have the CommandAst — reuse it directly.
            $CommandAst = $ParameterName
            $line       = $CommandAst.Extent.Text
        } else {
            $line    = [string]$ParameterName
            $tokens  = $null
            $parseErrs = $null
            $parsedAst = [System.Management.Automation.Language.Parser]::ParseInput(
                             $line, [ref]$tokens, [ref]$parseErrs)
            if ($parsedAst.EndBlock.Statements.Count -gt 0) {
                $pipeline = $parsedAst.EndBlock.Statements[0]
                if ($pipeline -is [System.Management.Automation.Language.PipelineAst] -and
                    $pipeline.PipelineElements.Count -gt 0 -and
                    $pipeline.PipelineElements[0] -is [System.Management.Automation.Language.CommandAst]) {
                    $CommandAst = $pipeline.PipelineElements[0]
                }
            }
        }

        if ($null -ne $CommandAst -and $CommandAst.CommandElements.Count -gt 1) {
            $lastEl     = $CommandAst.CommandElements[-1]
            $lastEnd    = $lastEl.Extent.EndOffset
            # Use Extent.Text universally — works for both CommandParameterAst
            # and StringConstantExpressionAst, and is safe under StrictMode.
            $lastTokVal = $lastEl.Extent.Text
            $cursorPastEnd  = $cursorCol -ge $line.Length
            $cursorPastTok  = $cursorCol -gt $lastEnd -and
                              ($cursorCol -gt $line.Length -or
                               [char]::IsWhiteSpace($line[$cursorCol - 1]))
            $isCompleteTok  = ($script:QwenTopCommands -contains $lastTokVal) -or
                              (($lastTokVal -like '-*') -and
                               $lastTokVal -notlike '*=*' -and
                               $lastTokVal.Length -gt 1 -and
                               $lastTokVal -ne '-')
            $hasTrailingSpace = $cursorPastTok -or
                              # Cursor at line end with a recognizable complete token means the
                              # user pressed Tab after finishing that token (no space typed yet).
                              # Only treat as trailing-space when cursor is actually PAST the
                              # token's own end offset; when cursor == lastEnd the token is
                              # still being completed as a prefix.
                              ($cursorPastEnd -and $isCompleteTok -and $cursorCol -gt $lastEnd)

            if ($hasTrailingSpace) {
                $WordToComplete = ''
            } elseif (-not [string]::IsNullOrEmpty($nativePartialWord)) {
                $WordToComplete = $nativePartialWord
            } else {
                $pfx = if ($cursorCol -le $line.Length) {
                           $line.Substring(0, $cursorCol)
                       } else { $line }
                $trimmed = $pfx.TrimEnd()
                $spc     = $trimmed.LastIndexOf(' ')
                if ($spc -ge 0) {
                    $WordToComplete = $trimmed.Substring($spc + 1)
                } else {
                    $fspc = $trimmed.IndexOf(' ')
                    $WordToComplete = if ($fspc -ge 0) {
                                         $trimmed.Substring($fspc + 1)
                                     } else { '' }
                }
            }
        } else {
            $WordToComplete = ''
        }
    }

    if ($null -eq $WordToComplete) { $WordToComplete = '' }
    if ($null -eq $CommandAst)     { return }

    $allElements = @($CommandAst.CommandElements)
    if ($allElements.Count -eq 0) { return }

    # -------------------------------------------------------------------------
    # Build the committed argument list, normalising CommandParameterAst tokens
    # (--flag) and StringConstantExpressionAst tokens identically.
    # -------------------------------------------------------------------------
    function Get-QwenTokenText {
        param($el)
        # CommandParameterAst (--flag or -f) may not have a .Value property;
        # use Extent.Text for all nodes to stay safe under StrictMode.
        $el.Extent.Text
    }

    $allArgs = @(foreach ($el in ($allElements | Select-Object -Skip 1)) {
        Get-QwenTokenText -el $el
    })

    # Exclude the word being completed from committed args (for positionals),
    # but keep it for flags so Test-QwenFlagTakesValue can set expectingValue.
    if ($allArgs.Count -gt 0 -and $allArgs[-1] -eq $WordToComplete) {
        if ($WordToComplete -like '-*') {
            $committedArgs = $allArgs
        } else {
            $cnt = $allArgs.Count - 2
            $committedArgs = if ($cnt -lt 0) { @() } else { $allArgs[0..$cnt] }
        }
    } else {
        $committedArgs = $allArgs
    }

    # -------------------------------------------------------------------------
    # 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 / channel pairing approve)
    $expectingValue  = $false
    $currentFlag     = $null
    $positionalCount = 0     # non-flag positionals seen after sub/subsub/sub3

    foreach ($token in $committedArgs) {
        if ($expectingValue) {
            $expectingValue = $false
            $currentFlag    = $null
            continue
        }

        if ($token -like '-*') {
            if ($token -like '*=*') { continue }    # inline --flag=value; value already consumed
            if (Test-QwenFlagTakesValue -FlagName $token -Sub $sub -SubSub $subsub -SubSubSub $sub3) {
                $expectingValue = $true
                $currentFlag    = Resolve-QwenFlagName -FlagName $token -Sub $sub -SubSub $subsub -SubSubSub $sub3
            }
            continue
        }

        # Positional token — try to match command levels first.
        $matched = $false

        if ($null -eq $sub -and $script:QwenTopCommands -contains $token) {
            $sub     = $token
            $matched = $true
        } elseif ($sub -and $null -eq $subsub) {
            $subs = $script:QwenSubSubcommands[$sub]
            if ($subs -and $subs -contains $token) {
                $subsub  = $token
                $matched = $true
            }
        } elseif ($sub -and $subsub -and $null -eq $sub3) {
            $l3k  = "$sub.$subsub"
            $l3cs = $script:QwenL3Subcommands[$l3k]
            if ($l3cs -and $l3cs -contains $token) {
                $sub3    = $token
                $matched = $true
            }
        }

        if (-not $matched) { $positionalCount++ }
    }

    # =========================================================================
    # 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 -Sub $sub -SubSub $subsub -SubSubSub $sub3

        $enumVals = Get-QwenEnumValues -FlagName $resolved -Sub $sub -SubSub $subsub -SubSubSub $sub3
        if ($enumVals) {
            $enumVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($valPfx) + '*') } | ForEach-Object {
                $tip = ($script:QwenFlagDesc[$resolved] ?? $_)
                New-QwenCompletion "$flagPart=$_" -ListItemText $_ -Tooltip $tip
            }
            return
        }

        # Path / dir flags with inline syntax.
        if ($script:QwenPathFlags -contains $resolved -or $script:QwenDirFlags -contains $resolved) {
            [System.Management.Automation.CompletionCompleters]::CompleteFilename($valPfx) |
                ForEach-Object {
                    New-QwenCompletion "$flagPart=$($_.CompletionText)" `
                        -ListItemText $_.ListItemText `
                        -ResultType   $_.ResultType `
                        -Tooltip      $_.ToolTip
                }
        }
        return
    }

    # =========================================================================
    # 2. Value completion after space-separated value-taking flag
    # =========================================================================
    if ($expectingValue) {
        $enumVals = Get-QwenEnumValues -FlagName $currentFlag -Sub $sub -SubSub $subsub -SubSubSub $sub3
        if ($enumVals) {
            $enumVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                $tip = ($script:QwenFlagDesc[$currentFlag] ?? $_)
                New-QwenCompletion $_ -Tooltip $tip
            }
            return
        }

        # Path / dir completion.
        if ($script:QwenPathFlags -contains $currentFlag -or
            $script:QwenDirFlags  -contains $currentFlag) {
            [System.Management.Automation.CompletionCompleters]::CompleteFilename($WordToComplete)
            return
        }

        # Number placeholder — suppresses filesystem fallback.
        if ($script:QwenNumberFlags -contains $currentFlag) {
            if ('' -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
                New-QwenCompletion '<n>' -Tooltip "Numeric value for $currentFlag"
            }
            return
        }

        # Generic placeholder — suppresses filesystem fallback for string/array flags.
        if ('' -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-QwenCompletion '<value>' -Tooltip "Value for $currentFlag"
        }
        return
    }

    # =========================================================================
    # 3. Flag completion (word starts with - or --)
    # =========================================================================
    if ($WordToComplete -like '-*') {
        $flagSet = Get-QwenFlagSet -Sub $sub -SubSub $subsub -SubSubSub $sub3
        $isShort = $WordToComplete -notlike '--*'

        foreach ($flag in $flagSet) {
            $desc  = $script:QwenFlagDesc[$flag] ?? ''
            $short = Get-QwenShortAlias -FlagName $flag -Sub $sub -SubSub $subsub -SubSubSub $sub3

            if ($isShort) {
                if (-not $short) { continue }
                if ($short -notlike "$WordToComplete*") { continue }
                New-QwenCompletion $short `
                    -ResultType ParameterName `
                    -Tooltip    "$short -> ${flag}: $desc"
            } else {
                if ($flag -notlike "$WordToComplete*") { continue }
                $tip = if ($short) { "$flag (alias: $short): $desc" } else { "${flag}: $desc" }
                New-QwenCompletion $flag -ResultType ParameterName -Tooltip $tip
            }
        }
        return
    }

    # =========================================================================
    # 4. Subcommand / positional completion
    # =========================================================================

    # --- 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) {
            $l3cs | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                $tip = $script:QwenSubcmdDesc["$sub.$subsub.$_"] ?? $_
                New-QwenCompletion $_ -Tooltip $tip
            }
            return
        }

        # No L3 commands → offer positional enum / path if applicable.
        $emitted = $false
        $posVals = $script:QwenPositionalEnums[$l3k]
        if ($posVals) {
            $posVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                New-QwenCompletion $_ -Tooltip "Positional: $_"
                $emitted = $true
            }
        }
        if ($script:QwenPathPositionalL2.Contains($l3k) -and $positionalCount -eq 0) {
            [System.Management.Automation.CompletionCompleters]::CompleteFilename($WordToComplete)
            $emitted = $true
        }

        $placeholder = Get-QwenPositionalPlaceholder -ContextKey $l3k -PositionIndex $positionalCount
        if ($placeholder -and $placeholder -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-QwenCompletion $placeholder -Tooltip "Positional value for $l3k"
            $emitted = $true
        }

        if ([string]::IsNullOrEmpty($WordToComplete)) {
            Write-QwenLongFlagResults -WordToComplete '' -Sub $sub -SubSub $subsub -SubSubSub $sub3
            return
        }

        if ($emitted) {
            return
        }
        return
    }

    # --- 4b. Have sub + subsub + sub3 → positional enum values only. ---
    if ($sub -and $subsub -and $sub3) {
        $posKey  = "$sub.$subsub.$sub3"
        $emitted = $false
        $posVals = $script:QwenPositionalEnums[$posKey]
        if ($posVals) {
            $posVals | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                New-QwenCompletion $_ -Tooltip "Positional: $_"
                $emitted = $true
            }
        }
        $placeholder = Get-QwenPositionalPlaceholder -ContextKey $posKey -PositionIndex $positionalCount
        if ($placeholder -and $placeholder -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*')) {
            New-QwenCompletion $placeholder -Tooltip "Positional value for $posKey"
            $emitted = $true
        }
        if ([string]::IsNullOrEmpty($WordToComplete)) {
            Write-QwenLongFlagResults -WordToComplete '' -Sub $sub -SubSub $subsub -SubSubSub $sub3
            return
        }
        if ($emitted) {
            return
        }
        return
    }

    # --- 4c. Have sub only → offer L2 subcommands. ---
    if ($sub -and $null -eq $subsub) {
        $subs = $script:QwenSubSubcommands[$sub]
        if ($subs) {
            $subs | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
                $tip = $script:QwenSubcmdDesc["$sub.$_"] ?? $_
                New-QwenCompletion $_ -Tooltip $tip
            }
        } elseif ([string]::IsNullOrEmpty($WordToComplete)) {
            Write-QwenLongFlagResults -WordToComplete '' -Sub $sub -SubSub $subsub -SubSubSub $sub3
        }
        return
    }

    # --- 4d. Top level → offer L1 subcommands. ---
    $script:QwenTopCommands | Where-Object { $_ -like ([System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*') } | ForEach-Object {
        $tip = $script:QwenCmdDesc[$_] ?? $_
        New-QwenCompletion $_ -Tooltip $tip
    }
}

#endregion

#region -- Registration -------------------------------------------------------------------------

Register-ArgumentCompleter -CommandName @('qwen', 'qwen.cmd', 'qwen.ps1') -Native -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
    Complete-QwenNative -CommandName $commandName -ParameterName $parameterName -WordToComplete $wordToComplete -CommandAst $commandAst -FakeBoundParameter $fakeBoundParameter
}

#endregion