Repository: GoogleCloudPlatform/kubectl-ai
File: pkg/ui/terminal.go (Go)

Code before:
	journal          journal.Recorder
	markdownRenderer *glamour.TermRenderer

	subscription io.Closer

	// Input handling fields (initialized once)
	rlInstance        *readline.Instance // For readline input
	ttyFile           *os.File           // For TTY input
	ttyReaderInstance *bufio.Reader      // For TTY input

	// currentBlock is the block we are rendering
	currentBlock Block
	// currentBlockText is text of the currentBlock that we have already rendered to the screen
	currentBlockText string

	// This is useful in cases where stdin is already been used for providing the input to the agent (caller in this case)
	// in such cases, stdin is already consumed and closed and reading input results in IO error.
	// In such cases, we open /dev/tty and use it for taking input.
	useTTYForInput bool
}

var _ UI = &TerminalUI{}

func NewTerminalUI(doc *Document, journal journal.Recorder, useTTYForInput bool) (*TerminalUI, error) {
	mdRenderer, err := glamour.NewTermRenderer(
		glamour.WithAutoStyle(),
		glamour.WithPreservedNewLines(),
		glamour.WithEmoji(),
	)
	if err != nil {
		return nil, fmt.Errorf("error initializing the markdown renderer: %w", err)
	}

	u := &TerminalUI{
		markdownRenderer: mdRenderer,
		journal:          journal,
		useTTYForInput:   useTTYForInput, // Store this flag
	}

	subscription := doc.AddSubscription(u)
	u.subscription = subscription

	return u, nil
}

func (u *TerminalUI) ttyReader() (*bufio.Reader, error) {
	if u.ttyReaderInstance != nil {
		return u.ttyReaderInstance, nil
	}
	// Initialize TTY input
	tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

Review comment (suggestion):
I wonder if we also allow user to set the `KUBECTL_AI_TERM_WIDTH=auto` to use the current terminal size automatically. This will give us a way to try it out to see if that will be sensible default.

Code after:
	journal          journal.Recorder
	markdownRenderer *glamour.TermRenderer

	subscription io.Closer

	// Input handling fields (initialized once)
	rlInstance        *readline.Instance // For readline input
	ttyFile           *os.File           // For TTY input
	ttyReaderInstance *bufio.Reader      // For TTY input

	// currentBlock is the block we are rendering
	currentBlock Block
	// currentBlockText is text of the currentBlock that we have already rendered to the screen
	currentBlockText string

	// This is useful in cases where stdin is already been used for providing the input to the agent (caller in this case)
	// in such cases, stdin is already consumed and closed and reading input results in IO error.
	// In such cases, we open /dev/tty and use it for taking input.
	useTTYForInput bool
}

var _ UI = &TerminalUI{}

func getCustomTerminalWidth() int {
	// Check for user-configured width via environment variable
	if widthStr := os.Getenv("KUBECTL_AI_TERM_WIDTH"); widthStr != "" {
		if width, err := strconv.Atoi(widthStr); err == nil && width > 0 {
			return width
		}
		klog.Warningf("Invalid KUBECTL_AI_TERM_WIDTH value %q, using default", widthStr)
	}

	// Return 0 to indicate no custom width should be set (use glamour's default)
	return 0
}

func NewTerminalUI(doc *Document, journal journal.Recorder, useTTYForInput bool) (*TerminalUI, error) {
	width := getCustomTerminalWidth()

	options := []glamour.TermRendererOption{
		glamour.WithAutoStyle(),
		glamour.WithPreservedNewLines(),
		glamour.WithEmoji(),
	}

	// Only add WordWrap if a valid width is configured
	if width > 0 {
		options = append(options, glamour.WithWordWrap(width))
	}

	mdRenderer, err := glamour.NewTermRenderer(options...)