Skip to content
Draft
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
15 changes: 15 additions & 0 deletions shared/llm/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ const (
ToolGetEnvironment ToolID = "get_environment"
ToolGlob ToolID = "glob"
ToolSearchCode ToolID = "search_code"
ToolGetPage ToolID = "get_page"
ToolGetElements ToolID = "get_elements"
ToolHighlightElement ToolID = "highlight_element"
ToolFillElement ToolID = "fill_element"
ToolClearHighlighting ToolID = "clear_highlighting"

// System tools
ToolWorkComplete ToolID = "work_complete" // Unified completion tool (replaces code_complete and query_complete)
Expand Down Expand Up @@ -215,6 +220,11 @@ var AvailableTools = []ToolID{
ToolListConnectedApps,
ToolListConnectedAppActions,
ToolExecuteConnectedAppAction,
ToolGetPage,
ToolGetElements,
ToolHighlightElement,
ToolFillElement,
ToolClearHighlighting,
}

var NoTools = []ToolID{}
Expand Down Expand Up @@ -256,6 +266,11 @@ func init() {
ToolGetEnvironment: ToolActorEnvironment,
ToolGlob: ToolActorEnvironment,
ToolSearchCode: ToolActorEnvironment,
ToolGetPage: ToolActorEnvironment,
ToolGetElements: ToolActorEnvironment,
ToolHighlightElement: ToolActorEnvironment,
ToolFillElement: ToolActorEnvironment,
ToolClearHighlighting: ToolActorEnvironment,

// System tools
ToolWorkComplete: ToolActorSystem,
Expand Down
73 changes: 73 additions & 0 deletions shared/tools/clear_highlighting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package tools

import (
"context"
"encoding/json"
"fmt"
)

type clearHighlightInput struct {
// No parameters needed
}

var clearHighlightSchema = GenerateSchema[clearHighlightInput]()

type ClearHighlightTool struct {
BaseTool
}

func NewClearHighlightTool(deps *ToolDependencies) *ClearHighlightTool {
return &ClearHighlightTool{
BaseTool: NewBaseTool(deps),
}
}

func (t *ClearHighlightTool) Name() string {
return "clear_highlighting"
}

func (t *ClearHighlightTool) UseCase() string {
return `Use this tool to clear all active highlight animations on the TUI screen.
This removes any visual highlights that were previously applied with highlight_element.`
}

func (t *ClearHighlightTool) Notes() string {
return `
- No parameters required
- Clears highlights from all components at once
- Use this after highlighting elements to return the UI to normal state
- Safe to call even if no highlights are active`
}

func (t *ClearHighlightTool) InputSchema() ToolInputSchema {
return clearHighlightSchema
}

func (t *ClearHighlightTool) validate(input *clearHighlightInput) error {
// No validation needed - no parameters
return nil
}

func (t *ClearHighlightTool) ValidateRawInput(input json.RawMessage) error {
_, err := ValidateAndParse(input, t.validate)
return err
}

func (t *ClearHighlightTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) {
_, err := ValidateAndParse(input, t.validate)
if err != nil {
return false, "", err
}

if t.deps.TUIState == nil {
return false, "", fmt.Errorf("clear_highlighting requires TUI state provider (tool cannot run in this context)")
}

err = t.deps.TUIState.ClearHighlight(ctx)
if err != nil {
return false, "", fmt.Errorf("failed to clear highlights: %w", err)
}

result := "Successfully cleared all highlights"
return false, result, nil
}
7 changes: 7 additions & 0 deletions shared/tools/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Executor struct {
threadPath string
connectedAppClient ConnectedAppClient
geminiClient *genai.Client
tuiState TUIStateProvider
}

// NewExecutor creates a new tool executor
Expand Down Expand Up @@ -58,6 +59,11 @@ func (e *Executor) SetGeminiClient(geminiClient *genai.Client) {
e.geminiClient = geminiClient
}

// SetTUIState sets the TUI state provider for TUI awareness tools
func (e *Executor) SetTUIState(tuiState TUIStateProvider) {
e.tuiState = tuiState
}

// Execute executes a tool and returns the result
// The tool is loaded, executed, and then freed from memory
// Note: Actor validation should be done by the caller before calling Execute
Expand Down Expand Up @@ -117,6 +123,7 @@ func (e *Executor) loadTool(toolName string) (Tool, error) {
ThreadPath: e.threadPath,
ConnectedAppClient: e.connectedAppClient,
GeminiClient: e.geminiClient,
TUIState: e.tuiState,
}
return LoadTool(toolName, deps)
}
Expand Down
128 changes: 128 additions & 0 deletions shared/tools/fill_element.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package tools

import (
"context"
"encoding/json"
"fmt"
)

type fillElementInput struct {
ElementName string `json:"element_name" jsonschema_description:"The name of the text input element to fill (use get_elements to see available elements)" jsonschema:"required"`
Text string `json:"text" jsonschema_description:"The text to insert into the input element" jsonschema:"required"`
}

var fillElementSchema = GenerateSchema[fillElementInput]()

type FillElementTool struct {
BaseTool
}

func NewFillElementTool(deps *ToolDependencies) *FillElementTool {
return &FillElementTool{
BaseTool: NewBaseTool(deps),
}
}

func (t *FillElementTool) Name() string {
return "fill_element"
}

func (t *FillElementTool) UseCase() string {
return `Use this tool to fill text into a text input element in the TUI.
This is useful for helping the user by pre-filling commands, queries, or other text they might want to use.
Use get_elements first to see the available text input elements.`
}

func (t *FillElementTool) Notes() string {
return `
- Requires element_name and text parameters
- The element_name must match a text_input element returned by get_elements
- Only works with text_input type elements - cannot fill other component types
- This helps the user by pre-filling text they might want to send or modify
- The user can still edit the filled text before sending
- Use this to suggest slash commands, queries, or other helpful text
- Returns an error if the element is not found or is not a text input`
}

func (t *FillElementTool) InputSchema() ToolInputSchema {
return fillElementSchema
}

func (t *FillElementTool) validate(input *fillElementInput) error {
if input.ElementName == "" {
return fmt.Errorf("element_name parameter is required")
}
if input.Text == "" {
return fmt.Errorf("text parameter is required")
}
return nil
}

func (t *FillElementTool) ValidateRawInput(input json.RawMessage) error {
_, err := ValidateAndParse(input, t.validate)
return err
}

func (t *FillElementTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) {
t.deps.Logger.Infow("fill_element: START", "input", string(input))

parsed, err := ValidateAndParse(input, t.validate)
if err != nil {
t.deps.Logger.Errorw("fill_element: validation failed", "error", err)
return false, "", err
}

t.deps.Logger.Infow("fill_element: parsed input", "elementName", parsed.ElementName, "text", parsed.Text)

if t.deps.TUIState == nil {
t.deps.Logger.Errorw("fill_element: TUIState is nil")
return false, "", fmt.Errorf("fill_element requires TUI state provider (tool cannot run in this context)")
}

t.deps.Logger.Infow("fill_element: getting elements for validation")

// Get all elements to validate the element exists and is fillable
elements, err := t.deps.TUIState.GetElements(ctx)
if err != nil {
t.deps.Logger.Errorw("fill_element: failed to get elements", "error", err)
return false, "", fmt.Errorf("failed to get elements: %w", err)
}

t.deps.Logger.Infow("fill_element: got elements", "count", len(elements))

// Find the element and check if it's a text input
var found bool
var elementType string
for _, elem := range elements {
if elem.Name == parsed.ElementName {
found = true
elementType = elem.Type
t.deps.Logger.Infow("fill_element: found element", "name", elem.Name, "type", elem.Type)
break
}
}

if !found {
t.deps.Logger.Errorw("fill_element: element not found", "elementName", parsed.ElementName)
return false, "", fmt.Errorf("element '%s' not found. Use get_elements to see available elements", parsed.ElementName)
}

// Only allow filling text_input elements
if elementType != "text_input" {
t.deps.Logger.Errorw("fill_element: element is not text_input", "elementName", parsed.ElementName, "actualType", elementType)
return false, "", fmt.Errorf("element '%s' is not a text input (type: %s). Only text_input elements can be filled with text", parsed.ElementName, elementType)
}

t.deps.Logger.Infow("fill_element: calling TUIState.FillElement", "elementName", parsed.ElementName, "text", parsed.Text)

err = t.deps.TUIState.FillElement(ctx, parsed.ElementName, parsed.Text)
if err != nil {
t.deps.Logger.Errorw("fill_element: FillElement failed", "error", err)
return false, "", fmt.Errorf("failed to fill element '%s': %w", parsed.ElementName, err)
}

t.deps.Logger.Infow("fill_element: SUCCESS - FillElement completed")

result := fmt.Sprintf("Successfully filled element '%s' with text", parsed.ElementName)
return false, result, nil
}
122 changes: 122 additions & 0 deletions shared/tools/get_elements.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package tools

import (
"context"
"encoding/json"
"fmt"
"strings"
)

type getElementsInput struct {
// This tool takes no parameters
}

var getElementsSchema = GenerateSchema[getElementsInput]()

type GetElementsTool struct {
BaseTool
}

func NewGetElementsTool(deps *ToolDependencies) *GetElementsTool {
return &GetElementsTool{
BaseTool: NewBaseTool(deps),
}
}

func (t *GetElementsTool) Name() string {
return "get_elements"
}

func (t *GetElementsTool) UseCase() string {
return `Use this tool to get a list of all visible elements on the current TUI screen.
This includes UI components (buttons, inputs, panels, etc.) and available slash commands.
Each element comes with a description and location information.`
}

func (t *GetElementsTool) Notes() string {
return `
- This tool takes no parameters
- Returns a list of all visible elements including:
- UI components (buttons, text inputs, panels, etc.)
- Available slash commands
- Each element includes:
- Name: unique identifier for the element
- Type: "component", "text_input", or "slash_command"
- Description: what the element does
- Location: where the element appears on screen
- IsHighlighted: whether the element is currently highlighted (only for components)
- Use this to understand what actions are available to the user
- Use element names with the highlight_element tool to draw attention to specific UI elements
- Check IsHighlighted field to see which components are currently highlighted`
}

func (t *GetElementsTool) InputSchema() ToolInputSchema {
return getElementsSchema
}

func (t *GetElementsTool) ValidateRawInput(input json.RawMessage) error {
// No validation needed - tool takes no parameters
return nil
}

func (t *GetElementsTool) ExecuteRaw(ctx context.Context, input json.RawMessage) (bool, string, error) {
if t.deps.TUIState == nil {
return false, "", fmt.Errorf("get_elements requires TUI state provider (tool cannot run in this context)")
}

elements, err := t.deps.TUIState.GetElements(ctx)
if err != nil {
return false, "", fmt.Errorf("failed to get elements: %w", err)
}

if len(elements) == 0 {
return false, "No elements found on current page.", nil
}

// Format elements into a readable list
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Found %d element(s) on current page:\n\n", len(elements)))

// Group by type
components := []TUIElement{}
slashCommands := []TUIElement{}

for _, elem := range elements {
if elem.Type == "slash_command" {
slashCommands = append(slashCommands, elem)
} else {
components = append(components, elem)
}
}

// Display components first
if len(components) > 0 {
sb.WriteString("=== UI Components ===\n\n")
for _, elem := range components {
highlightStatus := ""
if elem.IsHighlighted {
highlightStatus = " [HIGHLIGHTED]"
}
sb.WriteString(fmt.Sprintf("• %s%s\n", elem.Name, highlightStatus))
sb.WriteString(fmt.Sprintf(" Type: %s\n", elem.Type))
sb.WriteString(fmt.Sprintf(" Description: %s\n", elem.Description))
sb.WriteString(fmt.Sprintf(" Location: %s\n", elem.Location))
sb.WriteString(fmt.Sprintf(" IsHighlighted: %v\n\n", elem.IsHighlighted))
}
}

// Display slash commands
if len(slashCommands) > 0 {
sb.WriteString("=== Available Slash Commands ===\n\n")
for _, elem := range slashCommands {
sb.WriteString(fmt.Sprintf("• %s\n", elem.Name))
sb.WriteString(fmt.Sprintf(" Description: %s\n", elem.Description))
if elem.Location != "" {
sb.WriteString(fmt.Sprintf(" Location: %s\n", elem.Location))
}
sb.WriteString("\n")
}
}

return false, sb.String(), nil
}
Loading
Loading