From 2129fef43aa6c5a5d1672b721649dfe7295a776d Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 25 May 2026 18:13:55 -0400 Subject: [PATCH 1/5] Fix loadHtml failing on HTML with JSON-escape-required characters (issue #203) The streaming JSON parser in process_input reconstructs JSON by re-emitting string and field name values wrapped in quotes, but without re-escaping special characters. This produces invalid JSON when the input contains quotes, backslashes, control characters, or other characters that require JSON escaping, causing loadHtml payloads to silently fail deserialization. Fixed by using serde_json::to_string() to produce properly escaped JSON string literals for both FieldName and ValueString events. Also added 6 comprehensive test cases covering: - Quotes and newlines (reproduces original issue #203) - Backslashes in JS regex and Windows paths - Control characters (tabs, CR, form-feed) - Unicode (multi-byte UTF-8, emoji, DEL) - Braces and brackets inside string content - Long payloads (~64 KB with escape-requiring chars) - Special chars in header map keys and values All tests round-trip payloads through process_input and verify byte-for-byte fidelity. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib.rs | 144 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 135 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 93ed8fb..243c6b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -355,18 +355,16 @@ fn process_input( json_string.push(']'); } JsonEvent::FieldName => { - if json_string.ends_with('{') { - json_string.push('"'); - } else { - json_string.push_str(",\""); + if !json_string.ends_with('{') { + json_string.push(','); } - json_string.push_str(parser.current_str().unwrap()); - json_string.push_str("\":"); + json_string + .push_str(&serde_json::to_string(parser.current_str().unwrap()).unwrap()); + json_string.push(':'); } JsonEvent::ValueString => { - json_string.push('"'); - json_string.push_str(parser.current_str().unwrap()); - json_string.push('"'); + json_string + .push_str(&serde_json::to_string(parser.current_str().unwrap()).unwrap()); } JsonEvent::ValueInt => { json_string.push_str(&parser.current_int::().unwrap().to_string()); @@ -895,6 +893,134 @@ mod tests { ); } + /// Round-trips a `LoadHtml` payload through `process_input` and asserts + /// the received `html` matches the original byte-for-byte. + fn assert_load_html_roundtrip(html: &str) { + let request = Request::LoadHtml { + id: 42, + html: html.to_string(), + origin: Some("example.com".to_string()), + }; + + let json = serde_json::to_vec(&request).unwrap(); + let cursor = Cursor::new(json); + let reader = BufReader::new(cursor); + let (sender, receiver) = mpsc::channel(); + + process_input(reader, sender); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + match receiver.try_recv() { + Ok(Request::LoadHtml { + id, + html: received_html, + origin, + }) => { + assert_eq!(id, 42); + assert_eq!(received_html, html); + assert_eq!(origin.as_deref(), Some("example.com")); + } + Ok(other) => panic!("Unexpected request variant: {:?}", other), + Err(e) => panic!("Failed to receive message: {:?}", e), + } + } + + /// Reproduces https://github.com/just-be-dev/webview/issues/203 + /// + /// `loadHtml` fails when the HTML contains characters that require JSON + /// escaping (e.g. `"` in attribute values, newlines). The streaming JSON + /// reconstruction in `process_input` re-emits string values raw, producing + /// invalid JSON that `serde_json::from_str` then rejects. + #[test] + fn test_process_input_load_html_with_quotes_and_newlines() { + assert_load_html_roundtrip( + "\n \n click\n \n", + ); + } + + #[test] + fn test_process_input_load_html_with_backslashes() { + // Inline JS with a regex literal β€” backslashes must survive a round-trip. + assert_load_html_roundtrip( + r#""#, + ); + } + + #[test] + fn test_process_input_load_html_with_control_chars() { + // Tabs, carriage returns, and form-feed all require JSON escaping. + assert_load_html_roundtrip("
\tcol1\tcol2\r\nrow1\tval\x0c
"); + } + + #[test] + fn test_process_input_load_html_with_unicode() { + // Multi-byte UTF-8, emoji (surrogate pair when JSON-escaped), and a + // raw U+007F DEL control character. + assert_load_html_roundtrip("

hΓ©llo δΈ–η•Œ 🌍 \u{007f}

"); + } + + #[test] + fn test_process_input_load_html_with_braces_in_string() { + // Curly braces inside string content must not confuse the depth tracker. + assert_load_html_roundtrip( + r#""#, + ); + } + + #[test] + fn test_process_input_load_html_long() { + // The original report mentions "long" HTML. Build a ~64 KB payload + // sprinkled with characters that must be JSON-escaped. + let chunk = r#"
"hi"
"# .to_string() + "\n"; + let mut html = String::with_capacity(64 * 1024); + while html.len() < 64 * 1024 { + html.push_str(&chunk); + } + assert_load_html_roundtrip(&html); + } + + #[test] + fn test_process_input_load_url_headers_with_special_chars() { + // Exercises the FieldName path (header map keys) and string values + // that contain JSON-escape-required characters. + let headers = HashMap::from([ + ( + "X-Quoted".to_string(), + r#"value with "quotes" and \backslash"#.to_string(), + ), + ("X-Newline".to_string(), "line1\nline2".to_string()), + ]); + let request = Request::LoadUrl { + id: 7, + url: "https://example.com/path?q=\"x\"".to_string(), + headers: Some(headers.clone()), + }; + + let json = serde_json::to_vec(&request).unwrap(); + let cursor = Cursor::new(json); + let reader = BufReader::new(cursor); + let (sender, receiver) = mpsc::channel(); + + process_input(reader, sender); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + match receiver.try_recv() { + Ok(Request::LoadUrl { + id, + url, + headers: received, + }) => { + assert_eq!(id, 7); + assert_eq!(url, "https://example.com/path?q=\"x\""); + assert_eq!(received, Some(headers)); + } + Ok(other) => panic!("Unexpected request variant: {:?}", other), + Err(e) => panic!("Failed to receive message: {:?}", e), + } + } + #[test] fn test_process_output_multiple() { let output = Arc::new(Mutex::new(Vec::new())); From cae55b7fab683404197d3fd13c838e811efe965e Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 25 May 2026 18:25:28 -0400 Subject: [PATCH 2/5] Replace actson with serde_json::Deserializer streaming reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The actson-based approach stream-parsed JSON into events and then reconstructed each top-level value as a String to hand back to serde_json::from_str. That reconstruction was the source of issue #203 (naive string wrapping without escaping) and adds complexity without buying anything for this use case β€” each Request is consumed as a complete unit, so the per-value streaming actson enables is unused. serde_json::Deserializer::from_reader(...).into_iter::() provides the same effective behavior (streaming reads from stdin, one value at a time) in ~10 lines with no reconstruction step. Drops the actson dependency entirely. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 30 -------------------- Cargo.toml | 1 - src/lib.rs | 83 +++++++++++------------------------------------------- 3 files changed, 16 insertions(+), 98 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a91350..2ea25a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actson" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ceaa5a04ff2e693fcfc1d39d1a12d04b66846b66ff5eb00033942785ec4464" -dependencies = [ - "btoi", - "num-traits", - "thiserror 2.0.11", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -87,15 +76,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "btoi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" -dependencies = [ - "num-traits", -] - [[package]] name = "byteorder" version = "1.5.0" @@ -1079,15 +1059,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "num_enum" version = "0.7.3" @@ -2261,7 +2232,6 @@ dependencies = [ name = "webview" version = "0.3.0" dependencies = [ - "actson", "parking_lot", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index ea3493c..674aa22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ tao = "0.33.0" wry = "0.51.0" schemars = "0.8.21" parking_lot = "0.12" -actson = "2.0.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/src/lib.rs b/src/lib.rs index 243c6b2..6cf4caa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -use actson::options::JsonParserOptionsBuilder; use parking_lot::Mutex; use std::borrow::Cow; use std::collections::HashMap; @@ -23,9 +22,6 @@ use wry::http::header::{HeaderName, HeaderValue}; use wry::http::Response as HttpResponse; use wry::WebViewBuilder; -use actson::feeder::BufReaderJsonFeeder; -use actson::{JsonEvent, JsonParser}; - /// The version of the webview binary. const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -303,78 +299,31 @@ impl From for ResultType { } } -/// Incrementally parses JSON input from a reader and sends the parsed requests to a sender. +/// Streams JSON requests from the reader and forwards them to the sender. /// -/// This is used in the main program to read JSON input from stdin and send it to the webview -/// event loop. +/// Used by the main program to read JSON requests from stdin. Each request +/// must be a complete top-level JSON object; values may be concatenated or +/// separated by whitespace. On a parse error the stream is abandoned β€” +/// serde_json's `StreamDeserializer` cannot recover from a malformed value. fn process_input( reader: BufReader, sender: Sender, ) { std::thread::spawn(move || { - let feeder = BufReaderJsonFeeder::new(reader); - let mut parser = JsonParser::new_with_options( - feeder, - JsonParserOptionsBuilder::default() - .with_streaming(true) - .build(), - ); - - let mut json_string = String::new(); - let mut depth = 0; - - while let Some(event) = parser.next_event().unwrap() { - match event { - JsonEvent::NeedMoreInput => parser.feeder.fill_buf().unwrap(), - JsonEvent::StartObject => { - depth += 1; - json_string.push('{'); - } - JsonEvent::EndObject => { - depth -= 1; - json_string.push('}'); - - // If we're back at depth 0, we have a complete JSON object - if depth == 0 { - match serde_json::from_str::(&json_string) { - Ok(request) => { - debug!(request = ?request, "Received request from client"); - sender.send(request).unwrap() - } - Err(e) => error!("Failed to deserialize request: {:?}", e), - } - json_string.clear(); + let stream = serde_json::Deserializer::from_reader(reader).into_iter::(); + for result in stream { + match result { + Ok(request) => { + debug!(request = ?request, "Received request from client"); + if sender.send(request).is_err() { + return; } } - JsonEvent::StartArray => { - depth += 1; - json_string.push('['); - } - JsonEvent::EndArray => { - depth -= 1; - json_string.push(']'); - } - JsonEvent::FieldName => { - if !json_string.ends_with('{') { - json_string.push(','); - } - json_string - .push_str(&serde_json::to_string(parser.current_str().unwrap()).unwrap()); - json_string.push(':'); - } - JsonEvent::ValueString => { - json_string - .push_str(&serde_json::to_string(parser.current_str().unwrap()).unwrap()); - } - JsonEvent::ValueInt => { - json_string.push_str(&parser.current_int::().unwrap().to_string()); - } - JsonEvent::ValueFloat => { - json_string.push_str(&parser.current_float().unwrap().to_string()); + Err(e) if e.is_eof() => return, + Err(e) => { + error!("Failed to deserialize request: {:?}", e); + return; } - JsonEvent::ValueTrue => json_string.push_str("true"), - JsonEvent::ValueFalse => json_string.push_str("false"), - JsonEvent::ValueNull => json_string.push_str("null"), } } }); From 49d7b0dc6d71eceb81fd947729830fe21d905c7f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 25 May 2026 18:27:38 -0400 Subject: [PATCH 3/5] Add tests for streaming behavior and error handling in process_input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers behaviors of the serde_json-based stream reader that the previous suite didn't exercise: - Whitespace separation between top-level values - Malformed JSON terminates the stream (no resync) - Schema mismatch terminates the stream - Empty input exits cleanly - Chunked reader (one byte per read) β€” verifies actual streaming - Receiver dropped does not panic the worker thread Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib.rs | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 6cf4caa..7484f56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -970,6 +970,165 @@ mod tests { } } + #[test] + fn test_process_input_whitespace_separated_values() { + // Real clients write back-to-back JSON, but the streaming deserializer + // should also accept whitespace (newlines, spaces, tabs) between values. + let payload = b"{\"$type\":\"getVersion\",\"id\":1}\n {\"$type\":\"getVersion\",\"id\":2}\t\r\n{\"$type\":\"getVersion\",\"id\":3}"; + let reader = BufReader::new(Cursor::new(payload.to_vec())); + let (sender, receiver) = mpsc::channel(); + + process_input(reader, sender); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + for expected_id in [1, 2, 3] { + match receiver.try_recv() { + Ok(Request::GetVersion { id }) => assert_eq!(id, expected_id), + Ok(other) => panic!("Unexpected request variant: {:?}", other), + Err(e) => panic!("Expected request id={} but got {:?}", expected_id, e), + } + } + } + + #[test] + fn test_process_input_malformed_json_terminates_stream() { + // A garbled value should terminate the stream β€” `serde_json::StreamDeserializer` + // cannot resync after a parse error, so subsequent valid values are not delivered. + let payload = b"{\"$type\":\"getVersion\",\"id\":1}\n{not valid json}\n{\"$type\":\"getVersion\",\"id\":2}"; + let reader = BufReader::new(Cursor::new(payload.to_vec())); + let (sender, receiver) = mpsc::channel(); + + process_input(reader, sender); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + // First valid value comes through. + match receiver.try_recv() { + Ok(Request::GetVersion { id }) => assert_eq!(id, 1), + other => panic!("Expected GetVersion id=1, got {:?}", other), + } + // Nothing further β€” sender thread has exited. + match receiver.try_recv() { + Err(mpsc::TryRecvError::Disconnected) => {} + other => panic!("Expected Disconnected after malformed input, got {:?}", other), + } + } + + #[test] + fn test_process_input_wrong_schema_terminates_stream() { + // Well-formed JSON that doesn't match `Request` also stops the stream. + let payload = + b"{\"$type\":\"getVersion\",\"id\":1}\n{\"$type\":\"notARealRequest\",\"id\":2}\n{\"$type\":\"getVersion\",\"id\":3}"; + let reader = BufReader::new(Cursor::new(payload.to_vec())); + let (sender, receiver) = mpsc::channel(); + + process_input(reader, sender); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + match receiver.try_recv() { + Ok(Request::GetVersion { id }) => assert_eq!(id, 1), + other => panic!("Expected GetVersion id=1, got {:?}", other), + } + match receiver.try_recv() { + Err(mpsc::TryRecvError::Disconnected) => {} + other => panic!("Expected Disconnected after schema error, got {:?}", other), + } + } + + #[test] + fn test_process_input_empty_input() { + // Empty input should exit cleanly without producing messages or panicking. + let reader = BufReader::new(Cursor::new(Vec::::new())); + let (sender, receiver) = mpsc::channel(); + + process_input(reader, sender); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + match receiver.try_recv() { + Err(mpsc::TryRecvError::Disconnected) => {} + other => panic!("Expected Disconnected on empty input, got {:?}", other), + } + } + + /// A `Read` that returns at most one byte per call. Verifies that the + /// stream parser actually streams β€” it must not require the whole input + /// (or even a whole value) to be buffered before it can parse. + struct OneByteAtATime { + data: Vec, + pos: usize, + } + + impl Read for OneByteAtATime { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if self.pos >= self.data.len() || buf.is_empty() { + return Ok(0); + } + buf[0] = self.data[self.pos]; + self.pos += 1; + Ok(1) + } + } + + #[test] + fn test_process_input_chunked_reader() { + let requests = vec![ + Request::GetVersion { id: 1 }, + Request::LoadHtml { + id: 2, + html: "

hi \"world\"

".to_string(), + origin: None, + }, + ]; + let mut bytes = Vec::new(); + for req in &requests { + bytes.extend(serde_json::to_vec(req).unwrap()); + } + let reader = BufReader::new(OneByteAtATime { data: bytes, pos: 0 }); + let (sender, receiver) = mpsc::channel(); + + process_input(reader, sender); + + std::thread::sleep(std::time::Duration::from_millis(200)); + + match receiver.try_recv() { + Ok(Request::GetVersion { id }) => assert_eq!(id, 1), + other => panic!("Expected GetVersion id=1, got {:?}", other), + } + match receiver.try_recv() { + Ok(Request::LoadHtml { id, html, origin }) => { + assert_eq!(id, 2); + assert_eq!(html, "

hi \"world\"

"); + assert_eq!(origin, None); + } + other => panic!("Expected LoadHtml id=2, got {:?}", other), + } + } + + #[test] + fn test_process_input_receiver_dropped_does_not_panic() { + // Before the refactor, `sender.send(request).unwrap()` would panic in + // the worker thread when the receiver was dropped. The new code + // returns gracefully. + let mut bytes = Vec::new(); + for id in 0..100 { + bytes.extend(serde_json::to_vec(&Request::GetVersion { id }).unwrap()); + } + let reader = BufReader::new(Cursor::new(bytes)); + let (sender, receiver) = mpsc::channel(); + + process_input(reader, sender); + drop(receiver); + + // Give the worker time to attempt sends and exit. If it panics, the + // thread is detached so we can't observe it directly β€” but the test + // process should not abort. A short sleep is enough to surface a + // panic during local runs; CI catches more reliably. + std::thread::sleep(std::time::Duration::from_millis(100)); + } + #[test] fn test_process_output_multiple() { let output = Arc::new(Mutex::new(Vec::new())); From 5cea417e9230687b6c7e8f4dd6d0caaf8b633eda Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 25 May 2026 19:35:14 -0400 Subject: [PATCH 4/5] Bump binary to 0.3.2 and update CHANGELOG Skipping 0.3.1 since that tag already exists from a prior release where Cargo.toml was not updated alongside the tag. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 5 +++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afd57e..8bd35da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.3.2 binary -- 2026-05-25 + +- Fixed `loadHtml` failing when the HTML payload contained characters requiring JSON escaping such as `"`, `\`, newlines, or control characters (#203) +- Refactored stdin JSON parsing to use `serde_json`'s streaming `Deserializer` directly; removes the `actson` dependency + ## 0.0.4 Python Client; 1.0.1-rc.2 Deno Client; 0.3.1 binary -- 2025-03-05 - Fixed a bug that caused webview to fail to open on linux diff --git a/Cargo.lock b/Cargo.lock index 2ea25a9..dee4bf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2230,7 +2230,7 @@ dependencies = [ [[package]] name = "webview" -version = "0.3.0" +version = "0.3.2" dependencies = [ "parking_lot", "schemars", diff --git a/Cargo.toml b/Cargo.toml index 674aa22..6af4dea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "webview" -version = "0.3.0" +version = "0.3.2" edition = "2021" [profile.release] From 6bd1f30a5a68d34ade28ee38b6b487ec0a69af5d Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 25 May 2026 19:38:43 -0400 Subject: [PATCH 5/5] Bump deno client to 1.0.1-rc.3 and python client to 0.0.5 Both clients pick up binary 0.3.2 to inherit the loadHtml fix (#203). CHANGELOG entry restructured to cover all three artifacts. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 12 +++++++++++- src/clients/deno/deno.json | 2 +- src/clients/deno/main.ts | 2 +- src/clients/python/pyproject.toml | 2 +- src/clients/python/src/justbe_webview/__init__.py | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bd35da..cabdfa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,20 @@ # Changelog -## 0.3.2 binary -- 2026-05-25 +## 0.0.5 Python Client; 1.0.1-rc.3 Deno Client; 0.3.2 binary -- 2026-05-25 + +Binary - Fixed `loadHtml` failing when the HTML payload contained characters requiring JSON escaping such as `"`, `\`, newlines, or control characters (#203) - Refactored stdin JSON parsing to use `serde_json`'s streaming `Deserializer` directly; removes the `actson` dependency +Python Client + +- Updated webview binary to 0.3.2 + +Deno Client + +- Updated webview binary to 0.3.2 + ## 0.0.4 Python Client; 1.0.1-rc.2 Deno Client; 0.3.1 binary -- 2025-03-05 - Fixed a bug that caused webview to fail to open on linux diff --git a/src/clients/deno/deno.json b/src/clients/deno/deno.json index fa8b8eb..adaf158 100644 --- a/src/clients/deno/deno.json +++ b/src/clients/deno/deno.json @@ -2,7 +2,7 @@ "name": "@justbe/webview", "exports": "./main.ts", "license": "MIT", - "version": "1.0.1-rc.2", + "version": "1.0.1-rc.3", "publish": { "include": ["README.md", "LICENSE", "*.ts", "schemas/*.ts"] }, diff --git a/src/clients/deno/main.ts b/src/clients/deno/main.ts index a3622b2..704a227 100644 --- a/src/clients/deno/main.ts +++ b/src/clients/deno/main.ts @@ -55,7 +55,7 @@ if ( // Should match the cargo package version /** The version of the webview binary that's expected */ -export const BIN_VERSION = "0.3.1"; +export const BIN_VERSION = "0.3.2"; type WebViewNotification = Extract< Message, diff --git a/src/clients/python/pyproject.toml b/src/clients/python/pyproject.toml index a4b63d4..e322680 100644 --- a/src/clients/python/pyproject.toml +++ b/src/clients/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "justbe-webview" -version = "0.0.4" +version = "0.0.5" description = "A simple webview client" readme = "README.md" requires-python = ">=3.12" diff --git a/src/clients/python/src/justbe_webview/__init__.py b/src/clients/python/src/justbe_webview/__init__.py index 8e273af..eb06e0d 100644 --- a/src/clients/python/src/justbe_webview/__init__.py +++ b/src/clients/python/src/justbe_webview/__init__.py @@ -48,7 +48,7 @@ ) # Constants -BIN_VERSION = "0.3.1" +BIN_VERSION = "0.3.2" T = TypeVar("T", bound=WebViewNotification)