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
12 changes: 10 additions & 2 deletions compat/testdata/config/default-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,21 @@ rules = [
# separated. How many segments sit between them is the dialog's own business
# -- a multi-question one adds "n to add notes" and "Tab to switch questions"
# -- so anchor both ends and let the middle be any number of segments.
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$" },
# A pane narrower than the legend wraps it at any space ("… · Esc" over "to
# cancel" at 40 columns; the "ctrl+g to edit in nano" segment a focused
# free-text row adds wraps it below 76), so a line break stands in for any
# space and each segment may wrap once. Missing the wrapped legend reads
# the dialog as idle, and the next queued message's Enter ticks a box.
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Enter(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)select(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)(?:[^\\x{B7}\\n]+(?:\\n[^\\x{B7}\\n]*)?\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n))*Esc(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)cancel[ \\x{A0}]*$" },
# The review page a stepper dialog ends on: the answers listed, "Ready to
# submit your answers?", then "❯ 1. Submit answers / 2. Cancel" -- and no
# legend at all, so the rule above cannot see it, and the marker row is the
# activity cutoff so the numbered rule cannot either. The question line sits
# above the cutoff and inside the newest turn, which is where rules look.
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$" },
# Below 29 columns the question wraps, and missing it there lets a queued
# message's Enter press "Submit answers", so a break may stand in for any
# space here too.
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Ready(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)submit(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)your(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)answers\\?[ \\x{A0}]*$" },
# spinner row of an active turn, any duration format:
# "✳ Drizzling… (6s · thinking)" / "✽ Zigzagging… (3m 18s · ↓ 1.4k tokens)"
{ state = "working", pattern = "(?m)^[✻✳✶✽✢·✦✧+*] \\S+… \\(" },
Expand Down
4 changes: 2 additions & 2 deletions compat/testdata/config/effective-absent.golden
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ file tools.claude.rules[0].pattern = "(?m)^[ \\x{A0}\\x{2502}|]*(?:[^\\n]*[,
file tools.claude.rules[0].state = "waiting"
file tools.claude.rules[1].pattern = "(?m)^[ \\x{A0}]*❯[ \\x{A0}]+\\d+\\."
file tools.claude.rules[1].state = "waiting"
file tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$"
file tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Enter(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)select(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)(?:[^\\x{B7}\\n]+(?:\\n[^\\x{B7}\\n]*)?\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n))*Esc(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)cancel[ \\x{A0}]*$"
file tools.claude.rules[2].state = "waiting"
file tools.claude.rules[3].pattern = "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$"
file tools.claude.rules[3].pattern = "(?m)^[ \\x{A0}]*Ready(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)submit(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)your(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)answers\\?[ \\x{A0}]*$"
file tools.claude.rules[3].state = "waiting"
file tools.claude.rules[4].pattern = "(?m)^[✻✳✶✽✢·✦✧+*] \\S+… \\("
file tools.claude.rules[4].state = "working"
Expand Down
4 changes: 2 additions & 2 deletions compat/testdata/config/effective-empty.golden
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ default tools.claude.rules[0].pattern = "(?m)^[ \\x{A0}\\x{2502}|]*(?:[^\\n]*[,
default tools.claude.rules[0].state = "waiting"
default tools.claude.rules[1].pattern = "(?m)^[ \\x{A0}]*❯[ \\x{A0}]+\\d+\\."
default tools.claude.rules[1].state = "waiting"
default tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$"
default tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Enter(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)select(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)(?:[^\\x{B7}\\n]+(?:\\n[^\\x{B7}\\n]*)?\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n))*Esc(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)cancel[ \\x{A0}]*$"
default tools.claude.rules[2].state = "waiting"
default tools.claude.rules[3].pattern = "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$"
default tools.claude.rules[3].pattern = "(?m)^[ \\x{A0}]*Ready(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)submit(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)your(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)answers\\?[ \\x{A0}]*$"
default tools.claude.rules[3].state = "waiting"
default tools.claude.rules[4].pattern = "(?m)^[✻✳✶✽✢·✦✧+*] \\S+… \\("
default tools.claude.rules[4].state = "working"
Expand Down
4 changes: 2 additions & 2 deletions compat/testdata/config/effective-full.golden
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ default tools.claude.rules[0].pattern = "(?m)^[ \\x{A0}\\x{2502}|]*(?:[^\\n]*[,
default tools.claude.rules[0].state = "waiting"
default tools.claude.rules[1].pattern = "(?m)^[ \\x{A0}]*❯[ \\x{A0}]+\\d+\\."
default tools.claude.rules[1].state = "waiting"
default tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$"
default tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Enter(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)select(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)(?:[^\\x{B7}\\n]+(?:\\n[^\\x{B7}\\n]*)?\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n))*Esc(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)cancel[ \\x{A0}]*$"
default tools.claude.rules[2].state = "waiting"
default tools.claude.rules[3].pattern = "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$"
default tools.claude.rules[3].pattern = "(?m)^[ \\x{A0}]*Ready(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)submit(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)your(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)answers\\?[ \\x{A0}]*$"
default tools.claude.rules[3].state = "waiting"
default tools.claude.rules[4].pattern = "(?m)^[✻✳✶✽✢·✦✧+*] \\S+… \\("
default tools.claude.rules[4].state = "working"
Expand Down
4 changes: 2 additions & 2 deletions compat/testdata/config/effective-minimal.golden
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ default tools.claude.rules[0].pattern = "(?m)^[ \\x{A0}\\x{2502}|]*(?:[^\\n]*[,
default tools.claude.rules[0].state = "waiting"
default tools.claude.rules[1].pattern = "(?m)^[ \\x{A0}]*❯[ \\x{A0}]+\\d+\\."
default tools.claude.rules[1].state = "waiting"
default tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$"
default tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Enter(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)select(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)(?:[^\\x{B7}\\n]+(?:\\n[^\\x{B7}\\n]*)?\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n))*Esc(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)cancel[ \\x{A0}]*$"
default tools.claude.rules[2].state = "waiting"
default tools.claude.rules[3].pattern = "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$"
default tools.claude.rules[3].pattern = "(?m)^[ \\x{A0}]*Ready(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)submit(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)your(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)answers\\?[ \\x{A0}]*$"
default tools.claude.rules[3].state = "waiting"
default tools.claude.rules[4].pattern = "(?m)^[✻✳✶✽✢·✦✧+*] \\S+… \\("
default tools.claude.rules[4].state = "working"
Expand Down
4 changes: 2 additions & 2 deletions compat/testdata/config/effective-old.golden
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ file tools.claude.revive_command = "claude --continue"
file* tools.claude.rules[0].pattern = "(?m)^[ \\x{A0}\\x{2502}|]*(?:[^\\n]*[,\\x{B7}][ \\x{A0}]*)?Enter to confirm(?:[ \\x{A0}]*[,\\x{B7}][^\\n]*)?[ \\x{A0}\\x{2502}|]*$"
(file: "Enter to confirm")
file tools.claude.rules[0].state = "waiting"
file* tools.claude.rules[1].pattern = "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$"
file* tools.claude.rules[1].pattern = "(?m)^[ \\x{A0}]*Enter(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)select(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)(?:[^\\x{B7}\\n]+(?:\\n[^\\x{B7}\\n]*)?\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n))*Esc(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)cancel[ \\x{A0}]*$"
(file: "esc to interrupt")
file* tools.claude.rules[1].state = "waiting"
(file: "working")
default tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$"
default tools.claude.rules[2].pattern = "(?m)^[ \\x{A0}]*Ready(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)submit(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)your(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)answers\\?[ \\x{A0}]*$"
default tools.claude.rules[2].state = "waiting"
default tools.claude.rules[3].pattern = "esc to interrupt"
default tools.claude.rules[3].state = "working"
Expand Down
4 changes: 2 additions & 2 deletions compat/testdata/status/patterns.golden
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ echo_budget = "0s"
interrupt_keys = ["Escape"]
rules[0] waiting "(?m)^[ \\x{A0}\\x{2502}|]*(?:[^\\n]*[,\\x{B7}][ \\x{A0}]*)?Enter to confirm(?:[ \\x{A0}]*[,\\x{B7}][^\\n]*)?[ \\x{A0}\\x{2502}|]*$"
rules[1] waiting "(?m)^[ \\x{A0}]*❯[ \\x{A0}]+\\d+\\."
rules[2] waiting "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$"
rules[3] waiting "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$"
rules[2] waiting "(?m)^[ \\x{A0}]*Enter(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)select(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)(?:[^\\x{B7}\\n]+(?:\\n[^\\x{B7}\\n]*)?\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n))*Esc(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)cancel[ \\x{A0}]*$"
rules[3] waiting "(?m)^[ \\x{A0}]*Ready(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)submit(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)your(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)answers\\?[ \\x{A0}]*$"
rules[4] working "(?m)^[✻✳✶✽✢·✦✧+*] \\S+… \\("
rules[5] working "esc to interrupt"
rules[6] errored "(?im)^\\s*error:"
Expand Down
56 changes: 54 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,15 @@ const busyLineWaitOnly = `^[✻✳✶✽✢·✦✧+*] Waiting for \d+ (?:backgr
// current pattern; one edited by hand keeps what its author wrote.
const waitingEnterToConfirmBare = `Enter to confirm`

// waitingAskLegendOneLine and waitingAskReviewOneLine are the AskUserQuestion
// patterns claude shipped with before they read a line the pane wraps. A
// config carrying either verbatim takes the current pattern, the way one
// carrying the bare phrase above does.
const (
waitingAskLegendOneLine = `(?m)^[ \x{A0}]*Enter to select \x{B7} (?:[^\x{B7}\n]+\x{B7} )*Esc to cancel[ \x{A0}]*$`
waitingAskReviewOneLine = `(?m)^[ \x{A0}]*Ready to submit your answers\?[ \x{A0}]*$`
)

// codexTurnEndRuleOnly, codexChromeLineMCPOnly and codexWorkingNoHints are
// the codex patterns shipped before codex 0.154 moved the turn end to a dim
// label under the reply and 0.157 parked hint rows above the composer. A
Expand Down Expand Up @@ -651,6 +660,7 @@ func mergeTool(name string, user, def Tool) Tool {
user.Rules = def.Rules
} else if name == "claude" {
upgradeBareEnterToConfirm(user.Rules, def.Rules)
upgradeOneLineAskRules(user.Rules, def.Rules)
user.Rules = withAskUserQuestionRules(user.Rules, def.Rules)
} else if name == "opencode" {
user.Rules = withDialogRules(user.Rules, def.Rules, opencodeDialogSamples)
Expand Down Expand Up @@ -681,6 +691,20 @@ func mergeTool(name string, user, def Tool) Tool {
const askUserQuestionLegend = "Enter to select \u00b7 \u2191/\u2193 to navigate \u00b7 Esc to cancel"
const askUserQuestionReview = "Ready to submit your answers?"

// The legend as a 40-column pane draws it, and the review question as a
// 24-column one does.
const (
askUserQuestionLegendWrapped = "Enter to select \u00b7 \u2191/\u2193 to navigate \u00b7 Esc\nto cancel"
askUserQuestionReviewWrapped = "Ready to submit your\nanswers?"
)

// oneLineAskRules maps each superseded one-line pattern to the wrapped line
// its replacement has to read.
var oneLineAskRules = map[string]string{
waitingAskLegendOneLine: askUserQuestionLegendWrapped,
waitingAskReviewOneLine: askUserQuestionReviewWrapped,
}

var askUserQuestionSamples = []string{askUserQuestionLegend, askUserQuestionReview}

// upgradeBareEnterToConfirm swaps the superseded bare phrase for whichever
Expand All @@ -701,6 +725,26 @@ func upgradeBareEnterToConfirm(user, def []Rule) {
}
}

// upgradeOneLineAskRules swaps each superseded one-line pattern for the
// default rule that reads the same line wrapped, in place.
func upgradeOneLineAskRules(user, def []Rule) {
for i, r := range user {
wrapped, superseded := oneLineAskRules[r.Pattern]
if r.State != "waiting" || !superseded {
continue
}
for _, d := range def {
if d.State != "waiting" {
continue
}
if re, err := regexp.Compile(d.Pattern); err == nil && re.MatchString(wrapped) {
user[i] = d
break
}
}
}
}

// codexDialogSamples is the legend of codex's first-run folder-trust dialog,
// recognized after the tool block shipped.
var codexDialogSamples = []string{
Expand Down Expand Up @@ -1092,13 +1136,21 @@ rules = [
# separated. How many segments sit between them is the dialog's own business
# -- a multi-question one adds "n to add notes" and "Tab to switch questions"
# -- so anchor both ends and let the middle be any number of segments.
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$" },
# A pane narrower than the legend wraps it at any space ("… · Esc" over "to
# cancel" at 40 columns; the "ctrl+g to edit in nano" segment a focused
# free-text row adds wraps it below 76), so a line break stands in for any
# space and each segment may wrap once. Missing the wrapped legend reads
# the dialog as idle, and the next queued message's Enter ticks a box.
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Enter(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)select(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)(?:[^\\x{B7}\\n]+(?:\\n[^\\x{B7}\\n]*)?\\x{B7}(?:[ \\x{A0}]+|[ \\x{A0}]*\\n))*Esc(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)cancel[ \\x{A0}]*$" },
# The review page a stepper dialog ends on: the answers listed, "Ready to
# submit your answers?", then "❯ 1. Submit answers / 2. Cancel" -- and no
# legend at all, so the rule above cannot see it, and the marker row is the
# activity cutoff so the numbered rule cannot either. The question line sits
# above the cutoff and inside the newest turn, which is where rules look.
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$" },
# Below 29 columns the question wraps, and missing it there lets a queued
# message's Enter press "Submit answers", so a break may stand in for any
# space here too.
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Ready(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)to(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)submit(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)your(?:[ \\x{A0}]+|[ \\x{A0}]*\\n)answers\\?[ \\x{A0}]*$" },
# spinner row of an active turn, any duration format:
# "✳ Drizzling… (6s · thinking)" / "✽ Zigzagging… (3m 18s · ↓ 1.4k tokens)"
{ state = "working", pattern = "(?m)^[✻✳✶✽✢·✦✧+*] \\S+… \\(" },
Expand Down
47 changes: 47 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,53 @@ rules = [
}
}

// A config written before the question legend and review page were read
// wrapped carries their one-line patterns verbatim, and keeps reading a narrow
// question as idle unless the load swaps each for the current one, in its own
// position.
func TestLoadDirUpgradesTheOneLineQuestionRules(t *testing.T) {
dir := t.TempDir()
legacy := `
[tools.claude]
command = "claude"
rules = [
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Enter to select \\x{B7} (?:[^\\x{B7}\\n]+\\x{B7} )*Esc to cancel[ \\x{A0}]*$" },
{ state = "waiting", pattern = "(?m)^[ \\x{A0}]*Ready to submit your answers\\?[ \\x{A0}]*$" },
{ state = "working", pattern = "esc to interrupt" },
]
`
if err := os.WriteFile(filepath.Join(dir, "config.toml"), []byte(legacy), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := LoadDir(dir)
if err != nil {
t.Fatalf("LoadDir: %v", err)
}
rules := cfg.Tools["claude"].Rules
if len(rules) == 0 || rules[0].State != "waiting" || rules[len(rules)-1].State != "working" {
t.Fatalf("claude rules lost their shape: %+v", rules)
}
for _, r := range rules {
if _, superseded := oneLineAskRules[r.Pattern]; superseded {
t.Fatalf("one-line pattern %q survived the upgrade", r.Pattern)
}
}
for i, lines := range [][]string{
{askUserQuestionLegend, askUserQuestionLegendWrapped},
{askUserQuestionReview, askUserQuestionReviewWrapped},
} {
re, err := regexp.Compile(rules[i].Pattern)
if err != nil {
t.Fatalf("compile upgraded pattern: %v", err)
}
for _, line := range lines {
if !re.MatchString(line) {
t.Fatalf("upgraded pattern %q does not read %q", rules[i].Pattern, line)
}
}
}
}

// A config written before either opencode overlay was recognized carries a
// rules array with no waiting rule at all, so both a permission ask and an
// agent's select prompt read working off the spinner row that sits above the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
tool except
AskUserQuestion.
Immediately call
AskUserQuestion
ONCE with THREE
questions: (1)
header Color,
single-select,
question "Pick a
color?", options
Red, Blue; (2)
header Checks,
multiSelect true,
question "Which
checks?", options
Build, Lint,
Tests; (3) header
Size,
single-select,
question "Pick a
size?", options
Small, Large.
Short
descriptions.
Then wait.
────────────────────
← ☐ ☐ ☐ ✔
C… … S… Submit →

Which checks?

❯ 1. [ ] Build
Compile the
project
2. [ ] Lint
Run the
linter
3. [ ] Tests
Run the
test suite
4. [ ] Type
something
Next
────────────────────
5. Chat about this

Enter to select ·
Tab/Arrow keys to
navigate · Esc to
cancel
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
▐▛███▛█ Haiku 5.5
▝▜██████▀ Claude Max
▝▝ ▝▝ ~/repos/sampl
e-repo


❯ Do not use any tool
except
AskUserQuestion.
Immediately call
AskUserQuestion ONCE
with THREE questions:
(1) header Color,
single-select,
question "Pick a
color?", options Red,
Blue; (2) header
Checks, multiSelect
true, question "Which
checks?", options
Build, Lint, Tests;
(3) header Size,
single-select,
question "Pick a
size?", options
Small, Large. Short
descriptions. Then
wait.

────────────────────────
← ☐ ☐ C… ☐ ✔
C… S… Submit →

Review your answers

⚠ You have not answered
all questions

Ready to submit your
answers?

❯ 1. Submit answers
2. Cancel







Loading