Skip to content
Open
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
42 changes: 32 additions & 10 deletions bin/fm-composer-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -395,13 +395,13 @@ FM_COMPOSER_SHELL_PROMPT_GLYPHS=$(printf '%s\n' '>' '$' '%' '#')
# The ONE fleet-wide idle-placeholder set: composer text a harness renders in
# an EMPTY composer that a plain capture cannot tell from typed text. Grok's
# bordered placeholder and opencode's left-bar hint (which uses either three
# ASCII periods or U+2026 and continues with a rotating quoted suggestion,
# hence the unanchored tail). cursor-agent renders
# ASCII periods or U+2026 and may continue with one rotating quoted
# suggestion). cursor-agent renders
# two, both anchored: `Plan, search, build anything` in a fresh session and
# `Add a follow-up` once a turn has completed (verified live on cursor-agent
# 2026.08.11-e8db854). FM_COMPOSER_IDLE_RE overrides for an unverified harness;
# matching is case-insensitive.
FM_COMPOSER_IDLE_RE_DEFAULT='^Type a message\.\.\.$|^Ask anything(\.\.\.|…)|^Plan, search, build anything$|^Add a follow-up$'
FM_COMPOSER_IDLE_RE_DEFAULT='^Type a message\.\.\.$|^Ask anything(\.\.\.|…)([[:space:]]+"[^"]*")?$|^Plan, search, build anything$|^Add a follow-up$'

# Opencode draws a mode/model footer line INSIDE its left-bar composer
# ("Build · GPT-5.5 Fast OpenAI · high"). It is composer furniture, not typed
Expand Down Expand Up @@ -1065,29 +1065,47 @@ _fm_composer_classify_bare_wrap() { # <screen> <styled> <glyph-row> <cursor-row

# _fm_composer_classify_leftbar: opencode's left-bar composer. Blank rows and
# the idle hint read empty; the run's LAST row may be the mode/model footer
# (composer furniture, never typed text). Real content is pending when styling
# can prove it real, unknown otherwise.
# (composer furniture, never typed text). An exact idle-hint row is excused as
# furniture only at its structural placeholder position, and furniture alone
# cannot prove emptiness: a later blank row or ghost-stripped row must
# independently corroborate it. Real content is pending when styling can prove
# it real, unknown otherwise.
_fm_composer_classify_leftbar() { # <screen> <styled> <first-row> <last-row>
local screen=$1 styled=$2 first=$3 last=$4
local row raw content pending_seen=0 footer_re leading_blank=1 placeholder_position=0
local row raw content plain pending_seen=0 empty_seen=0 footer_re leading_blank=1 placeholder_position=0
local idle_re=${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT} furniture_re
furniture_re="^(${idle_re})$"
footer_re=${FM_COMPOSER_LEFTBAR_FOOTER_RE:-$FM_COMPOSER_LEFTBAR_FOOTER_RE_DEFAULT}
row=$first
while [ "$row" -le "$last" ]; do
raw=$(_fm_composer_screen_row "$row" "$screen")
content=$(_fm_composer_row_content "$raw" "$styled")
plain=$(_fm_composer_row_content "$raw" 0)
case "$content" in
'┃'*) content=${content#┃} ;;
esac
case "$plain" in
'┃'*) plain=${plain#┃} ;;
esac
fm_composer_normalize_trim_var content
if [ -z "$content" ]; then row=$((row + 1)); continue; fi
fm_composer_normalize_trim_var plain
if [ -z "$content" ]; then
if [ -n "$plain" ]; then
empty_seen=1
leading_blank=0
elif [ "$leading_blank" = 0 ]; then
empty_seen=1
fi
row=$((row + 1)); continue
fi
if [ "$leading_blank" = 1 ] && [ "$row" -gt "$first" ]; then
placeholder_position=1
else
placeholder_position=0
fi
leading_blank=0
if [ "$placeholder_position" = 1 ] \
&& fm_composer_idle_matches "$content" "${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}" insensitive; then
&& fm_composer_idle_matches "$content" "$furniture_re" insensitive; then
row=$((row + 1)); continue
fi
if [ "$row" -eq "$last" ] \
Expand All @@ -1099,8 +1117,10 @@ _fm_composer_classify_leftbar() { # <screen> <styled> <first-row> <last-row>
done
if [ "$pending_seen" = 1 ]; then
if [ "$styled" = 1 ]; then printf 'pending'; else printf 'unknown'; fi
else
elif [ "$empty_seen" = 1 ]; then
printf 'empty'
else
printf 'unknown'
fi
}

Expand Down Expand Up @@ -1201,6 +1221,8 @@ _fm_composer_select_cursorless() {
fm_composer_extract_selected_content() { # <caps> <screen>
local caps=$1 screen=$2 styled=0 kv plain row raw content glyph joined='' footer_re prompt_row=-1
local leading_blank=1 placeholder_position=0 prompt_is_shell=0
local idle_re=${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT} furniture_re
furniture_re="^(${idle_re})$"
footer_re=${FM_COMPOSER_LEFTBAR_FOOTER_RE:-$FM_COMPOSER_LEFTBAR_FOOTER_RE_DEFAULT}
while IFS= read -r kv; do
[ "$kv" = styled=1 ] && styled=1
Expand Down Expand Up @@ -1261,7 +1283,7 @@ EOF
|| { { [ "$FM_COMPOSER_SELECTED_KIND" = leftbar ] \
|| { [ "$FM_COMPOSER_SELECTED_KIND" = box ] && [ "$prompt_is_shell" = 1 ]; }; } \
&& [ "$placeholder_position" = 1 ] \
&& fm_composer_idle_matches "$content" "${FM_COMPOSER_IDLE_RE:-$FM_COMPOSER_IDLE_RE_DEFAULT}" insensitive; } \
&& fm_composer_idle_matches "$content" "$furniture_re" insensitive; } \
|| { [ "$FM_COMPOSER_SELECTED_KIND" = leftbar ] \
&& [ "$row" -eq "$FM_COMPOSER_SELECTED_LAST" ] \
&& fm_composer_idle_matches "$content" "$footer_re" sensitive; }; then
Expand Down
10 changes: 9 additions & 1 deletion tests/fm-composer-lib.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ test_matrix_opencode_leftbar_signals() {
# blanks, and a Build-mode footer. Two independent idle signals: the shared
# idle-placeholder pattern (works on plain captures) and the ghost strip
# (works on styled captures even if the pattern is overridden away).
local screen typed dim_screen captured_idle captured_pending out
local screen typed dim_screen captured_idle captured_pending placeholder_prefixed furniture_only out
screen=$' ┃\n ┃ Ask anything... "What is the tech stack?"\n ┃\n ┃ Build · GPT-5.5 Fast OpenAI · high\n ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀'
dim_screen=$' ┃\n ┃ '"${ESC}[2mAsk anything...${ESC}[0m"$'\n ┃\n ┃ Build · GPT-5.5 Fast OpenAI · high\n ╹▀▀▀▀'
assert_screen "opencode idle on tmux (cursor on hint)" empty "$CAPS_TMUX" "$dim_screen" 1
Expand All @@ -410,6 +410,10 @@ test_matrix_opencode_leftbar_signals() {
assert_screen "opencode 1.18.30 completed-turn idle hint on tmux" empty "$CAPS_TMUX" "$captured_idle" 3
captured_pending=$' ▣ Build · Big Pickle · 3.4s\n\n ┃\n ┃ '"${ESC}[38;2;255;255;255mReply with OK.${ESC}[38;2;255;255;255m"$'\n ┃\n ┃ Build · Big Pickle OpenCode Zen\n ╹▀▀▀▀▀▀▀▀'
assert_screen "opencode 1.18.30 completed-turn typed composer on tmux" pending "$CAPS_TMUX" "$captured_pending" 3
placeholder_prefixed=$' ▣ Build · Big Pickle · 3.4s\n\n ┃\n ┃ '"${ESC}[38;2;255;255;255mAsk anything... please reply${ESC}[38;2;255;255;255m"$'\n ┃\n ┃ Build · Big Pickle OpenCode Zen\n ╹▀▀▀▀▀▀▀▀'
assert_screen "opencode 1.18.30 placeholder-prefixed draft on tmux" pending "$CAPS_TMUX" "$placeholder_prefixed" 3
furniture_only=$' ┃\n ┃ Ask anything...\n ┃ Build · Big Pickle OpenCode Zen\n ╹▀▀▀▀▀▀▀▀'
assert_screen "opencode furniture-only left-bar lacks empty proof" unknown "$CAPS_TMUX" "$furniture_only" 1
# Signal separation: with the idle pattern overridden to something that
# cannot match, a DIM-styled hint still proves empty through the ghost strip.
out=$(FM_COMPOSER_IDLE_RE='^NEVER-MATCHES$' fm_composer_classify_screen "$CAPS_TMUX" "$dim_screen" 1)
Expand Down Expand Up @@ -647,6 +651,10 @@ test_selected_content_is_composer_scoped_and_wrap_normalized() {
out=$(fm_composer_extract_selected_content "$CAPS_STYLED_NOID" "$screen")
[ "$out" = 'hello captain' ] \
|| fail "left-bar extraction should join user rows without footer furniture, got '$out'"
screen=$'┃\n┃ Ask anything... please reply\n┃\n┃ Build · GPT-5.5 Fast OpenAI · high'
out=$(fm_composer_extract_selected_content "$CAPS_STYLED_NOID" "$screen")
[ "$out" = 'Ask anything... please reply' ] \
|| fail "left-bar extraction should preserve placeholder-prefixed user input, got '$out'"
screen=$'╭────────────────────╮\n│ ❯ '"${ESC}[2mType a message...${ESC}[0m"$'│\n╰────────────────────╯'
out=$(fm_composer_extract_selected_content "$CAPS_STYLED_NOID" "$screen")
[ -z "$out" ] \
Expand Down
Loading