Skip to content

Commit bc07ae8

Browse files
committed
fix(desktop): recognize cursor-positioned plan tokens
Recognize the plan prefix after label punctuation even when terminal cursor controls provide visual spacing. Extend the native Windows fixture through 92-character code submission and complete token receipt. Fixes FOR-308
1 parent 645e57e commit bc07ae8

1 file changed

Lines changed: 40 additions & 9 deletions

File tree

desktop/src-tauri/src/plan.rs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,22 @@ breaks the first time a hint or a colour is added, and breaking here means telli
187187
approved in their browser that it failed.
188188
*/
189189
pub fn token_in(output: &str) -> Option<String> {
190-
plain(output)
191-
.split(|c: char| c.is_whitespace() || c == '"' || c == '\'')
192-
.map(|word| word.trim_matches(|c: char| !c.is_ascii_alphanumeric() && c != '-' && c != '_'))
193-
.find(|word| word.starts_with(PLAN_TOKEN_PREFIX) && word.len() > 30)
190+
fn token_character(c: char) -> bool {
191+
c.is_ascii_alphanumeric() || c == '-' || c == '_'
192+
}
193+
194+
let text = plain(output);
195+
// Cursor positioning can separate a label from its token visually without a space
196+
// in the stream. Accept the prefix after punctuation, but not inside another word.
197+
text.match_indices(PLAN_TOKEN_PREFIX)
198+
.filter(|(at, _)| !matches!(text[..*at].chars().next_back(), Some(c) if token_character(c)))
199+
.map(|(at, _)| {
200+
text[at..]
201+
.split(|c| !token_character(c))
202+
.next()
203+
.unwrap_or("")
204+
})
205+
.find(|token| token.len() > 30)
194206
.map(str::to_string)
195207
}
196208

@@ -773,12 +785,18 @@ mod tests {
773785
let source = root.join("podman.rs");
774786
std::fs::write(
775787
&source,
776-
r#"use std::io::{Read, Write};
788+
r#"use std::io::Write;
777789
fn main() {
778790
print!("\x1b]8;;https://claude.ai/oauth/authorize?synthetic=terminal-lifetime\x1b\\Sign in\x1b]8;;\x1b\\\r\n");
791+
println!("Paste code here if prompted");
779792
std::io::stdout().flush().unwrap();
780-
let mut input = Vec::new();
781-
std::io::stdin().read_to_end(&mut input).unwrap();
793+
let mut input = String::new();
794+
std::io::stdin().read_line(&mut input).unwrap();
795+
assert_eq!(input.trim(), format!("{}#{}", "c".repeat(43), "s".repeat(48)));
796+
print!("Your OAuth token (valid for 1 year):");
797+
std::io::stdout().flush().unwrap();
798+
std::thread::sleep(std::time::Duration::from_millis(60));
799+
println!("\x1b[40G\x1b[32msk-ant-oat01-{}\x1b[0m", "s".repeat(95));
782800
}"#,
783801
)
784802
.unwrap();
@@ -803,8 +821,11 @@ mod tests {
803821
"the login child must survive until the code can be supplied"
804822
);
805823
let draining = std::sync::Arc::downgrade(&signing.output);
806-
signing.stop();
807-
drop(signing);
824+
let code = format!("{}#{}", "c".repeat(43), "s".repeat(48));
825+
assert_eq!(
826+
signing.finish(&code)?,
827+
format!("sk-ant-oat01-{}", "s".repeat(95))
828+
);
808829
// Modern ClosePseudoConsole returns before its clients disconnect. The
809830
// reader's EOF, not the master's drop, marks completed console cleanup.
810831
// Keep this inside the deadline before deleting the fixture executable.
@@ -1034,6 +1055,16 @@ mod tests {
10341055
assert_eq!(authorize_url_in(""), None);
10351056
}
10361057

1058+
#[test]
1059+
fn token_after_a_cursor_positioned_label_is_found_in_full() {
1060+
let token = format!("{PLAN_TOKEN_PREFIX}01-{}", "s".repeat(95));
1061+
let output = format!(
1062+
"Your OAuth token (valid for 1 year):\x1b[1G\x1b[32m{token}\x1b[0m\nStore this token safely."
1063+
);
1064+
assert_eq!(token_in(&output), Some(token.clone()));
1065+
assert_eq!(token_in(&format!("other-{token}")), None);
1066+
}
1067+
10371068
/// The stripper has to survive what a TUI actually emits, including a bare ESC pair.
10381069
#[test]
10391070
fn escapes_come_out_and_the_words_stay() {

0 commit comments

Comments
 (0)