@@ -294,8 +294,11 @@ impl App for OpencodeApp {
294 // ── Lifecycle / connect ─────────────────────────────────────────
294 // ── Lifecycle / connect ─────────────────────────────────────────
295 Event::Start => render(),
295 Event::Start => render(),
297+ // Store the field verbatim while typing; normalizing here would fight
298+ // the user's edits (e.g. eat the "://" as they type it). The URL is
299+ // normalized once, on Connect.
297 Event::ServerUrlChanged(url) => {
300 Event::ServerUrlChanged(url) => {
298− model.server_url = normalize_url(&url);
301+ model.server_url = url;
301 Event::UsernameChanged(u) => {
304 Event::UsernameChanged(u) => {
@@ -308,6 +311,7 @@ impl App for OpencodeApp {
310 Event::Connect => {
313 Event::Connect => {
314+ model.server_url = normalize_url(&model.server_url);
311 if model.server_url.is_empty() {
315 if model.server_url.is_empty() {
312 model.error = Some("Enter a server URL".to_string());
316 model.error = Some("Enter a server URL".to_string());
313 return render();
317 return render();
@@ -803,28 +807,33 @@ mod tests {
806− fn server_url_is_normalized() {
810+ fn server_url_is_kept_raw_while_typing() {
807 let app = app();
811 let app = app();
808 let mut model = Model::default();
812 let mut model = Model::default();
813+ // Mid-scheme input must survive verbatim — normalizing per keystroke
814+ // would eat the "://" the user is typing and make the field unusable.
809− let _ = app.update(Event::ServerUrlChanged(" http://host:4096/ ".into()), &mut model);
815+ let _ = app.update(Event::ServerUrlChanged("http:/".into()), &mut model);
810− assert_eq!(app.view(&model).server_url, "http://host:4096");
816+ assert_eq!(app.view(&model).server_url, "http:/");
814− fn server_url_without_scheme_defaults_to_http() {
820+ fn connect_normalizes_scheme_less_url() {
815 let app = app();
821 let app = app();
816 let mut model = Model::default();
822 let mut model = Model::default();
817− // A scheme-less host (what a user often types) must gain http://, or the
823+ // A scheme-less host (what a user often types) must gain http:// on
818− // shell's URL parser throws and the connect attempt crashes.
824+ // connect, or the shell's URL parser throws and the attempt crashes.
819 let _ = app.update(Event::ServerUrlChanged("192.168.1.10:4096".into()), &mut model);
825 let _ = app.update(Event::ServerUrlChanged("192.168.1.10:4096".into()), &mut model);
826+ assert_eq!(app.view(&model).server_url, "192.168.1.10:4096");
827+ let _ = app.update(Event::Connect, &mut model);
820 assert_eq!(app.view(&model).server_url, "http://192.168.1.10:4096");
828 assert_eq!(app.view(&model).server_url, "http://192.168.1.10:4096");
824− fn server_url_keeps_https_scheme() {
832+ fn connect_trims_and_keeps_explicit_scheme() {
825 let app = app();
833 let app = app();
826 let mut model = Model::default();
834 let mut model = Model::default();
827− let _ = app.update(Event::ServerUrlChanged("https://host:4096".into()), &mut model);
835+ let _ = app.update(Event::ServerUrlChanged(" https://host:4096/ ".into()), &mut model);
836+ let _ = app.update(Event::Connect, &mut model);
828 assert_eq!(app.view(&model).server_url, "https://host:4096");
837 assert_eq!(app.view(&model).server_url, "https://host:4096");