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
- func ComposerDraft(content string) (draft string, composerVisible bool)
- func ComposerHasDraft(content string) bool
- func CurrentComposerPrompt(content string) (string, bool)
- func HasCurrentComposerPrompt(content string) bool
- func HasUnsentComposerPrompt(content, message string) bool
- func HasUnsentPastedPrompt(content string) bool
- func IsComposerDividerLine(line string) bool
- func IsComposerPlaceholder(text string) bool
- func NormalizePromptText(s string) string
- func ParsePromptFromComposerBlock(lines []string) (string, bool)
- func StatusIsBusy(status string) bool
- func WaitForAgentReady(target AgentReadyChecker, tool string, timeout time.Duration, ...) error
- func WaitUntilNotBusy(fetchStatus func() (string, error), timeout, poll time.Duration, ...) error
- type AgentReadyChecker
- type CodexApprovalOptions
- type CodexApprovalResult
- type CodexApprovalTarget
- type ComposerGuardOptions
- type ComposerGuardResult
- type ComposerGuardTarget
- type PromptGates
Constants ¶
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.
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
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
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 ¶
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 ¶
HasCurrentComposerPrompt returns true if a composer prompt is visible in the terminal pane content.
func HasUnsentComposerPrompt ¶
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 ¶
HasUnsentPastedPrompt detects Claude's composer marker for a pasted-but-unsent prompt. Example: "[Pasted text #1 +89 lines]".
func IsComposerDividerLine ¶
IsComposerDividerLine detects composer divider lines made of dash characters. Requires at least 10 consecutive dash-like characters (─, -, ━).
func IsComposerPlaceholder ¶ added in v1.9.58
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 ¶
NormalizePromptText normalizes whitespace in prompt text by replacing NBSP with regular spaces, trimming, and collapsing multiple whitespace runs.
func ParsePromptFromComposerBlock ¶
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
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
AgentReadyChecker abstracts the tmux surface readiness polling needs. *tmux.Session satisfies this interface.
type CodexApprovalOptions ¶ added in v1.10.9
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
func GuardComposerDraft(t ComposerGuardTarget, opts ComposerGuardOptions) ComposerGuardResult
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:
- Holds (bounded by HoldWait) while the composer shows a non-empty operator draft, polling for it to clear on its own.
- 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
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).