send

package
v1.10.10 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package send consolidates prompt detection and send verification functions used by both the CLI send path (session_cmd.go) and the Instance send path (instance.go). Having a single source of truth prevents fix divergence.

Index

Constants

View Source
const DefaultAgentReadyTimeout = 10 * time.Minute

DefaultAgentReadyTimeout is the readiness budget for launch / StartWithMessage. Matches `session send --timeout` default so cold-start TUIs (Cursor, Claude+MCP) are not cut off early.

View Source
const DeferPollInterval = 2 * time.Second

DeferPollInterval is the fixed cadence at which `session send --defer-if-busy` re-checks the target's hook-driven status while holding delivery.

Variables

This section is empty.

Functions

func ComposerDraft added in v1.9.58

func ComposerDraft(content string) (draft string, composerVisible bool)

ComposerDraft returns the normalized text currently sitting in the visible composer and whether a composer is visible at all. Claude's idle-suggestion placeholder is reported as an empty draft. Callers must pass ANSI-stripped pane content (same contract as CurrentComposerPrompt).

func ComposerHasDraft added in v1.9.58

func ComposerHasDraft(content string) bool

ComposerHasDraft reports whether the visible composer holds operator input. This is the shared "is the composer busy?" check automated senders must run before injecting keystrokes into the pane (issue #1409).

func CurrentComposerPrompt

func CurrentComposerPrompt(content string) (string, bool)

CurrentComposerPrompt extracts the current prompt text from the composer region at the bottom of the terminal pane. It searches for the last two divider lines and parses the prompt between them, with a fallback for layouts without dividers.

func HasCurrentComposerPrompt

func HasCurrentComposerPrompt(content string) bool

HasCurrentComposerPrompt returns true if a composer prompt is visible in the terminal pane content.

func HasUnsentComposerPrompt

func HasUnsentComposerPrompt(content, message string) bool

HasUnsentComposerPrompt detects when the message text is still present in the interactive input line (e.g., "❯ message"), which indicates Enter was not accepted yet even if no "[Pasted text ...]" marker is shown.

func HasUnsentPastedPrompt

func HasUnsentPastedPrompt(content string) bool

HasUnsentPastedPrompt detects Claude's composer marker for a pasted-but-unsent prompt. Example: "[Pasted text #1 +89 lines]".

func IsComposerDividerLine

func IsComposerDividerLine(line string) bool

IsComposerDividerLine detects composer divider lines made of dash characters. Requires at least 10 consecutive dash-like characters (─, -, ━).

func IsComposerPlaceholder added in v1.9.58

func IsComposerPlaceholder(text string) bool

IsComposerPlaceholder reports whether the visible composer text is Claude's idle-suggestion placeholder rather than operator input. Claude renders hint suggestions in the empty composer, e.g.:

❯ Try "write a test for <filepath>"

Treating these as operator drafts would make every automated send hold and Ctrl+C an actually-empty composer (issue #1409).

func NormalizePromptText

func NormalizePromptText(s string) string

NormalizePromptText normalizes whitespace in prompt text by replacing NBSP with regular spaces, trimming, and collapsing multiple whitespace runs.

func ParsePromptFromComposerBlock

func ParsePromptFromComposerBlock(lines []string) (string, bool)

ParsePromptFromComposerBlock parses the prompt text from a composer block (the lines between two divider lines). It looks for a prompt marker (❯ or ›) and collects any wrapped continuation lines.

func StatusIsBusy added in v1.10.9

func StatusIsBusy(status string) bool

StatusIsBusy reports whether a hook-driven status string represents a target that is mid-turn (actively generating) or still booting. These are the only states `--defer-if-busy` holds for; every other state (waiting/idle/error/stopped/queued/unknown) means the composer is free and delivery may proceed.

This keys off the SAME hook-driven signal `agent-deck list --json` reports (Claude's UserPromptSubmit hook writes "running", the Stop hook writes "waiting"), which is a true turn-finished edge — unlike WaitForAgentReady's pane-content-diff heuristic that false-positives to idle during tool calls and thinking pauses (issue #1578).

func WaitForAgentReady added in v1.9.70

func WaitForAgentReady(target AgentReadyChecker, tool string, timeout time.Duration, gates PromptGates) error

WaitForAgentReady waits until the agent accepts keyboard input.

Primary path: GetStatus active → waiting/idle transition (same as the TUI). Secondary path (#cursor-launch): during tmux's startup window GetStatus can return "starting" without re-capturing the pane when window_activity is flat, even though the tool prompt is already on screen and the user can type. When status is "starting", probe CapturePaneFresh directly for a tool prompt.

func WaitUntilNotBusy added in v1.10.9

func WaitUntilNotBusy(fetchStatus func() (string, error), timeout, poll time.Duration, sleep func(time.Duration)) error

WaitUntilNotBusy polls fetchStatus until the target is no longer busy (see StatusIsBusy), the timeout elapses, or the fetch fails persistently.

It is the core of `session send --defer-if-busy`: rather than interrupting a generating target with the composer-draft Ctrl+C guard (GuardComposerDraft, issue #1409), the sender holds until the target's hook-driven status shows the turn is over, then lets the normal readiness/guard/send pipeline run.

sleep is injected so tests run without real delay; production passes time.Sleep. A nil sleep defaults to time.Sleep; a non-positive poll defaults to DeferPollInterval. On timeout the message is NOT delivered and a non-zero error is returned (drop-on-timeout semantics).

Types

type AgentReadyChecker added in v1.9.70

type AgentReadyChecker interface {
	GetStatus() (string, error)
	CapturePaneFresh() (string, error)
}

AgentReadyChecker abstracts the tmux surface readiness polling needs. *tmux.Session satisfies this interface.

type CodexApprovalOptions added in v1.10.9

type CodexApprovalOptions struct {
	VerifyTimeout time.Duration
	PollInterval  time.Duration
}

CodexApprovalOptions controls the bounded post-key verification.

type CodexApprovalResult added in v1.10.9

type CodexApprovalResult struct {
	Choice         string
	OptionNumber   int
	OptionLabel    string
	KeySent        bool
	Verified       bool
	NextPromptSeen bool
}

CodexApprovalResult describes the option selected and whether the original approval overlay was observed disappearing.

func ApproveCodexPrompt added in v1.10.9

func ApproveCodexPrompt(target CodexApprovalTarget, choice string, opts CodexApprovalOptions) (CodexApprovalResult, error)

ApproveCodexPrompt resolves one currently visible Codex approval menu.

choice accepts a displayed option number, or one of "once", "always", and "session". The displayed number is sent as one literal keypress; Enter is intentionally not sent because Codex selects numbered approval options on the digit KeyEvent itself.

type CodexApprovalTarget added in v1.10.9

type CodexApprovalTarget interface {
	CapturePaneFresh() (string, error)
	SendNamedKey(string) error
}

CodexApprovalTarget is the minimum tmux surface needed to safely resolve a Codex approval overlay. SendNamedKey emits one tmux key event without an implicit Enter or message/paste semantics.

type ComposerGuardOptions added in v1.9.58

type ComposerGuardOptions struct {
	// HoldWait is the maximum time to wait for an operator draft to clear on
	// its own (operator submits or erases it) before falling back to
	// save-clear-restore.
	HoldWait time.Duration
	// PollInterval is the capture cadence during the hold phase.
	// Defaults to 250ms when <= 0.
	PollInterval time.Duration
	// ClearWait is the maximum time to wait, per Ctrl+C attempt, for the
	// composer to actually clear.
	ClearWait time.Duration
	// Strip is applied to raw captured pane content before composer
	// introspection (pass tmux.StripANSI). nil means identity.
	Strip func(string) string
}

ComposerGuardOptions tunes GuardComposerDraft. All bounds are mandatory so the guard can never hold a delivery indefinitely.

type ComposerGuardResult added in v1.9.58

type ComposerGuardResult struct {
	// Held is the total wall-clock time the guard spent before returning.
	Held time.Duration
	// SavedDraft is the operator draft that was cleared to make way for the
	// automated send. Empty when the composer was empty or cleared on its
	// own. Callers must restore it (type it back, without Enter) after the
	// automated delivery is confirmed.
	SavedDraft string
	// DraftCleared is true when the guard issued Ctrl+C and confirmed the
	// composer emptied.
	DraftCleared bool
	// ClearFailed is true when Ctrl+C attempts were exhausted and the
	// composer still held the draft. The caller proceeds with the send
	// regardless (delivery must not be dropped), accepting the residual
	// merge risk for this pathological case.
	ClearFailed bool
}

ComposerGuardResult reports what the guard did.

func GuardComposerDraft added in v1.9.58

GuardComposerDraft implements the composer-collision guard for automated sends (issue #1409): an automated SendKeysAndEnter against a composer that already holds half-typed operator input would merge with it and submit the merged prompt. The guard:

  1. Holds (bounded by HoldWait) while the composer shows a non-empty operator draft, polling for it to clear on its own.
  2. If the draft is still present at the bound, saves it, clears the composer with Ctrl+C (Claude clears the current input on a single Ctrl+C; same primitive the full-resend recovery path already uses) and confirms the clear, bounded by ClearWait per attempt.

The guard never blocks delivery indefinitely and never errors: on capture failures or a composer that refuses to clear it returns and lets the caller proceed, because watchers/conductors depend on the send going through.

type ComposerGuardTarget added in v1.9.58

type ComposerGuardTarget interface {
	CapturePaneFresh() (string, error)
	SendCtrlC() error
}

ComposerGuardTarget is the minimal pane surface GuardComposerDraft needs to hold an automated send while an operator draft occupies the composer. *tmux.Session satisfies it.

type PromptGates added in v1.9.70

type PromptGates struct {
	ClaudeComposer bool // IsClaudeCompatible tools: require ❯ composer line
	CodexPrompt    bool // IsCodexCompatible tools: require codex>/› prompt
}

PromptGates enables tool-specific prompt visibility checks once GetStatus reports waiting/idle (or during the startup-window prompt probe below).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL