Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
completionCmd := cli.NewCompletionCommand()
hashCmd := cli.NewHashCommand()
projectCmd := cli.NewProjectCommand()
lspCmd := cli.NewLSPCommand()

// Assign commands to groups
// Setup Commands
Expand Down Expand Up @@ -648,6 +649,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
completionCmd.GroupID = "utilities"
hashCmd.GroupID = "utilities"
projectCmd.GroupID = "utilities"
lspCmd.GroupID = "development"

// version command is intentionally left without a group (common practice)

Expand Down Expand Up @@ -677,6 +679,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
rootCmd.AddCommand(completionCmd)
rootCmd.AddCommand(hashCmd)
rootCmd.AddCommand(projectCmd)
rootCmd.AddCommand(lspCmd)
}

func main() {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/sourcegraph/conc v0.3.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v3 v3.0.4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot use existing yaml libraries

golang.org/x/crypto v0.48.0
golang.org/x/mod v0.33.0
golang.org/x/term v0.40.0
Expand Down Expand Up @@ -101,7 +102,6 @@ require (
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
Expand Down
57 changes: 57 additions & 0 deletions pkg/cli/lsp_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cli

import (
"fmt"
"os"

"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/lsp"
"github.com/spf13/cobra"
)

var lspLog = logger.New("cli:lsp")

// NewLSPCommand creates the lsp command
func NewLSPCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "lsp",
Short: "Start the Language Server Protocol server for workflow files",
Long: `Start a Language Server Protocol (LSP) server that provides IDE support
for agentic workflow Markdown files.

The LSP server communicates over stdio using JSON-RPC 2.0 and provides:
- Diagnostics (YAML syntax errors and schema validation)
- Hover information (schema descriptions for frontmatter keys)
- Completions (frontmatter keys, values, and workflow snippets)

The server is stateless (session-only memory, no daemon, no disk state).

Examples:
gh aw lsp # Start LSP server on stdio
echo '...' | gh aw lsp # Pipe LSP messages`,
Comment on lines +31 to +32
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second example "echo '...' | gh aw lsp" is misleading. LSP servers don't work well with simple piped input - they need to engage in a full JSON-RPC 2.0 request/response protocol over stdio.

Consider removing this example or replacing it with a more realistic one, such as:

Examples:
  gh aw lsp                           # Start LSP server on stdio (for IDE integration)

Or provide a more accurate example of how to configure it in an IDE (as shown in the PR description with the VS Code configuration).

Suggested change
gh aw lsp # Start LSP server on stdio
echo '...' | gh aw lsp # Pipe LSP messages`,
gh aw lsp # Start LSP server on stdio (for IDE integration)`,

Copilot uses AI. Check for mistakes.
RunE: func(cmd *cobra.Command, args []string) error {
return RunLSP()
},
}

return cmd
}

// RunLSP starts the LSP server on stdio.
func RunLSP() error {
lspLog.Print("Starting LSP server on stdio")

server, err := lsp.NewServer(os.Stdin, os.Stdout, os.Stderr)

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / mcp-server-compile-test

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / actions-build

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Workflow Compiler

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Workflow Misc Part 2

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / update

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Completion & Other

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Workflow Rendering & Bundling

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Progress Flag

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI HTTP MCP Connect

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Workflow Tools & MCP

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI MCP Other

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Audit Logs & Firewall

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Workflow Features

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CMD Tests

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration Add Workflows

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI MCP Connectivity

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Compile Workflows

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / audit

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Parser Remote Fetch & Cache

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Add & List Commands

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Compile & Poutine

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Workflow Validation

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Update Command

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Parser Location & Validation

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Workflow Actions & Containers

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Build & Test on macos-latest

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Alpine Container Test

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Docker Build

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / build

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / security

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Build & Test on windows-latest

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: CLI Security Tools

not enough arguments in call to lsp.NewServer

Check failure on line 45 in pkg/cli/lsp_command.go

View workflow job for this annotation

GitHub Actions / Integration: Workflow Infra

not enough arguments in call to lsp.NewServer
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NewServer function signature expects 4 parameters including a version string, but RunLSP() only passes 3 parameters (stdin, stdout, stderr). The version parameter should be passed using cli.GetVersion().

Change line 45 to:

server, err := lsp.NewServer(os.Stdin, os.Stdout, os.Stderr, cli.GetVersion())

This will ensure the LSP server can report its version in the initialize response.

Suggested change
server, err := lsp.NewServer(os.Stdin, os.Stdout, os.Stderr)
server, err := lsp.NewServer(os.Stdin, os.Stdout, os.Stderr, GetVersion())

Copilot uses AI. Check for mistakes.
if err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("failed to start LSP server: %s", err.Error())))
return fmt.Errorf("failed to start LSP server: %w", err)
}

if err := server.Run(); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("LSP server error: %s", err.Error())))
return fmt.Errorf("LSP server error: %w", err)
}

return nil
}
237 changes: 237 additions & 0 deletions pkg/lsp/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package lsp

import (
"fmt"
"sort"
"strings"

"github.com/github/gh-aw/pkg/logger"
)

var completionLog = logger.New("lsp:completion")

// HandleCompletion computes completion items for a position in a document.
func HandleCompletion(snap *DocumentSnapshot, pos Position, sp *SchemaProvider) *CompletionList {
if snap == nil || sp == nil {
return &CompletionList{}
}

// If no frontmatter, suggest the skeleton snippet
if !snap.HasFrontmatter {
return &CompletionList{
Items: snippetCompletions(),
}
}

// Only provide completions inside frontmatter
if !snap.PositionInFrontmatter(pos) {
return &CompletionList{}
}

// Compute the YAML path at the cursor position
yamlLine := pos.Line - snap.FrontmatterStartLine - 1
path, currentKey := YAMLPathAtPosition(snap.FrontmatterYAML, yamlLine)

completionLog.Printf("Completion at line %d: path=%v, key=%s", pos.Line, path, currentKey)

var items []CompletionItem

if len(path) == 0 && currentKey == "" {
// At top level with no context — suggest top-level keys
items = topLevelKeyCompletions(sp)
} else if len(path) == 0 && currentKey != "" {
// Typing a top-level key — filter top-level keys
items = filterCompletions(topLevelKeyCompletions(sp), currentKey)
} else {
// Nested context — first check if there are enum values for the value
if currentKey != "" {
enums := sp.EnumValues(append(path, currentKey))
if len(enums) > 0 {
items = enumCompletions(enums)
}
}

// Also suggest nested keys at this path
if len(items) == 0 {
props := sp.NestedProperties(path)
if len(props) > 0 {
items = propertyCompletions(props)
}
}
}

// Always include snippets when at top level
if len(path) == 0 {
items = append(items, snippetCompletions()...)
}

return &CompletionList{
IsIncomplete: false,
Items: items,
}
}

// topLevelKeyCompletions returns completion items for all top-level frontmatter keys.
func topLevelKeyCompletions(sp *SchemaProvider) []CompletionItem {
props := sp.TopLevelProperties()
return propertyCompletions(props)
}

// propertyCompletions converts PropertyInfo items into CompletionItem items.
func propertyCompletions(props []PropertyInfo) []CompletionItem {
sort.Slice(props, func(i, j int) bool {
// Required first, then alphabetical
if props[i].Required != props[j].Required {
return props[i].Required
}
return props[i].Name < props[j].Name
})

items := make([]CompletionItem, 0, len(props))
for i, p := range props {
item := CompletionItem{
Label: p.Name,
Kind: CompletionItemKindProperty,
Detail: p.Type,
Deprecated: p.Deprecated,
}

if p.Description != "" {
item.Documentation = &MarkupContent{
Kind: "markdown",
Value: p.Description,
}
}

if p.Required {
item.Detail = p.Type + " (required)"
}

// Insert text with colon and space
item.InsertText = p.Name + ": "
item.InsertTextFormat = InsertTextFormatPlainText

// Sort required items first using prefix
if p.Required {
item.SortText = "0_" + p.Name
} else {
item.SortText = "1_" + padIndex(i)
}

items = append(items, item)
}

return items
}

// enumCompletions returns completion items for enum values.
func enumCompletions(values []string) []CompletionItem {
items := make([]CompletionItem, 0, len(values))
for _, v := range values {
items = append(items, CompletionItem{
Label: v,
Kind: CompletionItemKindEnum,
InsertText: v,
InsertTextFormat: InsertTextFormatPlainText,
})
}
return items
}

// snippetCompletions returns pre-built snippet completions for common workflow patterns.
func snippetCompletions() []CompletionItem {
return []CompletionItem{
{
Label: "aw: Minimal workflow",
Kind: CompletionItemKindSnippet,
Detail: "New agentic workflow skeleton",
Documentation: &MarkupContent{
Kind: "markdown",
Value: "Creates a minimal agentic workflow with issue trigger, Copilot engine, and safe outputs.",
},
InsertText: `---
on:
issues:
types: [opened]
engine: copilot
permissions: read-all
safe-outputs:
add-comment:
---
# $1

$0`,
InsertTextFormat: InsertTextFormatSnippet,
SortText: "2_snippet_minimal",
},
{
Label: "aw: Slash command workflow",
Kind: CompletionItemKindSnippet,
Detail: "Workflow triggered by slash command",
Documentation: &MarkupContent{
Kind: "markdown",
Value: "Creates a workflow triggered by a slash command in issue or PR comments.",
},
InsertText: `---
on:
issue_comment:
types: [created]
slash_command:
name: $1
engine: copilot
permissions: read-all
safe-outputs:
add-comment:
---
# $2

$0`,
InsertTextFormat: InsertTextFormatSnippet,
SortText: "2_snippet_slash",
},
{
Label: "aw: Workflow with imports",
Kind: CompletionItemKindSnippet,
Detail: "Workflow with imported shared components",
Documentation: &MarkupContent{
Kind: "markdown",
Value: "Creates a workflow that imports shared workflow components.",
},
InsertText: `---
on:
issues:
types: [opened]
engine: copilot
imports:
- $1
permissions: read-all
safe-outputs:
add-comment:
---
# $2

$0`,
InsertTextFormat: InsertTextFormatSnippet,
SortText: "2_snippet_imports",
},
}
}

// filterCompletions filters completion items by prefix.
func filterCompletions(items []CompletionItem, prefix string) []CompletionItem {
if prefix == "" {
return items
}
prefix = strings.ToLower(prefix)
var filtered []CompletionItem
for _, item := range items {
if strings.HasPrefix(strings.ToLower(item.Label), prefix) {
filtered = append(filtered, item)
}
}
return filtered
}

func padIndex(i int) string {
return fmt.Sprintf("%04d", i)
}
Loading
Loading