Files
kit/internal/extensions/api.go
T
Ed Zynda 9449f1fcdf feat: add session management, persistence, editor text, and status bar APIs for extensions
Implement Phase 1 extension API gaps identified in the pi-mono gap analysis:

- Gap 1: Session Management API (GetMessages, GetSessionPath) — read-only
  access to conversation history from extensions
- Gap 2: Session Persistence (AppendEntry, GetEntries) — custom extension
  data survives across session restarts via new ExtensionDataEntry type
- Gap 10: SetEditorText — extensions can pre-fill the input editor
- Gap M3: Keyed Status Bar (SetStatus, RemoveStatus) — multiple extensions
  can place independent entries in the TUI status bar, ordered by priority
2026-03-02 01:33:56 +03:00

1039 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package extensions
// ---------------------------------------------------------------------------
// Internal types (used by runner, NOT exposed to Yaegi)
// ---------------------------------------------------------------------------
// Event is the interface satisfied by all event types internally.
type Event interface {
Type() EventType
}
// Result is the interface satisfied by all result types internally.
type Result interface {
isResult()
}
// HandlerFunc is the internal handler signature used by the runner.
type HandlerFunc func(event Event, ctx Context) Result
// ---------------------------------------------------------------------------
// Context (exposed to Yaegi — concrete struct, no interfaces)
// ---------------------------------------------------------------------------
// Context provides runtime information to handlers about the current session.
type Context struct {
SessionID string
CWD string
Model string
Interactive bool
// Print outputs plain text to the user. In interactive mode this
// routes through BubbleTea's scrollback (tea.Println); in
// non-interactive mode it writes to stdout. Extensions must use
// this instead of fmt.Println, which is swallowed by BubbleTea.
Print func(string)
// PrintInfo outputs text as a styled system message block (bordered,
// themed). Use this for informational notices the user should see.
PrintInfo func(string)
// PrintError outputs text as a styled error block (red border, bold).
// Use this for error messages or warnings.
PrintError func(string)
// PrintBlock outputs text as a custom styled block with caller-chosen
// border color and optional subtitle. Example:
//
// ctx.PrintBlock(ext.PrintBlockOpts{
// Text: "Deployment complete!",
// BorderColor: "#a6e3a1",
// Subtitle: "my-extension",
// })
PrintBlock func(PrintBlockOpts)
// SendMessage injects a message into the conversation and triggers a
// new agent turn. If the agent is currently busy the message is queued
// and processed after the current turn completes.
//
// This is safe to call from goroutines. Common pattern:
//
// go func() {
// out, _ := exec.Command("kit", "-p", task).Output()
// ctx.SendMessage("Subagent result:\n" + string(out))
// }()
SendMessage func(string)
// SetWidget places or updates a persistent widget in the TUI. Widgets
// remain visible across agent turns until explicitly removed. The
// widget is identified by WidgetConfig.ID; calling SetWidget with the
// same ID replaces the previous content.
//
// Example:
//
// ctx.SetWidget(ext.WidgetConfig{
// ID: "my-status",
// Placement: ext.WidgetAbove,
// Content: ext.WidgetContent{Text: "Build: passing"},
// Style: ext.WidgetStyle{BorderColor: "#a6e3a1"},
// })
SetWidget func(WidgetConfig)
// RemoveWidget removes a previously placed widget by its ID. No-op if
// the ID does not exist.
RemoveWidget func(id string)
// SetHeader places a custom header at the top of the TUI view, above
// the stream region. Only one header can be active at a time; calling
// SetHeader replaces any previous header. The header persists across
// agent turns until explicitly removed.
//
// Example:
//
// ctx.SetHeader(ext.HeaderFooterConfig{
// Content: ext.WidgetContent{Text: "Project: my-app | Branch: main"},
// Style: ext.WidgetStyle{BorderColor: "#89b4fa"},
// })
SetHeader func(HeaderFooterConfig)
// RemoveHeader removes the custom header. No-op if no header is set.
RemoveHeader func()
// SetFooter places a custom footer at the bottom of the TUI view,
// below the status bar. Only one footer can be active at a time;
// calling SetFooter replaces any previous footer. The footer persists
// across agent turns until explicitly removed.
//
// Example:
//
// ctx.SetFooter(ext.HeaderFooterConfig{
// Content: ext.WidgetContent{Text: "Ready | 3 tasks remaining"},
// Style: ext.WidgetStyle{BorderColor: "#a6e3a1"},
// })
SetFooter func(HeaderFooterConfig)
// RemoveFooter removes the custom footer. No-op if no footer is set.
RemoveFooter func()
// PromptSelect shows a selection list to the user and blocks until
// they pick an option or cancel (ESC). Returns a cancelled result in
// non-interactive mode. Safe to call from event handlers and slash
// command handlers.
//
// Example:
//
// result := ctx.PromptSelect(ext.PromptSelectConfig{
// Message: "Choose a deployment target:",
// Options: []string{"staging", "production", "local"},
// })
// if !result.Cancelled {
// fmt.Println("Selected:", result.Value)
// }
PromptSelect func(PromptSelectConfig) PromptSelectResult
// PromptConfirm shows a yes/no confirmation to the user and blocks
// until they respond or cancel. Returns a cancelled result in
// non-interactive mode.
//
// Example:
//
// result := ctx.PromptConfirm(ext.PromptConfirmConfig{
// Message: "Deploy to production?",
// DefaultValue: false,
// })
// if !result.Cancelled && result.Value {
// // proceed with deployment
// }
PromptConfirm func(PromptConfirmConfig) PromptConfirmResult
// PromptInput shows a text input field to the user and blocks until
// they submit text or cancel. Returns a cancelled result in
// non-interactive mode.
//
// Example:
//
// result := ctx.PromptInput(ext.PromptInputConfig{
// Message: "Enter the release tag:",
// Placeholder: "v1.0.0",
// })
// if !result.Cancelled {
// fmt.Println("Tag:", result.Value)
// }
PromptInput func(PromptInputConfig) PromptInputResult
// ShowOverlay displays a modal overlay dialog that blocks until the
// user dismisses it or selects an action. The overlay renders as a
// centered (or anchored) bordered box over the TUI. Returns a
// cancelled result in non-interactive mode.
//
// Example:
//
// result := ctx.ShowOverlay(ext.OverlayConfig{
// Title: "Deployment Summary",
// Content: ext.WidgetContent{Text: "All 3 services deployed."},
// Style: ext.OverlayStyle{BorderColor: "#a6e3a1"},
// Actions: []string{"Continue", "Rollback", "Details"},
// })
// if !result.Cancelled {
// fmt.Println("Selected:", result.Action)
// }
ShowOverlay func(OverlayConfig) OverlayResult
// SetEditor installs an editor interceptor that wraps the built-in
// input editor. The interceptor can intercept keys (remap, consume,
// submit) and modify the rendered output. Only one interceptor is
// active at a time; calling SetEditor replaces any previous interceptor.
//
// Example — vim-like normal mode:
//
// ctx.SetEditor(ext.EditorConfig{
// HandleKey: func(key, text string) ext.EditorKeyAction {
// switch key {
// case "h":
// return ext.EditorKeyAction{Type: ext.EditorKeyRemap, RemappedKey: "left"}
// case "i":
// ctx.ResetEditor()
// return ext.EditorKeyAction{Type: ext.EditorKeyConsumed}
// }
// return ext.EditorKeyAction{Type: ext.EditorKeyPassthrough}
// },
// Render: func(width int, content string) string {
// return "[NORMAL]\n" + content
// },
// })
SetEditor func(EditorConfig)
// ResetEditor removes the active editor interceptor and restores the
// default built-in editor behavior. No-op if no interceptor is set.
ResetEditor func()
// SetUIVisibility controls which built-in TUI chrome elements are
// visible. By default all elements are shown (zero value = show all).
// Call this during OnSessionStart to configure the initial layout.
//
// Example — minimal chrome:
//
// ctx.SetUIVisibility(ext.UIVisibility{
// HideStartupMessage: true,
// HideStatusBar: true,
// HideSeparator: true,
// HideInputHint: true,
// })
SetUIVisibility func(UIVisibility)
// GetContextStats returns current context-window usage information
// (estimated tokens, context limit, usage percentage, message count).
// Useful for building context meters, auto-compaction triggers, etc.
//
// Example:
//
// stats := ctx.GetContextStats()
// pct := int(stats.UsagePercent * 100)
// fmt.Sprintf("[%s%s] %d%%", strings.Repeat("#", pct/10), strings.Repeat("-", 10-pct/10), pct)
GetContextStats func() ContextStats
// --- Session Management (Gap 1) ---
// GetMessages returns the conversation messages on the current branch,
// ordered from root to leaf. This is a read-only view; extensions
// cannot modify messages directly.
//
// Example:
//
// msgs := ctx.GetMessages()
// for _, m := range msgs {
// if m.Role == "assistant" {
// lastResponse = m.Content
// }
// }
GetMessages func() []SessionMessage
// GetSessionPath returns the file path of the current session's JSONL
// file. Returns empty string for in-memory (ephemeral) sessions.
GetSessionPath func() string
// --- Session Persistence (Gap 2) ---
// AppendEntry persists custom extension data in the session tree.
// The data survives across session restarts and can be retrieved via
// GetEntries. Use entryType to namespace your data (e.g. "myext:state").
//
// Example:
//
// data, _ := json.Marshal(myState)
// ctx.AppendEntry("myext:state", string(data))
AppendEntry func(entryType string, data string) (string, error)
// GetEntries retrieves all persisted extension data entries matching
// the given type on the current branch, ordered root to leaf. Pass
// empty string to retrieve all extension data entries.
//
// Example — restore state on session resume:
//
// entries := ctx.GetEntries("myext:state")
// if len(entries) > 0 {
// last := entries[len(entries)-1]
// json.Unmarshal([]byte(last.Data), &myState)
// }
GetEntries func(entryType string) []ExtensionEntry
// SetEditorText sets the text content of the input editor. This can
// be used to pre-fill the editor with suggested text (e.g. extracted
// questions, handoff prompts). The cursor is moved to the end.
//
// Example:
//
// ctx.SetEditorText("Please review the changes in src/main.go")
SetEditorText func(text string)
// --- Keyed Status Bar (Gap M3) ---
// SetStatus places or updates a keyed entry in the TUI status bar.
// Multiple entries from different extensions coexist; each is identified
// by a unique key. Lower priority values render further left.
//
// Example:
//
// ctx.SetStatus("myext:branch", "main", 50)
SetStatus func(key string, text string, priority int)
// RemoveStatus removes a keyed status bar entry. No-op if the key
// does not exist.
RemoveStatus func(key string)
}
// ---------------------------------------------------------------------------
// Session types (exposed to Yaegi — concrete structs for session access)
// ---------------------------------------------------------------------------
// SessionMessage represents a conversation message exposed to extensions.
// This is a simplified, read-only view of the internal message structures.
type SessionMessage struct {
// ID is the unique entry identifier in the session tree.
ID string
// ParentID links this entry to its parent in the tree.
ParentID string
// Role is the message role: "user", "assistant", "tool", or "system".
Role string
// Content is the text content of the message (tool calls and results
// are serialized as text summaries).
Content string
// Model is the model that generated this message (empty for user messages).
Model string
// Provider is the provider used (empty for user messages).
Provider string
// Timestamp is the RFC3339-formatted creation time.
Timestamp string
}
// ExtensionEntry represents persisted extension data stored in the session.
// Extensions use AppendEntry to save custom state and GetEntries to retrieve
// it on session resume.
type ExtensionEntry struct {
// ID is the unique entry identifier.
ID string
// EntryType is the extension-defined type string (e.g. "plan-mode:state").
EntryType string
// Data is the extension-defined payload (JSON or plain text).
Data string
// Timestamp is the RFC3339-formatted creation time.
Timestamp string
}
// ---------------------------------------------------------------------------
// Status bar types (exposed to Yaegi — concrete structs)
// ---------------------------------------------------------------------------
// StatusBarEntry represents a keyed entry in the TUI status bar. Extensions
// can set multiple independent entries that render alongside the built-in
// model name and token usage display.
type StatusBarEntry struct {
// Key uniquely identifies this entry (e.g. "myext:git-branch").
Key string
// Text is the rendered content shown in the status bar.
Text string
// Priority controls ordering. Lower values render further left.
// Built-in entries (model, usage) have implicit priority 100-110.
Priority int
}
// PrintBlockOpts configures a custom styled block for PrintBlock.
type PrintBlockOpts struct {
// Text is the main content to display.
Text string
// BorderColor is a hex color string (e.g. "#a6e3a1") for the left border.
// Defaults to the theme's system color if empty.
BorderColor string
// Subtitle is optional text shown below the content in muted style
// (e.g. extension name, timestamp). Empty means no subtitle line.
Subtitle string
}
// ---------------------------------------------------------------------------
// API — the object passed to each extension's Init function.
//
// Instead of a generic On(EventType, HandlerFunc) that uses interfaces,
// we expose event-specific methods with concrete function signatures.
// This avoids Yaegi's genInterfaceWrapper crash entirely — no interfaces
// cross the Yaegi boundary.
// ---------------------------------------------------------------------------
// API is passed to each extension's Init function. Extensions use it to
// register typed event handlers, custom tools, and slash commands.
type API struct {
// Event-specific registration functions (wired by the loader).
onToolCall func(func(ToolCallEvent, Context) *ToolCallResult)
onToolExecStart func(func(ToolExecutionStartEvent, Context))
onToolExecEnd func(func(ToolExecutionEndEvent, Context))
onToolResult func(func(ToolResultEvent, Context) *ToolResultResult)
onInput func(func(InputEvent, Context) *InputResult)
onBeforeAgentStart func(func(BeforeAgentStartEvent, Context) *BeforeAgentStartResult)
onAgentStart func(func(AgentStartEvent, Context))
onAgentEnd func(func(AgentEndEvent, Context))
onMessageStart func(func(MessageStartEvent, Context))
onMessageUpdate func(func(MessageUpdateEvent, Context))
onMessageEnd func(func(MessageEndEvent, Context))
onSessionStart func(func(SessionStartEvent, Context))
onSessionShutdown func(func(SessionShutdownEvent, Context))
registerToolFn func(ToolDef)
registerCmdFn func(CommandDef)
registerToolRendererFn func(ToolRenderConfig)
}
// OnToolCall registers a handler that fires before a tool executes.
// Return a non-nil ToolCallResult with Block=true to prevent execution.
func (a *API) OnToolCall(handler func(ToolCallEvent, Context) *ToolCallResult) {
a.onToolCall(handler)
}
// OnToolExecutionStart registers a handler for tool execution start.
func (a *API) OnToolExecutionStart(handler func(ToolExecutionStartEvent, Context)) {
a.onToolExecStart(handler)
}
// OnToolExecutionEnd registers a handler for tool execution end.
func (a *API) OnToolExecutionEnd(handler func(ToolExecutionEndEvent, Context)) {
a.onToolExecEnd(handler)
}
// OnToolResult registers a handler that fires after tool execution.
// Return a non-nil ToolResultResult to modify the output.
func (a *API) OnToolResult(handler func(ToolResultEvent, Context) *ToolResultResult) {
a.onToolResult(handler)
}
// OnInput registers a handler that fires when user input is received.
// Return a non-nil InputResult to transform or handle the input.
func (a *API) OnInput(handler func(InputEvent, Context) *InputResult) {
a.onInput(handler)
}
// OnBeforeAgentStart registers a handler that fires before the agent loop.
func (a *API) OnBeforeAgentStart(handler func(BeforeAgentStartEvent, Context) *BeforeAgentStartResult) {
a.onBeforeAgentStart(handler)
}
// OnAgentStart registers a handler for when the agent loop begins.
func (a *API) OnAgentStart(handler func(AgentStartEvent, Context)) {
a.onAgentStart(handler)
}
// OnAgentEnd registers a handler for when the agent finishes responding.
func (a *API) OnAgentEnd(handler func(AgentEndEvent, Context)) {
a.onAgentEnd(handler)
}
// OnMessageStart registers a handler for when an assistant message begins.
func (a *API) OnMessageStart(handler func(MessageStartEvent, Context)) {
a.onMessageStart(handler)
}
// OnMessageUpdate registers a handler for streaming text chunks.
func (a *API) OnMessageUpdate(handler func(MessageUpdateEvent, Context)) {
a.onMessageUpdate(handler)
}
// OnMessageEnd registers a handler for when the assistant message is complete.
func (a *API) OnMessageEnd(handler func(MessageEndEvent, Context)) {
a.onMessageEnd(handler)
}
// OnSessionStart registers a handler for when a session is loaded or created.
func (a *API) OnSessionStart(handler func(SessionStartEvent, Context)) {
a.onSessionStart(handler)
}
// OnSessionShutdown registers a handler for when the application is closing.
func (a *API) OnSessionShutdown(handler func(SessionShutdownEvent, Context)) {
a.onSessionShutdown(handler)
}
// RegisterTool adds a custom tool that the LLM can invoke.
func (a *API) RegisterTool(tool ToolDef) {
a.registerToolFn(tool)
}
// RegisterCommand adds a slash command available in interactive mode.
func (a *API) RegisterCommand(cmd CommandDef) {
a.registerCmdFn(cmd)
}
// RegisterToolRenderer registers a custom renderer for a specific tool's
// display in the TUI. The renderer controls the header (parameter summary)
// and/or body (result display) of the tool's output block. If multiple
// extensions register renderers for the same tool name, the last one wins.
func (a *API) RegisterToolRenderer(config ToolRenderConfig) {
a.registerToolRendererFn(config)
}
// ---------------------------------------------------------------------------
// Widget types (exposed to Yaegi — concrete structs, no interfaces)
// ---------------------------------------------------------------------------
// WidgetPlacement determines where a widget appears in the TUI layout
// relative to the input area.
type WidgetPlacement string
const (
// WidgetAbove places the widget above the input area, between the
// separator and queued messages.
WidgetAbove WidgetPlacement = "above"
// WidgetBelow places the widget below the input area, between the
// input and the status bar.
WidgetBelow WidgetPlacement = "below"
)
// WidgetContent describes what to render in a widget slot.
type WidgetContent struct {
// Text is the content to display.
Text string
// Markdown, when true, renders Text as styled markdown instead of
// plain text.
Markdown bool
}
// WidgetStyle configures the visual appearance of a widget.
type WidgetStyle struct {
// BorderColor is a hex color (e.g. "#a6e3a1") for the left border.
// Empty uses the theme's default accent color.
BorderColor string
// NoBorder disables the left border entirely.
NoBorder bool
}
// WidgetConfig fully describes a widget for placement in the TUI.
// Extensions identify widgets by ID; calling SetWidget with the same ID
// replaces the previous widget. IDs should be descriptive to avoid
// collisions across extensions (e.g. "myext:token-counter").
type WidgetConfig struct {
// ID uniquely identifies this widget. Must be non-empty.
ID string
// Placement determines where the widget appears (above or below input).
Placement WidgetPlacement
// Content describes what to render.
Content WidgetContent
// Style configures the appearance.
Style WidgetStyle
// Priority controls ordering within a placement slot. Lower values
// render first. Widgets with equal priority are ordered by insertion
// time.
Priority int
}
// ---------------------------------------------------------------------------
// Interactive prompt types (exposed to Yaegi — concrete structs)
// ---------------------------------------------------------------------------
// PromptSelectConfig configures a selection prompt that presents the user
// with a list of options to choose from.
type PromptSelectConfig struct {
// Message is the question or instruction displayed to the user.
Message string
// Options is the list of choices the user can select from.
Options []string
}
// PromptSelectResult is the response from a selection prompt.
type PromptSelectResult struct {
// Value is the text of the selected option.
Value string
// Index is the zero-based index of the selected option.
Index int
// Cancelled is true if the user dismissed the prompt (ESC) or
// the prompt was unavailable (non-interactive mode).
Cancelled bool
}
// PromptConfirmConfig configures a yes/no confirmation prompt.
type PromptConfirmConfig struct {
// Message is the question displayed to the user.
Message string
// DefaultValue is the pre-selected answer (true = Yes).
DefaultValue bool
}
// PromptConfirmResult is the response from a confirmation prompt.
type PromptConfirmResult struct {
// Value is true for "Yes", false for "No".
Value bool
// Cancelled is true if the user dismissed the prompt.
Cancelled bool
}
// PromptInputConfig configures a free-form text input prompt.
type PromptInputConfig struct {
// Message is the question displayed to the user.
Message string
// Placeholder is ghost text shown when the input is empty.
Placeholder string
// Default is the pre-filled value in the input field.
Default string
}
// PromptInputResult is the response from a text input prompt.
type PromptInputResult struct {
// Value is the text the user entered.
Value string
// Cancelled is true if the user dismissed the prompt.
Cancelled bool
}
// ---------------------------------------------------------------------------
// Header/Footer types (exposed to Yaegi — concrete structs)
// ---------------------------------------------------------------------------
// HeaderFooterConfig describes a custom header or footer region that replaces
// or augments the default TUI chrome. Extensions use ctx.SetHeader/SetFooter
// to place one; only one header and one footer can be active at a time (the
// latest call wins). Reuses WidgetContent and WidgetStyle for consistency.
type HeaderFooterConfig struct {
// Content describes what to render.
Content WidgetContent
// Style configures the appearance.
Style WidgetStyle
}
// ---------------------------------------------------------------------------
// UI visibility (exposed to Yaegi — concrete struct)
// ---------------------------------------------------------------------------
// UIVisibility controls which built-in TUI chrome elements are visible.
// The zero value shows everything (backward compatible). Extensions call
// ctx.SetUIVisibility to customise the layout — for example, a "minimal"
// theme can hide the startup banner, status bar, and input hint and replace
// them with a single custom footer.
type UIVisibility struct {
HideStartupMessage bool // Hide the "Model loaded..." startup block
HideStatusBar bool // Hide the "provider · model Tokens: ..." line
HideSeparator bool // Hide the "────────" divider between stream and input
HideInputHint bool // Hide the "enter submit · ctrl+j..." hint below input
}
// ---------------------------------------------------------------------------
// Context stats (exposed to Yaegi — concrete struct)
// ---------------------------------------------------------------------------
// ContextStats contains current context-window usage information.
// Extensions can poll this via ctx.GetContextStats() to build usage
// meters, auto-compaction triggers, etc.
type ContextStats struct {
EstimatedTokens int // Estimated token count of the current conversation
ContextLimit int // Model's context window size (tokens), 0 if unknown
UsagePercent float64 // Fraction of context used (0.01.0), 0 if limit unknown
MessageCount int // Number of messages in the conversation
}
// ---------------------------------------------------------------------------
// Overlay types (exposed to Yaegi — concrete structs)
// ---------------------------------------------------------------------------
// OverlayAnchor determines the vertical position of an overlay dialog
// within the TUI view.
type OverlayAnchor string
const (
// OverlayCenter positions the dialog in the vertical center.
OverlayCenter OverlayAnchor = "center"
// OverlayTopCenter positions the dialog near the top of the view.
OverlayTopCenter OverlayAnchor = "top-center"
// OverlayBottomCenter positions the dialog near the bottom of the view.
OverlayBottomCenter OverlayAnchor = "bottom-center"
)
// OverlayStyle configures the visual appearance of an overlay dialog.
type OverlayStyle struct {
// BorderColor is a hex color (e.g. "#89b4fa") for the dialog border.
// Empty uses a default blue accent.
BorderColor string
// Background is a hex color (e.g. "#1e1e2e") for the dialog background.
// Empty means no explicit background (inherits terminal default).
Background string
}
// OverlayConfig fully describes a modal overlay dialog. Extensions call
// ctx.ShowOverlay(config) to display the dialog and block until the user
// dismisses it or selects an action. The dialog renders as a bordered box
// positioned within the TUI, with optional scrollable content and action
// buttons.
//
// Example:
//
// result := ctx.ShowOverlay(ext.OverlayConfig{
// Title: "Build Results",
// Content: ext.WidgetContent{Text: "All 42 tests passed."},
// Style: ext.OverlayStyle{BorderColor: "#a6e3a1"},
// Width: 60,
// Actions: []string{"Continue", "Show Details"},
// })
type OverlayConfig struct {
// Title is displayed at the top of the dialog. Empty means no title.
Title string
// Content describes what to render inside the dialog body. The Text
// field is required; set Markdown=true to render as styled markdown.
Content WidgetContent
// Style configures the appearance.
Style OverlayStyle
// Width is the dialog width in columns. 0 = 60% of terminal width.
// Clamped to [30, termWidth-4].
Width int
// MaxHeight limits the dialog height in lines. 0 = 80% of terminal
// height. Content exceeding this height becomes scrollable.
MaxHeight int
// Anchor determines vertical positioning. Default is "center".
Anchor OverlayAnchor
// Actions, if non-empty, shows selectable action buttons at the
// bottom of the dialog. The user navigates with left/right arrows
// and selects with Enter. The selected action's text and index are
// returned in OverlayResult.
//
// If empty, the dialog is a simple info panel dismissed with ESC
// or Enter (result.Cancelled=false, result.Action="", result.Index=-1).
Actions []string
}
// OverlayResult is the response from a ShowOverlay call.
type OverlayResult struct {
// Action is the text of the selected action, or "" if no actions
// were configured or the dialog was dismissed without selection.
Action string
// Index is the zero-based index of the selected action, or -1 if
// no action was selected.
Index int
// Cancelled is true if the user dismissed the dialog with ESC.
Cancelled bool
}
// ---------------------------------------------------------------------------
// ToolDef / CommandDef
// ---------------------------------------------------------------------------
// ToolDef describes a custom tool registered by an extension.
type ToolDef struct {
Name string
Description string
Parameters string // JSON Schema string
Execute func(input string) (string, error)
}
// CommandDef describes a slash command registered by an extension.
type CommandDef struct {
Name string
Description string
Execute func(args string, ctx Context) (string, error)
}
// ---------------------------------------------------------------------------
// Custom tool rendering (exposed to Yaegi — concrete structs)
// ---------------------------------------------------------------------------
// ToolRenderConfig provides custom rendering functions for a tool's display
// in the TUI. Extensions register tool renderers via API.RegisterToolRenderer()
// during Init. Both render functions are optional — if nil or if they return
// an empty string, the builtin renderer (or default) is used as a fallback.
//
// Example:
//
// api.RegisterToolRenderer(ext.ToolRenderConfig{
// ToolName: "my-tool",
// RenderHeader: func(toolArgs string, width int) string {
// // Parse args and return a compact summary for the header
// return "my-tool: doing something"
// },
// RenderBody: func(toolResult string, isError bool, width int) string {
// // Return custom formatted result body
// if isError {
// return "ERROR: " + toolResult
// }
// return "Result: " + toolResult
// },
// })
type ToolRenderConfig struct {
// ToolName is the name of the tool this renderer applies to. Must match
// the tool's registered name exactly (e.g. "bash", "read", "my-tool").
ToolName string
// DisplayName, if non-empty, replaces the auto-capitalized tool name
// shown in the header line (e.g. "Shell" instead of "Bash").
DisplayName string
// BorderColor, if non-empty, overrides the default border color for
// the tool result block. Accepts a hex color string (e.g. "#89b4fa").
// By default, the border is green for success and red for error.
BorderColor string
// Background, if non-empty, sets a background color for the entire
// tool result block. Accepts a hex color string (e.g. "#1e1e2e").
// By default, no background is applied.
Background string
// BodyMarkdown, when true, passes the RenderBody output through the
// glamour markdown renderer before display. This lets extensions return
// markdown-formatted text without needing access to Kit's internal
// rendering functions. Ignored when RenderBody is nil or returns empty.
BodyMarkdown bool
// RenderHeader, if non-nil, replaces the default parameter formatting
// in the tool header line. Receives the JSON-encoded arguments and the
// maximum width in columns. Return a short summary string for display
// after the tool name, or empty string to fall back to default formatting.
RenderHeader func(toolArgs string, width int) string
// RenderBody, if non-nil, replaces the default tool result body rendering.
// Receives the result text, error flag, and available width in columns.
// Return the full styled body content, or empty string to fall back to
// the builtin renderer (or default).
RenderBody func(toolResult string, isError bool, width int) string
}
// ---------------------------------------------------------------------------
// Editor interceptor types (exposed to Yaegi — concrete structs)
// ---------------------------------------------------------------------------
// EditorKeyActionType defines the outcome of an editor key interception.
type EditorKeyActionType string
const (
// EditorKeyPassthrough lets the built-in editor handle the key normally.
EditorKeyPassthrough EditorKeyActionType = "passthrough"
// EditorKeyConsumed means the extension handled the key. The editor
// should re-render but not process the key further.
EditorKeyConsumed EditorKeyActionType = "consumed"
// EditorKeyRemap transforms the key into a different key before passing
// it to the built-in editor. Use RemappedKey to specify the target
// (e.g., "left", "right", "up", "down", "backspace", "delete", "enter",
// "tab", "home", "end", or a single character like "a").
EditorKeyRemap EditorKeyActionType = "remap"
// EditorKeySubmit forces immediate text submission. The SubmitText field
// specifies the text to submit (empty = use editor's current text).
EditorKeySubmit EditorKeyActionType = "submit"
)
// EditorKeyAction is returned by an editor interceptor's HandleKey function
// to indicate how a key press should be handled.
type EditorKeyAction struct {
// Type determines the action taken.
Type EditorKeyActionType
// RemappedKey is the target key name for EditorKeyRemap. Must be a
// recognized key name (e.g., "left", "right", "up", "down", "backspace",
// "delete", "enter", "tab", "home", "end", "esc", "space", or a single
// printable character).
RemappedKey string
// SubmitText is the text to submit for EditorKeySubmit. If empty, the
// editor's current content is submitted instead.
SubmitText string
}
// EditorConfig defines an editor interceptor/decorator that wraps the built-in
// input editor. Extensions can intercept key events (remap, consume, or force
// submit) and/or modify the rendered output (add mode indicators, apply visual
// effects).
//
// This follows Pi's extension editor pattern (modal editor, rainbow editor)
// but uses concrete function fields instead of interfaces for Yaegi safety.
//
// IMPORTANT (Yaegi limitation): Function fields MUST be set using anonymous
// function literals (closures), NOT bare function references. Yaegi does not
// correctly propagate return values from named function references assigned to
// struct fields. Wrap any named function in a closure:
//
// // WRONG — Yaegi returns zero values:
// ctx.SetEditor(ext.EditorConfig{HandleKey: myHandler, Render: myRender})
//
// // CORRECT — closure wrapper works:
// ctx.SetEditor(ext.EditorConfig{
// HandleKey: func(k string, t string) ext.EditorKeyAction { return myHandler(k, t) },
// Render: func(w int, c string) string { return myRender(w, c) },
// })
type EditorConfig struct {
// HandleKey intercepts key presses before they reach the built-in editor.
// It receives the key name (e.g., "a", "enter", "ctrl+c", "backspace")
// and the editor's current text content. Return an EditorKeyAction to
// control how the key is handled.
//
// If nil, all keys pass through to the built-in editor unchanged.
HandleKey func(key string, currentText string) EditorKeyAction
// Render wraps the built-in editor's rendered output. It receives the
// available width and the default-rendered content (including title,
// textarea, popup, and help text). Return the modified content to display.
//
// If nil, the default rendering is used unchanged.
Render func(width int, defaultContent string) string
}
// ---------------------------------------------------------------------------
// Typed events (all concrete structs — safe for Yaegi)
// ---------------------------------------------------------------------------
// ToolCallEvent fires before a tool executes.
type ToolCallEvent struct {
ToolName string
ToolCallID string
Input string // JSON-encoded tool parameters
}
func (e ToolCallEvent) Type() EventType { return ToolCall }
// ToolCallResult controls whether the tool call proceeds.
type ToolCallResult struct {
Block bool
Reason string
}
func (ToolCallResult) isResult() {}
// ToolExecutionStartEvent fires when a tool begins executing.
type ToolExecutionStartEvent struct {
ToolName string
}
func (e ToolExecutionStartEvent) Type() EventType { return ToolExecutionStart }
// ToolExecutionEndEvent fires when a tool finishes executing.
type ToolExecutionEndEvent struct {
ToolName string
}
func (e ToolExecutionEndEvent) Type() EventType { return ToolExecutionEnd }
// ToolResultEvent fires after tool execution with the output.
type ToolResultEvent struct {
ToolName string
Input string
Content string
IsError bool
}
func (e ToolResultEvent) Type() EventType { return ToolResult }
// ToolResultResult can modify the tool's output before it reaches the LLM.
type ToolResultResult struct {
Content *string // nil = unchanged
IsError *bool // nil = unchanged
}
func (ToolResultResult) isResult() {}
// InputEvent fires when user input is received.
type InputEvent struct {
Text string
Source string // "interactive", "cli", "script", "queue"
}
func (e InputEvent) Type() EventType { return Input }
// InputResult controls what happens with user input.
//
// Action: "continue" (default), "transform", "handled"
type InputResult struct {
Action string
Text string // replacement text when Action="transform"
}
func (InputResult) isResult() {}
// BeforeAgentStartEvent fires before the agent loop begins.
type BeforeAgentStartEvent struct {
Prompt string
}
func (e BeforeAgentStartEvent) Type() EventType { return BeforeAgentStart }
// BeforeAgentStartResult can inject context before the agent runs.
type BeforeAgentStartResult struct {
InjectText *string
SystemPrompt *string
}
func (BeforeAgentStartResult) isResult() {}
// AgentStartEvent fires when the agent loop begins.
type AgentStartEvent struct {
Prompt string
}
func (e AgentStartEvent) Type() EventType { return AgentStart }
// AgentEndEvent fires when the agent finishes responding.
type AgentEndEvent struct {
Response string
StopReason string // "completed", "cancelled", "error"
}
func (e AgentEndEvent) Type() EventType { return AgentEnd }
// MessageStartEvent fires when a new assistant message begins.
type MessageStartEvent struct{}
func (e MessageStartEvent) Type() EventType { return MessageStart }
// MessageUpdateEvent fires for each streaming text chunk.
type MessageUpdateEvent struct {
Chunk string
}
func (e MessageUpdateEvent) Type() EventType { return MessageUpdate }
// MessageEndEvent fires when the assistant message is complete.
type MessageEndEvent struct {
Content string
}
func (e MessageEndEvent) Type() EventType { return MessageEnd }
// SessionStartEvent fires when a session is loaded or created.
type SessionStartEvent struct {
SessionID string
}
func (e SessionStartEvent) Type() EventType { return SessionStart }
// SessionShutdownEvent fires when the application is closing.
type SessionShutdownEvent struct{}
func (e SessionShutdownEvent) Type() EventType { return SessionShutdown }