You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/23 21:09:05 UTC

(maka) branch feat/runtime-host-rust updated (9c651da46a -> a66604c8d1)

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a change to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git


    from 9c651da46a fix(providers): preserve account reasoning policy through Responses
     new dec572c3c9 test(cli): verify candidate lifecycle through the native Client
     new 1d34d3da0a feat(tui): search and scroll the command palette
     new d01501b44f merge: integrate native TUI with Session bundle notifications
     new 606de60b88 merge: integrate account reasoning policy with native TUI
     new a66604c8d1 test(cli): await finite activation cleanup before releasing the writer

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 crates/cli/tests/integration/candidate.rs          | 179 +++++++--
 crates/cli/tests/integration/deployment.rs         |   9 +
 crates/cli/tests/integration/tui.rs                |  13 +-
 crates/cli/tests/integration/tui/attachments.rs    |   3 +-
 crates/cli/tests/integration/tui/branch.rs         |   9 +-
 crates/cli/tests/integration/tui/changes.rs        |   3 +-
 .../cli/tests/integration/tui/connection_test.rs   |   7 +-
 crates/cli/tests/integration/tui/connections.rs    |  15 +-
 crates/cli/tests/integration/tui/credentials.rs    |   3 +-
 crates/cli/tests/integration/tui/enabled_models.rs |   3 +-
 crates/cli/tests/integration/tui/imports.rs        |   4 +-
 crates/cli/tests/integration/tui/management.rs     |  24 +-
 crates/cli/tests/integration/tui/model_fetch.rs    |   3 +-
 crates/cli/tests/integration/tui/model_profiles.rs |   3 +-
 crates/cli/tests/integration/tui/models.rs         |   9 +-
 crates/cli/tests/integration/tui/oauth.rs          |  12 +-
 crates/cli/tests/integration/tui/projects.rs       |  21 +-
 crates/cli/tests/integration/tui/recap.rs          |   9 +-
 crates/cli/tests/integration/tui/recovery.rs       |   6 +-
 crates/cli/tests/integration/tui/references.rs     |   3 +-
 crates/cli/tests/integration/tui/removal.rs        |  12 +-
 crates/cli/tests/integration/tui/revision.rs       |   9 +-
 crates/cli/tests/integration/tui/skills.rs         |   7 +-
 crates/tui/locales/en.ftl                          |   2 +
 crates/tui/locales/zh-CN.ftl                       |   2 +
 crates/tui/locales/zh-TW.ftl                       |   2 +
 crates/tui/src/app.rs                              |  57 +--
 crates/tui/src/pages.rs                            |   1 +
 crates/tui/src/pages/commands.rs                   | 442 +++++++++++++++++++++
 crates/tui/src/pages/sending.rs                    |   3 +
 crates/tui/src/view.rs                             |  42 +-
 crates/tui/src/view/session.rs                     |   6 +
 crates/tui/src/view/session/search.rs              |  12 +-
 33 files changed, 711 insertions(+), 224 deletions(-)
 create mode 100644 crates/tui/src/pages/commands.rs


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


(maka) 05/05: test(cli): await finite activation cleanup before releasing the writer

Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git

commit a66604c8d1d2b67d1176e94766063cc284865487
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Thu Sep 24 05:04:25 2026 +0800

    test(cli): await finite activation cleanup before releasing the writer
    
    Verify the worker deadline and idle executor before subsequent deployment operations; observer timeout alone is not evidence of cleanup.
    
    Generated-by: Codex
---
 crates/cli/tests/integration/deployment.rs | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/crates/cli/tests/integration/deployment.rs b/crates/cli/tests/integration/deployment.rs
index adfc1165ca..41bfdef657 100644
--- a/crates/cli/tests/integration/deployment.rs
+++ b/crates/cli/tests/integration/deployment.rs
@@ -99,10 +99,13 @@ fn managed_installation_pins_code_before_migration_and_preserves_live_authority(
         {
             // A live but unreachable writer is not a stale lock. The command
             // must leave on its own deadline without taking over that Root.
+            // Run the finite worker directly: its exit, unlike the observer's
+            // timeout, proves cleanup before we release this writer or mutate.
             let owner = RootOwner::open(&fixture.root, &namespaces).unwrap();
             let started = Instant::now();
             let waiting = Command::new(executable)
                 .args([
+                    "--operation-worker",
                     "host",
                     "activate",
                     "--root-id",
@@ -114,6 +117,12 @@ fn managed_installation_pins_code_before_migration_and_preserves_live_authority(
                 .unwrap();
             assert!(!waiting.status.success());
             assert!(started.elapsed() < Duration::from_secs(3));
+            assert!(
+                String::from_utf8_lossy(&waiting.stderr)
+                    .contains("Host did not become Ready before the activation deadline"),
+                "{waiting:?}"
+            );
+            assert_eq!(query("status")["operation"], "idle");
             assert!(RootOwner::open(&fixture.root, &namespaces).is_err());
             assert_eq!(query("status")["deployment"], installed);
             drop(owner);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


(maka) 02/05: feat(tui): search and scroll the command palette

Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 1d34d3da0aa45e228c7755bbb70bee032884101f
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Thu Sep 24 04:44:13 2026 +0800

    feat(tui): search and scroll the command palette
    
    Keep filtered commands identity-stable, preserve drafts, and isolate modal cursors.
    
    Generated-by: Codex
---
 crates/cli/tests/integration/tui.rs                |  13 +-
 crates/cli/tests/integration/tui/attachments.rs    |   3 +-
 crates/cli/tests/integration/tui/branch.rs         |   9 +-
 crates/cli/tests/integration/tui/changes.rs        |   3 +-
 .../cli/tests/integration/tui/connection_test.rs   |   7 +-
 crates/cli/tests/integration/tui/connections.rs    |  15 +-
 crates/cli/tests/integration/tui/credentials.rs    |   3 +-
 crates/cli/tests/integration/tui/enabled_models.rs |   3 +-
 crates/cli/tests/integration/tui/imports.rs        |   4 +-
 crates/cli/tests/integration/tui/management.rs     |  24 +-
 crates/cli/tests/integration/tui/model_fetch.rs    |   3 +-
 crates/cli/tests/integration/tui/model_profiles.rs |   3 +-
 crates/cli/tests/integration/tui/models.rs         |   9 +-
 crates/cli/tests/integration/tui/oauth.rs          |  12 +-
 crates/cli/tests/integration/tui/projects.rs       |  21 +-
 crates/cli/tests/integration/tui/recap.rs          |   9 +-
 crates/cli/tests/integration/tui/recovery.rs       |   6 +-
 crates/cli/tests/integration/tui/references.rs     |   3 +-
 crates/cli/tests/integration/tui/removal.rs        |  12 +-
 crates/cli/tests/integration/tui/revision.rs       |   9 +-
 crates/cli/tests/integration/tui/skills.rs         |   7 +-
 crates/tui/locales/en.ftl                          |   2 +
 crates/tui/locales/zh-CN.ftl                       |   2 +
 crates/tui/locales/zh-TW.ftl                       |   2 +
 crates/tui/src/app.rs                              |  57 +--
 crates/tui/src/pages.rs                            |   1 +
 crates/tui/src/pages/commands.rs                   | 442 +++++++++++++++++++++
 crates/tui/src/pages/sending.rs                    |   3 +
 crates/tui/src/view.rs                             |  42 +-
 crates/tui/src/view/session.rs                     |   6 +
 crates/tui/src/view/session/search.rs              |  12 +-
 31 files changed, 552 insertions(+), 195 deletions(-)

diff --git a/crates/cli/tests/integration/tui.rs b/crates/cli/tests/integration/tui.rs
index 287c1994b7..a2ced982f2 100644
--- a/crates/cli/tests/integration/tui.rs
+++ b/crates/cli/tests/integration/tui.rs
@@ -441,12 +441,10 @@ fn real_host_catalog_subscription_and_remote_updates_reach_clients() {
     tui.click_text("ⓘ");
     tui.wait_for("Last input 9.5k / 128.0k"); // Diagnostics only, not current context occupancy.
     tui.click_text("ⓘ");
-    tui.send(b"\x10");
-    tui.wait_for("Show execution details");
+    tui.filter_command("Show execution details");
     tui.click_text("Show execution details");
     tui.wait_for("Completed"); // Durable terminal state is available in the opt-in trace.
-    tui.send(b"\x10");
-    tui.wait_for("Hide execution details");
+    tui.filter_command("Hide execution details");
     tui.click_text("Hide execution details");
     tui.wait_until(|screen| !screen.contains("Completed"));
     tui.wait_for("Read × 2 · Search × 1");
@@ -908,6 +906,13 @@ impl Pty {
     fn send(&mut self, bytes: &[u8]) {
         self.master.write_all(bytes).unwrap();
     }
+    fn filter_command(&mut self, label: &str) {
+        self.send(b"\x10");
+        self.wait_for("Search commands…");
+        self.send(format!("\x1b[200~{}\x1b[201~", label.to_lowercase()).as_bytes());
+        // Lowercase query differs from the command's title: await the filtered row.
+        self.wait_until(|screen| !screen.contains("Search commands…") && screen.contains(label));
+    }
     fn click_text(&mut self, text: &str) {
         self.click_matching_text(text, false);
     }
diff --git a/crates/cli/tests/integration/tui/attachments.rs b/crates/cli/tests/integration/tui/attachments.rs
index 8140588d9c..93353c561b 100644
--- a/crates/cli/tests/integration/tui/attachments.rs
+++ b/crates/cli/tests/integration/tui/attachments.rs
@@ -76,8 +76,7 @@ fn local_file_upload_survives_routes_and_restart_then_sends_without_text() {
     tui.wait_for("Attachment session");
     tui.click_text("Attachment session");
     tui.wait_for("No messages yet.");
-    tui.send(b"\x10");
-    tui.wait_for("Attach files");
+    tui.filter_command("Attach files");
     tui.click_text("Attach files");
     tui.wait_for("Local files");
     tui.wait_for("attachment 中文.txt");
diff --git a/crates/cli/tests/integration/tui/branch.rs b/crates/cli/tests/integration/tui/branch.rs
index 9723a4f45b..d794093806 100644
--- a/crates/cli/tests/integration/tui/branch.rs
+++ b/crates/cli/tests/integration/tui/branch.rs
@@ -82,14 +82,12 @@ fn branch_preserves_selected_history_and_draft_and_reopens_by_original_receipt()
     tui.send(b"Preserved branch draft");
     tui.wait_for("Preserved branch draft");
     tui.click_text("Selected branch point");
-    tui.send(b"\x10");
-    tui.wait_for("Branch from this turn");
+    tui.filter_command("Branch from this turn");
     tui.click_text("Branch from this turn");
     tui.wait_for("same working directory.");
     tui.send(b"\r"); // Initial focus is Cancel.
     tui.wait_until(|s| !s.contains("same working directory."));
-    tui.send(b"\x10");
-    tui.wait_for("Branch from this turn");
+    tui.filter_command("Branch from this turn");
     tui.click_text("Branch from this turn");
     tui.wait_for("same working directory.");
     tui.click_text("Create branch");
@@ -138,8 +136,7 @@ fn branch_preserves_selected_history_and_draft_and_reopens_by_original_receipt()
     let mut reopened = Pty::spawn(&["--root", host.root.to_str().unwrap()]);
     reopened.wait_for("Preserved branch draft");
     reopened.wait_for("Later reply."); // Restored draft alone does not mean Host is connected.
-    reopened.send(b"\x10");
-    reopened.wait_for("Check session branch");
+    reopened.filter_command("Check session branch");
     reopened.click_text("Check session branch");
     reopened.wait_for("Result not yet confirmed.");
     reopened.click_text("Check result");
diff --git a/crates/cli/tests/integration/tui/changes.rs b/crates/cli/tests/integration/tui/changes.rs
index ac67bbffd5..8c0531a609 100644
--- a/crates/cli/tests/integration/tui/changes.rs
+++ b/crates/cli/tests/integration/tui/changes.rs
@@ -274,8 +274,7 @@ fn code_mode_patch_stops_after_failure_and_keeps_real_tools_visible() {
             .screen
             .contains("Delete · later.txt")
     );
-    tui.send(b"\x10");
-    tui.wait_for("Show execution details");
+    tui.filter_command("Show execution details");
     tui.click_text("Show execution details");
     tui.wait_for("exec");
     tui.send(b"\x11");
diff --git a/crates/cli/tests/integration/tui/connection_test.rs b/crates/cli/tests/integration/tui/connection_test.rs
index ca06d243fa..ad6d18a70a 100644
--- a/crates/cli/tests/integration/tui/connection_test.rs
+++ b/crates/cli/tests/integration/tui/connection_test.rs
@@ -23,9 +23,7 @@ use maka_protocol::Operation;
 use serde_json::json;
 
 fn open(tui: &mut Pty) {
-    tui.send(b"\x10");
-    tui.wait_for("Commands · Esc closes");
-    tui.wait_for("Test connection");
+    tui.filter_command("Test connection");
     tui.click_text("Test connection");
     tui.wait_for("incur usage");
     tui.wait_for("Cancel");
@@ -172,8 +170,7 @@ fn connection_test_confirms_network_records_failure_preserves_configuration_and_
         });
         tui.wait_for(name);
         if name == "Before menu open" {
-            tui.send(b"\x10");
-            tui.wait_for("Test connection");
+            tui.filter_command("Test connection");
         }
     }
     assert!(
diff --git a/crates/cli/tests/integration/tui/connections.rs b/crates/cli/tests/integration/tui/connections.rs
index 817b59438f..6a91f1e1a6 100644
--- a/crates/cli/tests/integration/tui/connections.rs
+++ b/crates/cli/tests/integration/tui/connections.rs
@@ -208,8 +208,7 @@ fn connections_directory_pages_reopens_and_manages_fixed_targets_with_cas() {
         .block_on(client.connection_catalog(Query::Start))
         .unwrap()["revision"]
         .clone();
-    reopened.send(b"\x10");
-    reopened.wait_for("Change service address");
+    reopened.filter_command("Change service address");
     reopened.click_text("Change service address");
     reopened.wait_for("Review");
     reopened.send(b"not a URL\r");
@@ -225,8 +224,7 @@ fn connections_directory_pages_reopens_and_manages_fixed_targets_with_cas() {
             .unwrap()["revision"],
         before_endpoint
     );
-    reopened.send(b"\x10");
-    reopened.wait_for("Change service address");
+    reopened.filter_command("Change service address");
     reopened.click_text("Change service address");
     reopened.wait_for("Review");
     reopened.send(b"http://127.0.0.1:9/v2\r");
@@ -279,8 +277,7 @@ fn connections_directory_pages_reopens_and_manages_fixed_targets_with_cas() {
         );
     });
     for (command, enabled) in [("Disable connection", false), ("Enable connection", true)] {
-        reopened.send(b"\x10");
-        reopened.wait_for(command);
+        reopened.filter_command(command);
         reopened.click_text(command);
         reopened.wait_for("Cancel");
         reopened.send(b"\t\r");
@@ -315,8 +312,7 @@ fn connections_directory_pages_reopens_and_manages_fixed_targets_with_cas() {
             .unwrap();
     });
     reopened.wait_for("Default: model-0");
-    reopened.send(b"\x10");
-    reopened.wait_for("Remove connection");
+    reopened.filter_command("Remove connection");
     reopened.click_text("Remove connection");
     reopened.wait_for("Permanently removes");
     reopened.wait_for("Cancel");
@@ -328,8 +324,7 @@ fn connections_directory_pages_reopens_and_manages_fixed_targets_with_cas() {
             .unwrap()["connectionCount"],
         18
     );
-    reopened.send(b"\x10");
-    reopened.wait_for("Remove connection");
+    reopened.filter_command("Remove connection");
     reopened.click_text("Remove connection");
     reopened.wait_for("Cancel");
     reopened.send(b"\t\r");
diff --git a/crates/cli/tests/integration/tui/credentials.rs b/crates/cli/tests/integration/tui/credentials.rs
index 783a90fb12..1aca09c138 100644
--- a/crates/cli/tests/integration/tui/credentials.rs
+++ b/crates/cli/tests/integration/tui/credentials.rs
@@ -210,8 +210,7 @@ fn credential_management_rotates_with_cas_uses_the_new_key_and_clears_without_le
 }
 
 fn open(tui: &mut Pty, clear: bool) {
-    tui.send(b"\x10");
-    tui.wait_for("Clear API key");
+    tui.filter_command("API key");
     if clear {
         tui.click_text("Clear API key");
     } else {
diff --git a/crates/cli/tests/integration/tui/enabled_models.rs b/crates/cli/tests/integration/tui/enabled_models.rs
index 16afac302b..7e3462e3a0 100644
--- a/crates/cli/tests/integration/tui/enabled_models.rs
+++ b/crates/cli/tests/integration/tui/enabled_models.rs
@@ -44,8 +44,7 @@ fn ids(items: &[Value]) -> Vec<String> {
         .collect()
 }
 fn open(tui: &mut Pty) {
-    tui.send(b"\x10");
-    tui.wait_for("Enabled models");
+    tui.filter_command("Enabled models");
     tui.click_text("Enabled models");
     tui.wait_for("Search models");
     tui.wait_for("130 enabled.");
diff --git a/crates/cli/tests/integration/tui/imports.rs b/crates/cli/tests/integration/tui/imports.rs
index 0c61e88a6e..61db7bb4a2 100644
--- a/crates/cli/tests/integration/tui/imports.rs
+++ b/crates/cli/tests/integration/tui/imports.rs
@@ -147,7 +147,9 @@ fn imported_observations_remain_readable_without_offering_executable_turn_action
     for message in ["Original imported question", "Original imported answer"] {
         tui.click_text(message);
         tui.send(b"\x10");
-        tui.wait_for("Commands · Esc closes");
+        tui.wait_for("Search commands…");
+        tui.send(b"branch from this turn");
+        tui.wait_for("No matching commands.");
         assert!(
             !tui.screen
                 .snapshot()
diff --git a/crates/cli/tests/integration/tui/management.rs b/crates/cli/tests/integration/tui/management.rs
index f9092c0518..a802e0a303 100644
--- a/crates/cli/tests/integration/tui/management.rs
+++ b/crates/cli/tests/integration/tui/management.rs
@@ -52,8 +52,7 @@ fn session_management_cas_and_archive_preserve_identity_and_composer() {
     tui.click_text("Original session");
     tui.wait_for("Message…");
     tui.send(b"draft remains here");
-    tui.send(b"\x10");
-    tui.wait_for("Rename session");
+    tui.filter_command("Rename session");
     tui.click_text("Rename session");
     tui.wait_for("Cancel");
     tui.send("\x1b[200~我的新名称\x1b[201~".as_bytes());
@@ -80,8 +79,7 @@ fn session_management_cas_and_archive_preserve_identity_and_composer() {
     assert!(tui.screen.snapshot().unwrap().screen.contains("我的新名称"));
     tui.send(b"\r\x1b"); // Disabled Save cannot overwrite the concurrent writer.
     tui.wait_until(|text| text.contains("Changed elsewhere") && !text.contains("Cancel"));
-    tui.send(b"\x10");
-    tui.wait_for("Rename session");
+    tui.filter_command("Rename session");
     tui.click_text("Rename session");
     tui.wait_for("Cancel");
     tui.send("\x1b[200~中文会话\x1b[201~".as_bytes());
@@ -94,8 +92,7 @@ fn session_management_cas_and_archive_preserve_identity_and_composer() {
             .screen
             .contains("draft remains here")
     );
-    tui.send(b"\x10");
-    tui.wait_for("Archive session");
+    tui.filter_command("Archive session");
     tui.click_text("Archive session");
     tui.wait_for("History is kept.");
     tui.send(b"\r"); // Default focus is Cancel.
@@ -107,8 +104,7 @@ fn session_management_cas_and_archive_preserve_identity_and_composer() {
             .unwrap()
             .is_archived
     );
-    tui.send(b"\x10");
-    tui.wait_for("Archive session");
+    tui.filter_command("Archive session");
     tui.click_text("Archive session");
     tui.wait_for("History is kept.");
     tui.send(b"\t\r");
@@ -125,8 +121,7 @@ fn session_management_cas_and_archive_preserve_identity_and_composer() {
     tui.wait_for("中文会话 · Archived");
     tui.click_text("中文会话 · Archived");
     tui.wait_for("draft remains here");
-    tui.send(b"\x10");
-    tui.wait_for("Restore session");
+    tui.filter_command("Restore session");
     tui.click_text("Restore session");
     tui.wait_for("History is kept.");
     tui.send(b"\t\r");
@@ -142,8 +137,7 @@ fn session_management_cas_and_archive_preserve_identity_and_composer() {
     });
     let moved = directory.path().join("中文 workspace");
     std::fs::create_dir(&moved).unwrap();
-    tui.send(b"\x10");
-    tui.wait_for("Change workspace");
+    tui.filter_command("Change workspace");
     tui.click_text("Change workspace");
     tui.wait_for("Absolute directory on the Host.");
     tui.send(format!("\x1b[200~{}/missing\x1b[201~\r", directory.path().display()).as_bytes());
@@ -190,8 +184,7 @@ fn session_management_cas_and_archive_preserve_identity_and_composer() {
             }),
         )
         .unwrap();
-    tui.send(b"\x10");
-    tui.wait_for("Change project");
+    tui.filter_command("Change project");
     tui.click_text("Change project");
     tui.wait_for("Selected project");
     tui.send(b"\r"); // Opening the chooser does not implicitly select the first row.
@@ -246,8 +239,7 @@ fn session_management_cas_and_archive_preserve_identity_and_composer() {
     tui.wait_for("This session changed elsewhere.");
     tui.send(b"\x1b");
     tui.wait_until(|text| text.contains("Concurrent project edit") && !text.contains("Cancel"));
-    tui.send(b"\x10");
-    tui.wait_for("Change project");
+    tui.filter_command("Change project");
     tui.click_text("Change project");
     tui.wait_for("Selected project");
     tui.click_text("Selected project");
diff --git a/crates/cli/tests/integration/tui/model_fetch.rs b/crates/cli/tests/integration/tui/model_fetch.rs
index c649ad0d9b..111f2bfa9c 100644
--- a/crates/cli/tests/integration/tui/model_fetch.rs
+++ b/crates/cli/tests/integration/tui/model_fetch.rs
@@ -210,8 +210,7 @@ fn row<'a>(page: &'a Value, id: &str) -> &'a Value {
         .unwrap()
 }
 fn open(tui: &mut Pty) {
-    tui.send(b"\x10");
-    tui.wait_for("Fetch models");
+    tui.filter_command("Fetch models");
     tui.click_text("Fetch models");
     tui.wait_for("Queries the currently configured service");
     tui.wait_for("Cancel");
diff --git a/crates/cli/tests/integration/tui/model_profiles.rs b/crates/cli/tests/integration/tui/model_profiles.rs
index b2b9688b9a..32379bef8e 100644
--- a/crates/cli/tests/integration/tui/model_profiles.rs
+++ b/crates/cli/tests/integration/tui/model_profiles.rs
@@ -23,8 +23,7 @@ use maka_protocol::Operation;
 use serde_json::{Value, json};
 
 fn open(tui: &mut Pty) {
-    tui.send(b"\x10");
-    tui.wait_for("Model settings");
+    tui.filter_command("Model settings");
     tui.click_text("Model settings");
     tui.wait_for("Choose a model");
     tui.send(b"fixture-model");
diff --git a/crates/cli/tests/integration/tui/models.rs b/crates/cli/tests/integration/tui/models.rs
index da20ec1905..26a3ea4f03 100644
--- a/crates/cli/tests/integration/tui/models.rs
+++ b/crates/cli/tests/integration/tui/models.rs
@@ -215,8 +215,7 @@ fn model_choice_preserves_session_draft_and_default_and_uses_selected_context_wi
     // Host defaults use catalog CAS, not the open session's revision.
     tui.send(b"default-keeps-draft");
     tui.wait_for("default-keeps-draft");
-    tui.send(b"\x10");
-    tui.wait_for("Default model");
+    tui.filter_command("Default model");
     tui.click_text("Default model");
     tui.wait_for("No default model");
     tui.wait_for("Alternate model");
@@ -237,8 +236,7 @@ fn model_choice_preserves_session_draft_and_default_and_uses_selected_context_wi
         assert_eq!(neighbor.llm_connection_id.as_deref(),default["connectionId"].as_str());
         client.session("managed").await.unwrap().unwrap().revision
     });
-    tui.send(b"\x10");
-    tui.wait_for("Default model");
+    tui.filter_command("Default model");
     tui.click_text("Default model");
     tui.wait_for("No default model");
     tui.wait_for("Alternate model · Current default");
@@ -257,8 +255,7 @@ fn model_choice_preserves_session_draft_and_default_and_uses_selected_context_wi
             alternate["connectionId"]
         );
     });
-    tui.send(b"\x10");
-    tui.wait_for("Default model");
+    tui.filter_command("Default model");
     tui.click_text("Default model");
     tui.wait_for("No default model");
     tui.wait_for("Current default");
diff --git a/crates/cli/tests/integration/tui/oauth.rs b/crates/cli/tests/integration/tui/oauth.rs
index d119f73666..574eda21c4 100644
--- a/crates/cli/tests/integration/tui/oauth.rs
+++ b/crates/cli/tests/integration/tui/oauth.rs
@@ -49,8 +49,7 @@ fn oauth_entry_uses_host_enrollment_mouse_keyboard_and_never_starts_on_dismiss()
     tui.click_text("◉ Host");
     tui.wait_for("Host epoch:");
     tui.click_text("▤ Workspace");
-    tui.send(b"\x10");
-    tui.wait_for("Sign in to a provider");
+    tui.filter_command("Sign in to a provider");
     tui.click_text("Sign in to a provider");
     tui.wait_for("Sign-in for this provider is disabled");
     tui.wait_for("Continue");
@@ -74,8 +73,7 @@ fn oauth_entry_uses_host_enrollment_mouse_keyboard_and_never_starts_on_dismiss()
     });
     tui.resize(100, 30);
     tui.wait_for("▤ Workspace");
-    tui.send(b"\x10");
-    tui.wait_for("Sign in to a provider");
+    tui.filter_command("Sign in to a provider");
     tui.click_text("Sign in to a provider");
     tui.wait_for("Choose a provider");
     tui.send(b"\x1b[<0;1;1M\x1b[<0;1;1m");
@@ -122,8 +120,7 @@ fn oauth_start_is_durable_before_dispatch_and_crash_reopens_only_the_original_qu
         tui.click_text("◉ Host");
         tui.wait_for("Host epoch:");
         tui.click_text("▤ Workspace");
-        tui.send(b"\x10");
-        tui.wait_for("Sign in to a provider");
+        tui.filter_command("Sign in to a provider");
         tui.click_text("Sign in to a provider");
         tui.wait_for("Choose a provider.");
         custom_identity(&mut tui);
@@ -166,8 +163,7 @@ fn oauth_start_is_durable_before_dispatch_and_crash_reopens_only_the_original_qu
         reopened.wait_for("Workspace");
         reopened.click_text("◉ Host");
         reopened.wait_for("Host epoch:");
-        reopened.send(b"\x10");
-        reopened.wait_for("View sign-in");
+        reopened.filter_command("View sign-in");
         reopened.click_text("View sign-in");
         reopened.wait_for("The outcome is unknown.");
         reopened.wait_for("Close   Check   Cancel");
diff --git a/crates/cli/tests/integration/tui/projects.rs b/crates/cli/tests/integration/tui/projects.rs
index 33aa06cc55..296fdefc2c 100644
--- a/crates/cli/tests/integration/tui/projects.rs
+++ b/crates/cli/tests/integration/tui/projects.rs
@@ -203,20 +203,17 @@ fn project_catalog_notifications_and_creation_use_host_project_identity() {
     let mut tui = Pty::spawn(&["--root", host.root.to_str().unwrap()]);
     tui.wait_for("Renamed project"); // Restored route re-queries after connection, not a false empty state.
     tui.click_text("Renamed project");
-    tui.send(b"\x10");
-    tui.wait_for("Rename project");
+    tui.filter_command("Rename project");
     tui.click_text("Rename project");
     tui.wait_for("Cancel");
     tui.send("\x1b[200~TUI 项目\x1b[201~\r".as_bytes());
     tui.wait_until(|text| text.contains("TUI 项目") && !text.contains("Cancel"));
-    tui.send(b"\x10");
-    tui.wait_for("Archive project");
+    tui.filter_command("Archive project");
     tui.click_text("Archive project");
     tui.wait_for("Archiving prevents");
     tui.send(b"\r"); // Default focus cancels instead of archiving.
     tui.wait_until(|text| !text.contains("Cancel") && text.contains("TUI 项目"));
-    tui.send(b"\x10");
-    tui.wait_for("Archive project");
+    tui.filter_command("Archive project");
     tui.click_text("Archive project");
     tui.wait_for("Archiving prevents");
     tui.wait_until(|text| {
@@ -225,8 +222,7 @@ fn project_catalog_notifications_and_creation_use_host_project_identity() {
     });
     tui.click_last_text("Archive project");
     tui.wait_until(|text| text.contains("TUI 项目 · Archived") && !text.contains("Cancel"));
-    tui.send(b"\x10");
-    tui.wait_for("Restore project");
+    tui.filter_command("Restore project");
     tui.click_text("Restore project");
     tui.wait_for("Archiving prevents");
     tui.send(b"\t\r");
@@ -298,16 +294,14 @@ fn project_catalog_notifications_and_creation_use_host_project_identity() {
         target
     });
     tui.click_text("TUI 项目");
-    tui.send(b"\x10");
-    tui.wait_for("Relink project");
+    tui.filter_command("Relink project");
     tui.click_text("Relink project");
     tui.wait_for("Replacement absolute directory");
     tui.send(format!("\x1b[200~{}/missing\x1b[201~\r", directory.path().display()).as_bytes());
     tui.wait_for("Relink this project?");
     tui.send(b"\r"); // Reviewing defaults to Cancel, without a write.
     tui.wait_until(|text| text.contains("TUI 项目") && !text.contains("Cancel"));
-    tui.send(b"\x10");
-    tui.wait_for("Relink project");
+    tui.filter_command("Relink project");
     tui.click_text("Relink project");
     tui.wait_for("Replacement absolute directory");
     tui.send(format!("\x1b[200~{}/missing\x1b[201~\r", directory.path().display()).as_bytes());
@@ -346,8 +340,7 @@ fn project_catalog_notifications_and_creation_use_host_project_identity() {
         assert!(client.session(&session.id).await.unwrap().unwrap().revision > before.revision);
     });
     assert!(project_path.is_dir(), "relink changes metadata, not files");
-    tui.send(b"\x10");
-    tui.wait_for("Project locations");
+    tui.filter_command("Project locations");
     tui.click_text("Project locations");
     tui.wait_for("Preferred Host directory");
     tui.wait_for(browse_target.to_str().unwrap());
diff --git a/crates/cli/tests/integration/tui/recap.rs b/crates/cli/tests/integration/tui/recap.rs
index 1a5bc6a28f..536de33ef9 100644
--- a/crates/cli/tests/integration/tui/recap.rs
+++ b/crates/cli/tests/integration/tui/recap.rs
@@ -103,15 +103,13 @@ fn recap_uses_native_remote_preserves_draft_and_reads_saved_result_after_reopen(
     tui.click_text("Message…");
     tui.send(b"Keep this unsent draft");
     tui.wait_for("Keep this unsent draft");
-    tui.send(b"\x10");
-    tui.wait_for("Session recap");
+    tui.filter_command("Session recap");
     tui.click_text("Session recap");
     tui.wait_for("No recap has been saved");
     tui.send(b"\r"); // default focus closes; it cannot charge the model.
     tui.wait_until(|s| !s.contains("No recap has been saved"));
     assert_eq!(calls.load(Ordering::SeqCst), 1);
-    tui.send(b"\x10");
-    tui.wait_for("Session recap");
+    tui.filter_command("Session recap");
     tui.click_text("Session recap");
     tui.wait_for("No recap has been saved");
     tui.click_text("Generate new");
@@ -152,8 +150,7 @@ fn recap_uses_native_remote_preserves_draft_and_reads_saved_result_after_reopen(
     let mut reopened = Pty::spawn(&["--root", host.root.to_str().unwrap()]);
     reopened.wait_for("Keep this unsent draft");
     reopened.wait_for("Original verified answer.");
-    reopened.send(b"\x10");
-    reopened.wait_for("Session recap");
+    reopened.filter_command("Session recap");
     reopened.click_text("Session recap");
     reopened.wait_for("Recap confirmed:");
     assert_eq!(calls.load(Ordering::SeqCst), 2);
diff --git a/crates/cli/tests/integration/tui/recovery.rs b/crates/cli/tests/integration/tui/recovery.rs
index 02fef1dba0..82fd154555 100644
--- a/crates/cli/tests/integration/tui/recovery.rs
+++ b/crates/cli/tests/integration/tui/recovery.rs
@@ -75,8 +75,7 @@ fn recover(after_acceptance: bool) {
             tui.send(b"\x1b[200~ storage offline edit\x1b[201~");
             tui.wait_for("storage offline edit");
             std::fs::remove_dir(&checkpoint).unwrap();
-            tui.send(b"\x10");
-            tui.wait_for("Retry original message");
+            tui.filter_command("Retry original message");
             assert!(proxy.requests.lock().unwrap().is_empty(), "storage recovery must not automatically resend");
             tui.click_text("Retry original message");
         }
@@ -105,8 +104,7 @@ fn recover(after_acceptance: bool) {
         tui.wait_for("plus new edits");
         tui.wait_for("Delivery uncertain");
         assert_eq!(proxy.requests.lock().unwrap().len(), 1, "reopening must not automatically replay");
-        tui.send(b"\x10");
-        tui.wait_for("Retry original message");
+        tui.filter_command("Retry original message");
         // wait_for already waits for a complete synchronized frame. An unrelated
         // last command may be below the viewport when a live turn adds actions.
         tui.click_text("Retry original message");
diff --git a/crates/cli/tests/integration/tui/references.rs b/crates/cli/tests/integration/tui/references.rs
index 23d81932ba..88591b96ee 100644
--- a/crates/cli/tests/integration/tui/references.rs
+++ b/crates/cli/tests/integration/tui/references.rs
@@ -76,8 +76,7 @@ fn host_directory_reference_survives_restart_and_sends_without_text_or_registrat
     tui.wait_for("Reference session");
     tui.click_text("Reference session");
     tui.wait_for("No messages yet.");
-    tui.send(b"\x10");
-    tui.wait_for("Host directories");
+    tui.filter_command("Host directories");
     tui.click_text("Host directories");
     tui.wait_for("Published folders");
     tui.click_text("Published folders");
diff --git a/crates/cli/tests/integration/tui/removal.rs b/crates/cli/tests/integration/tui/removal.rs
index 80438a8969..9ff81831a5 100644
--- a/crates/cli/tests/integration/tui/removal.rs
+++ b/crates/cli/tests/integration/tui/removal.rs
@@ -62,8 +62,7 @@ fn deletion_confirms_cas_and_remote_retirement_preserves_recoverable_draft() {
     tui.wait_for("Local deletion");
     tui.click_text("Local deletion");
     tui.wait_for("Message…");
-    tui.send(b"\x10");
-    tui.wait_for("Delete session");
+    tui.filter_command("Delete session");
     tui.click_text("Delete session");
     tui.wait_for("Permanently deletes");
     tui.send(b"\r"); // Cancel is the initial focus.
@@ -74,8 +73,7 @@ fn deletion_confirms_cas_and_remote_retirement_preserves_recoverable_draft() {
             .unwrap()
             .is_some()
     );
-    tui.send(b"\x10");
-    tui.wait_for("Delete session");
+    tui.filter_command("Delete session");
     tui.click_text("Delete session");
     tui.wait_for("Permanently deletes");
     runtime.block_on(async {
@@ -105,8 +103,7 @@ fn deletion_confirms_cas_and_remote_retirement_preserves_recoverable_draft() {
     tui.wait_until(|s| {
         s.contains("Concurrent deletion target") && !s.contains("This session changed elsewhere.")
     });
-    tui.send(b"\x10");
-    tui.wait_for("Delete session");
+    tui.filter_command("Delete session");
     tui.click_text("Delete session");
     tui.wait_for("Permanently deletes");
     tui.click_last_text("Delete");
@@ -129,8 +126,7 @@ fn deletion_confirms_cas_and_remote_retirement_preserves_recoverable_draft() {
     tui.wait_for("Message…");
     tui.send(b"recover this local draft");
     tui.wait_for("recover this local draft");
-    tui.send(b"\x10");
-    tui.wait_for("Delete session");
+    tui.filter_command("Delete session");
     tui.click_text("Delete session");
     tui.wait_for("Permanently deletes");
     runtime.block_on(async {
diff --git a/crates/cli/tests/integration/tui/revision.rs b/crates/cli/tests/integration/tui/revision.rs
index 5a2dfd21e1..99591b8d4f 100644
--- a/crates/cli/tests/integration/tui/revision.rs
+++ b/crates/cli/tests/integration/tui/revision.rs
@@ -110,8 +110,7 @@ fn revision_edits_ordered_inputs_preserves_attachments_and_reopens_without_resub
     tui.send(b"Composer stays here");
     tui.wait_for("Composer stays here");
     tui.click_text("Original reply.");
-    tui.send(b"\x10");
-    tui.wait_for("Revise this turn");
+    tui.filter_command("Revise this turn");
     tui.click_text("Revise this turn");
     tui.wait_for("Input  1 / 2");
     tui.send("\x1b[200~中文 \x1b[201~".as_bytes());
@@ -181,8 +180,7 @@ fn revision_edits_ordered_inputs_preserves_attachments_and_reopens_without_resub
     let mut reopened = Pty::spawn(&["--root", host.root.to_str().unwrap()]);
     reopened.wait_for("Composer stays here");
     reopened.wait_for("Original reply.");
-    reopened.send(b"\x10");
-    reopened.wait_for("Continue revision");
+    reopened.filter_command("Continue revision");
     reopened.click_text("Continue revision");
     reopened.wait_for("Input  2 / 2");
     reopened.send(b"\x1b[200~revised \x1b[201~");
@@ -275,8 +273,7 @@ fn revision_edits_ordered_inputs_preserves_attachments_and_reopens_without_resub
     );
     let mut recovered = Pty::spawn(&["--root", host.root.to_str().unwrap()]);
     recovered.wait_for("Original reply.");
-    recovered.send(b"\x10");
-    recovered.wait_for("Continue revision");
+    recovered.filter_command("Continue revision");
     recovered.click_text("Continue revision");
     recovered.wait_for("Result not confirmed.");
     recovered.click_text("Check result");
diff --git a/crates/cli/tests/integration/tui/skills.rs b/crates/cli/tests/integration/tui/skills.rs
index 0bb013a0c6..dfe3896c8a 100644
--- a/crates/cli/tests/integration/tui/skills.rs
+++ b/crates/cli/tests/integration/tui/skills.rs
@@ -75,10 +75,13 @@ fn skills_candidates_page_without_execution_and_restore_exact_selection_before_s
     tui.wait_for("Candidate session");
     tui.click_text("Candidate session");
     tui.wait_for("No messages yet.");
-    tui.send(b"\x10");
-    tui.wait_for("Commands");
+    tui.filter_command("Skills");
     tui.click_text("Skills");
     tui.wait_for("Candidate 000");
+    assert!(
+        !tui.screen.snapshot().unwrap().cursor.visible,
+        "composer cursor must not leak through the Skills picker"
+    );
     tui.send(b"\x1b[C"); // Next provider page, not local list scrolling.
     tui.wait_for("Candidate 128");
     tui.click_text("Candidate 128");
diff --git a/crates/tui/locales/en.ftl b/crates/tui/locales/en.ftl
index 3f2ce5db24..6f67699b00 100644
--- a/crates/tui/locales/en.ftl
+++ b/crates/tui/locales/en.ftl
@@ -142,6 +142,8 @@ footer-back = < Back
 footer-forward = Forward >
 footer-quit = Ctrl+Q Quit
 palette-title = Commands · Esc closes
+palette-filter = Search commands…
+palette-empty = No matching commands.
 route-session = Session
 sessions-loading = Loading sessions…
 sessions-failed = Could not load sessions. Retry when the Host is available.
diff --git a/crates/tui/locales/zh-CN.ftl b/crates/tui/locales/zh-CN.ftl
index 28ef21740b..fa4cc3ed82 100644
--- a/crates/tui/locales/zh-CN.ftl
+++ b/crates/tui/locales/zh-CN.ftl
@@ -142,6 +142,8 @@ footer-back = < 后退
 footer-forward = 前进 >
 footer-quit = Ctrl+Q 退出
 palette-title = 命令 · Esc 关闭
+palette-filter = 搜索命令…
+palette-empty = 没有匹配的命令。
 route-session = 会话
 sessions-loading = 正在加载会话…
 sessions-failed = 无法加载会话,请在 Host 可用后重试。
diff --git a/crates/tui/locales/zh-TW.ftl b/crates/tui/locales/zh-TW.ftl
index 1a5a8e512a..95bc7caa0f 100644
--- a/crates/tui/locales/zh-TW.ftl
+++ b/crates/tui/locales/zh-TW.ftl
@@ -142,6 +142,8 @@ footer-back = < 返回
 footer-forward = 前進 >
 footer-quit = Ctrl+Q 離開
 palette-title = 命令 · Esc 關閉
+palette-filter = 搜尋命令…
+palette-empty = 沒有符合的命令。
 route-session = 工作階段
 sessions-loading = 正在載入工作階段…
 sessions-failed = 無法載入工作階段,請在 Host 可用後重試。
diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs
index 8de0bb3a4d..5d5f6b114a 100644
--- a/crates/tui/src/app.rs
+++ b/crates/tui/src/app.rs
@@ -147,7 +147,7 @@ pub struct App {
     // Local to this Root/client instance. Reconnects must not discard drafts.
     pub drafts: HashMap<String, crate::editor::Editor>,
     pub palette: Option<usize>,
-    palette_commands: Vec<(Action, &'static str)>,
+    pub command_palette: crate::pages::commands::State,
     pub hits: Vec<Hit>,
     pub modal_area: Option<Rect>,
     pub hover: Option<Action>,
@@ -195,7 +195,7 @@ impl App {
             sending: HashMap::new(),
             drafts: HashMap::new(),
             palette: None,
-            palette_commands: Vec::new(),
+            command_palette: Default::default(),
             hits: Vec::new(),
             modal_area: None,
             hover: None,
@@ -215,7 +215,7 @@ impl App {
     }
     pub fn commands(&self) -> Vec<(Action, &'static str)> {
         if self.palette.is_some() {
-            return self.palette_commands.clone();
+            return self.command_palette.filtered(&self.i18n);
         }
         let mut commands = vec![
             (Action::Visit(Route::Workspace), "command-workspace"),
@@ -655,7 +655,7 @@ impl App {
                 self.invalidate_editor_geometry();
                 self.hover = None;
                 // Background updates may disable an action, never move its hit target.
-                self.palette_commands = self.commands();
+                self.command_palette = crate::pages::commands::State::new(self.commands());
                 self.palette = Some(0);
             }
             Action::ToggleTheme => self.theme.cycle(),
@@ -1065,6 +1065,7 @@ impl App {
         }
         self.management.invalidate_geometry();
         self.skills.invalidate_geometry();
+        self.command_palette.invalidate();
         self.management.oauth.invalidate_identity_geometry();
         self.branch.invalidate_geometry();
         self.recap.invalidate_geometry();
@@ -1268,6 +1269,14 @@ impl App {
             }
             return self.interaction_input(event);
         }
+        if self.palette.is_some()
+            && !matches!(
+                event,
+                Event::Resize(_, _) | Event::FocusGained | Event::FocusLost
+            )
+        {
+            return self.palette_input(event);
+        }
         if self.palette.is_none()
             && let Event::Key(key) = &event
             && key.kind != KeyEventKind::Release
@@ -1401,35 +1410,6 @@ impl App {
             Event::Key(key) if key.kind != KeyEventKind::Release => {
                 if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('q') {
                     Some(Action::Quit)
-                } else if let Some(selected) = self.palette {
-                    match key.code {
-                        KeyCode::Esc => {
-                            self.palette = None;
-                            None
-                        }
-                        KeyCode::Up => {
-                            self.palette = Some(selected.saturating_sub(1));
-                            None
-                        }
-                        KeyCode::Down => {
-                            self.palette = Some((selected + 1).min(self.commands().len() - 1));
-                            None
-                        }
-                        KeyCode::Home => {
-                            self.palette = Some(0);
-                            None
-                        }
-                        KeyCode::End => {
-                            self.palette = Some(self.commands().len().saturating_sub(1));
-                            None
-                        }
-                        KeyCode::Enter => {
-                            let action = self.commands()[selected].0.clone();
-                            self.palette = None;
-                            Some(action)
-                        }
-                        _ => return (false, None),
-                    }
                 } else if key.modifiers.contains(KeyModifiers::CONTROL)
                     && key.code == KeyCode::Char('f')
                     && matches!(self.navigation.current(), Route::Session(_))
@@ -1815,17 +1795,6 @@ impl App {
                         }
                         target
                     }
-                    MouseEventKind::ScrollDown | MouseEventKind::ScrollUp
-                        if self.palette.is_some() =>
-                    {
-                        let selected = self.palette.unwrap();
-                        self.palette = Some(if mouse.kind == MouseEventKind::ScrollDown {
-                            (selected + 1).min(self.commands().len() - 1)
-                        } else {
-                            selected.saturating_sub(1)
-                        });
-                        None
-                    }
                     MouseEventKind::ScrollDown | MouseEventKind::ScrollUp
                         if self.navigation.current() == Route::Connections
                             && matches!(
diff --git a/crates/tui/src/pages.rs b/crates/tui/src/pages.rs
index 6192b1156c..0dced15282 100644
--- a/crates/tui/src/pages.rs
+++ b/crates/tui/src/pages.rs
@@ -20,6 +20,7 @@
 pub mod attachments;
 pub mod branch;
 pub mod chat;
+pub mod commands;
 pub mod connections;
 pub mod interactions;
 pub mod manage;
diff --git a/crates/tui/src/pages/commands.rs b/crates/tui/src/pages/commands.rs
new file mode 100644
index 0000000000..69108ba412
--- /dev/null
+++ b/crates/tui/src/pages/commands.rs
@@ -0,0 +1,442 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use crate::{
+    app::{Action, App},
+    i18n::I18n,
+};
+use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
+use ratatui::{
+    Frame,
+    layout::{Alignment, Rect},
+    style::Style,
+    widgets::{Block, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
+};
+use unicode_segmentation::UnicodeSegmentation;
+use unicode_width::UnicodeWidthStr;
+
+#[derive(Default)]
+pub struct State {
+    items: Vec<(Action, &'static str)>,
+    query: String,
+    list: Option<Rect>,
+    top: usize,
+    dragging: bool,
+}
+
+impl State {
+    pub fn new(items: Vec<(Action, &'static str)>) -> Self {
+        Self {
+            items,
+            ..Self::default()
+        }
+    }
+    pub fn filtered(&self, i18n: &I18n) -> Vec<(Action, &'static str)> {
+        let query = self.query.to_lowercase();
+        self.items
+            .iter()
+            .filter(|(_, key)| {
+                let text = i18n.text(key).to_lowercase();
+                query.split_whitespace().all(|word| text.contains(word))
+            })
+            .cloned()
+            .collect()
+    }
+    pub fn invalidate(&mut self) {
+        self.list = None;
+        self.dragging = false;
+    }
+    fn insert(&mut self, text: &str) {
+        for ch in text.chars().filter(|ch| !ch.is_control()) {
+            if self.query.len() + ch.len_utf8() > 512 {
+                break;
+            }
+            self.query.push(ch);
+        }
+    }
+}
+
+impl App {
+    pub(crate) fn palette_input(&mut self, event: Event) -> (bool, Option<Action>) {
+        let items = self.commands();
+        let selected = self.palette.unwrap_or_default();
+        let last = items.len().saturating_sub(1);
+        let mut action = None;
+        let mut changed_query = false;
+        match event {
+            Event::Key(key) if key.kind != KeyEventKind::Release => {
+                self.command_palette.dragging = false;
+                if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('q') {
+                    return (true, Some(Action::Quit));
+                }
+                match key.code {
+                    KeyCode::Esc => self.palette = None,
+                    KeyCode::Up => self.palette = Some(selected.saturating_sub(1)),
+                    KeyCode::Down => self.palette = Some((selected + 1).min(last)),
+                    KeyCode::Home => self.palette = Some(0),
+                    KeyCode::End => self.palette = Some(last),
+                    KeyCode::PageUp | KeyCode::PageDown => {
+                        let page = self
+                            .command_palette
+                            .list
+                            .map_or(1, |r| usize::from(r.height));
+                        self.palette = Some(if key.code == KeyCode::PageUp {
+                            selected.saturating_sub(page)
+                        } else {
+                            (selected + page).min(last)
+                        });
+                    }
+                    KeyCode::Enter if self.command_palette.list.is_some() => {
+                        action = items.get(selected).map(|(action, _)| action.clone())
+                    }
+                    KeyCode::Backspace => {
+                        if let Some((index, _)) = self
+                            .command_palette
+                            .query
+                            .grapheme_indices(true)
+                            .next_back()
+                        {
+                            self.command_palette.query.truncate(index);
+                            changed_query = true;
+                        }
+                    }
+                    KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
+                        self.command_palette.query.clear();
+                        changed_query = true;
+                    }
+                    KeyCode::Char(ch)
+                        if !key
+                            .modifiers
+                            .intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
+                    {
+                        self.command_palette.insert(&ch.to_string());
+                        changed_query = true;
+                    }
+                    _ => return (false, None),
+                }
+            }
+            Event::Paste(text) => {
+                self.command_palette.insert(&text);
+                changed_query = true;
+            }
+            Event::Mouse(mouse) => {
+                let state = &mut self.command_palette;
+                if let Some(list) = state.list {
+                    match mouse.kind {
+                        MouseEventKind::Up(MouseButton::Left) => state.dragging = false,
+                        MouseEventKind::ScrollDown | MouseEventKind::ScrollUp
+                            if list.contains((mouse.column, mouse.row).into()) =>
+                        {
+                            state.dragging = false;
+                            self.palette = Some(if mouse.kind == MouseEventKind::ScrollDown {
+                                (selected + 1).min(last)
+                            } else {
+                                selected.saturating_sub(1)
+                            });
+                        }
+                        MouseEventKind::Down(MouseButton::Left)
+                        | MouseEventKind::Drag(MouseButton::Left)
+                            if state.dragging
+                                || (items.len() > usize::from(list.height)
+                                    && mouse.column == list.right() - 1
+                                    && list.contains((mouse.column, mouse.row).into())) =>
+                        {
+                            state.dragging = true;
+                            let position = usize::from(
+                                mouse
+                                    .row
+                                    .saturating_sub(list.y)
+                                    .min(list.height.saturating_sub(1)),
+                            );
+                            state.top = position
+                                * items.len().saturating_sub(usize::from(list.height))
+                                / usize::from(list.height.saturating_sub(1).max(1));
+                            self.palette = Some(state.top);
+                        }
+                        MouseEventKind::Down(MouseButton::Left) => {
+                            action = self
+                                .hits
+                                .iter()
+                                .find(|h| h.area.contains((mouse.column, mouse.row).into()))
+                                .map(|h| h.action.clone());
+                        }
+                        _ => return (false, None),
+                    }
+                } else {
+                    return (false, None);
+                }
+            }
+            _ => return (false, None),
+        }
+        if changed_query {
+            self.palette = Some(0);
+            self.command_palette.top = 0;
+            // Query edits retire row geometry before another click can arrive.
+            self.command_palette.invalidate();
+            self.hits.clear();
+        }
+        if action.as_ref().is_some_and(|action| self.enabled(action)) {
+            self.palette = None;
+            self.hits.clear();
+            return (true, action.and_then(|action| self.apply(action)));
+        }
+        (true, None)
+    }
+}
+
+pub fn draw(frame: &mut Frame<'_>, app: &mut App, area: Rect, base: Style) {
+    app.hits.clear();
+    if area.width < 24 || area.height < 8 {
+        app.command_palette.invalidate();
+        return;
+    }
+    let width = area.width.saturating_sub(4).min(64);
+    let items = app.commands();
+    // Shrink only the results below the anchored search field.
+    let maximum_height = (app.command_palette.items.len() as u16 + 6)
+        .min(26)
+        .min(area.height - 2);
+    let height = (items.len().max(1) as u16 + 6).min(maximum_height);
+    let popup = Rect::new(
+        area.x + (area.width - width) / 2,
+        area.y + (area.height - maximum_height) / 2,
+        width,
+        height,
+    );
+    app.modal_area = Some(popup);
+    crate::view::clear_overlay(frame, popup);
+    let colors = app.theme.colors();
+    let block = Block::bordered()
+        .border_type(if app.chrome.ascii {
+            ratatui::widgets::BorderType::Plain
+        } else {
+            ratatui::widgets::BorderType::Rounded
+        })
+        .title(app.i18n.text("palette-title"))
+        .title_alignment(Alignment::Center)
+        .style(base)
+        .border_style(Style::default().fg(colors.accent));
+    let inner = block.inner(popup);
+    frame.render_widget(block, popup);
+    let search = Rect::new(inner.x + 1, inner.y + 1, inner.width.saturating_sub(2), 1);
+    let query = &app.command_palette.query;
+    let label = if query.is_empty() {
+        app.i18n.text("palette-filter")
+    } else {
+        query.clone()
+    };
+    let offset = query
+        .width()
+        .saturating_sub(usize::from(search.width.saturating_sub(1)));
+    frame.render_widget(
+        Paragraph::new(label)
+            .style(Style::default().fg(if query.is_empty() {
+                colors.subtle
+            } else {
+                colors.foreground
+            }))
+            .scroll((0, offset as u16)),
+        search,
+    );
+    frame.set_cursor_position((
+        search.x + query.width().saturating_sub(offset) as u16,
+        search.y,
+    ));
+    let list = Rect::new(
+        inner.x + 1,
+        inner.y + 3,
+        inner.width.saturating_sub(2),
+        inner.height.saturating_sub(3),
+    );
+    let selected = app
+        .palette
+        .unwrap_or_default()
+        .min(items.len().saturating_sub(1));
+    app.palette = Some(selected);
+    let capacity = usize::from(list.height).max(1);
+    let state = &mut app.command_palette;
+    state.list = Some(list);
+    state.top = state
+        .top
+        .min(selected)
+        .max(selected.saturating_sub(capacity - 1))
+        .min(items.len().saturating_sub(capacity));
+    let top = state.top;
+    let scrolling = items.len() > capacity;
+    if items.is_empty() {
+        frame.render_widget(
+            Paragraph::new(app.i18n.text("palette-empty"))
+                .style(Style::default().fg(colors.subtle)),
+            list,
+        );
+    }
+    for (index, (action, key)) in items.iter().enumerate().skip(top).take(capacity) {
+        let row = Rect::new(
+            list.x,
+            list.y + (index - top) as u16,
+            list.width.saturating_sub(if scrolling { 2 } else { 0 }),
+            1,
+        );
+        crate::view::list_item(
+            frame,
+            app,
+            row,
+            &app.i18n.text(key),
+            action.clone(),
+            index == selected,
+        );
+    }
+    if scrolling {
+        let widget = Scrollbar::new(ScrollbarOrientation::VerticalRight)
+            .begin_symbol(None)
+            .end_symbol(None)
+            .track_symbol(Some(app.chrome.symbol("│", "|")))
+            .thumb_symbol(app.chrome.symbol("┃", "#"))
+            .track_style(Style::default().fg(colors.subtle))
+            .thumb_style(Style::default().fg(colors.accent));
+        let mut state = ScrollbarState::new(items.len() - capacity + 1)
+            .position(top)
+            .viewport_content_length(capacity);
+        frame.render_stateful_widget(widget, list, &mut state);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        i18n::{Locale, LocalePreference},
+        navigation::Route,
+    };
+    use crossterm::event::{KeyEvent, MouseEvent};
+    use ratatui::{Terminal, backend::TestBackend};
+    fn frame(app: &mut App, width: u16, height: u16) {
+        Terminal::new(TestBackend::new(width, height))
+            .unwrap()
+            .draw(|f| crate::view::draw(f, app))
+            .unwrap();
+    }
+    fn key(code: KeyCode) -> Event {
+        Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
+    }
+    fn mouse(kind: MouseEventKind, x: u16, y: u16) -> Event {
+        Event::Mouse(MouseEvent {
+            kind,
+            column: x,
+            row: y,
+            modifiers: KeyModifiers::NONE,
+        })
+    }
+    #[test]
+    fn search_and_drag_preserve_captured_commands_drafts_and_published_geometry() {
+        for locale in [Locale::En, Locale::ZhCn, Locale::ZhTw] {
+            let mut app = App::new(
+                "/unused".into(),
+                I18n::new(LocalePreference::Explicit(locale), locale),
+            );
+            app.apply(Action::Visit(Route::Session("draft".into())));
+            app.drafts
+                .get_mut("draft")
+                .unwrap()
+                .insert("preserved draft");
+            app.apply(Action::Palette);
+            frame(&mut app, 52, 18);
+            let list = app.command_palette.list.unwrap();
+            assert!(!app.input(mouse(MouseEventKind::Moved, list.x, list.y)).0);
+            let count = app.commands().len();
+            assert!(count > usize::from(list.height));
+            assert!(
+                app.input(mouse(
+                    MouseEventKind::Down(MouseButton::Left),
+                    list.right() - 1,
+                    list.bottom() - 1
+                ))
+                .1
+                .is_none()
+            );
+            frame(&mut app, 52, 18);
+            assert_eq!(app.command_palette.top, count - usize::from(list.height));
+            app.input(Event::Resize(80, 28));
+            app.input(mouse(
+                MouseEventKind::Drag(MouseButton::Left),
+                list.right() - 1,
+                list.y,
+            ));
+            assert!(app.command_palette.list.is_none());
+            frame(&mut app, 80, 28);
+            let stale = app.command_palette.list.unwrap();
+            app.input(key(KeyCode::Home));
+            assert_eq!(app.palette, Some(0));
+            app.input(key(KeyCode::End));
+            assert_eq!(app.palette, Some(count - 1));
+            app.input(Event::Paste("not-a-command 👨‍👩‍👧‍👦".into()));
+            app.input(key(KeyCode::Backspace));
+            assert_eq!(app.command_palette.query, "not-a-command ");
+            frame(&mut app, 80, 28);
+            assert!(app.commands().is_empty());
+            assert!(app.input(key(KeyCode::Enter)).1.is_none());
+            assert!(app.palette.is_some());
+            app.input(Event::Key(KeyEvent::new(
+                KeyCode::Char('u'),
+                KeyModifiers::CONTROL,
+            )));
+            let query = app.i18n.text("command-settings").to_lowercase();
+            app.input(Event::Paste(query));
+            assert_eq!(
+                app.commands(),
+                vec![(Action::Visit(Route::Settings), "command-settings")]
+            );
+            // Old geometry cannot activate a different row after filtering.
+            app.input(mouse(
+                MouseEventKind::Down(MouseButton::Left),
+                stale.x,
+                stale.y,
+            ));
+            assert_eq!(app.navigation.current(), Route::Session("draft".into()));
+            frame(&mut app, 80, 28);
+            assert_eq!(app.command_palette.list.unwrap().y, stale.y);
+            assert_eq!(app.modal_area.unwrap().height, 7);
+            let stable = app.commands();
+            app.connection = crate::app::ConnectionState::Failed("offline".into());
+            assert_eq!(app.commands(), stable);
+            let hit = app
+                .hits
+                .iter()
+                .find(|h| h.action == Action::Visit(Route::Settings))
+                .unwrap()
+                .area;
+            app.input(mouse(
+                MouseEventKind::Down(MouseButton::Left),
+                hit.x + 2,
+                hit.y,
+            ));
+            assert!(app.palette.is_none());
+            assert_eq!(app.navigation.current(), Route::Settings);
+            assert_eq!(app.drafts["draft"].text(), "preserved draft");
+            app.apply(Action::Palette);
+            assert!(app.command_palette.query.is_empty());
+            app.input(Event::Paste("🦀".repeat(200)));
+            assert!(app.command_palette.query.len() <= 512);
+            frame(&mut app, 20, 6);
+            assert!(app.input(key(KeyCode::Enter)).1.is_none());
+            app.input(key(KeyCode::Esc));
+            assert!(app.palette.is_none());
+        }
+    }
+}
diff --git a/crates/tui/src/pages/sending.rs b/crates/tui/src/pages/sending.rs
index d3dc51e76d..45a87b0d6f 100644
--- a/crates/tui/src/pages/sending.rs
+++ b/crates/tui/src/pages/sending.rs
@@ -461,6 +461,9 @@ mod tests {
             assert!(app.submission().is_none());
             app.apply(Action::Palette);
             app.palette = Some(index);
+            terminal
+                .draw(|frame| crate::view::draw(frame, &mut app))
+                .unwrap();
             assert_eq!(
                 app.input(crossterm::event::Event::Key(
                     crossterm::event::KeyEvent::new(
diff --git a/crates/tui/src/view.rs b/crates/tui/src/view.rs
index 9d4aa9f6f1..a8cf667a8e 100644
--- a/crates/tui/src/view.rs
+++ b/crates/tui/src/view.rs
@@ -409,8 +409,8 @@ pub fn draw(frame: &mut Frame<'_>, app: &mut App) {
         crate::pages::interactions::draw(frame, app, area, base);
     } else if app.queue.edit.is_some() {
         queue::edit(frame, app, area, base);
-    } else if let Some(selected) = app.palette {
-        draw_palette(frame, app, area, base, selected);
+    } else if app.palette.is_some() {
+        crate::pages::commands::draw(frame, app, area, base);
     } else if app.tooltip_visible() {
         draw_tooltip(frame, app, area, base);
     }
@@ -814,44 +814,6 @@ fn page_lines(app: &App) -> Vec<Line<'static>> {
     }
 }
 
-fn draw_palette(frame: &mut Frame<'_>, app: &mut App, area: Rect, base: Style, selected: usize) {
-    app.hits.clear();
-    let width = area.width.min(64).saturating_sub(4);
-    let height = (app.commands().len() as u16 + 2).min(area.height.saturating_sub(2));
-    let popup = Rect::new(
-        area.x + (area.width - width) / 2,
-        area.y + (area.height - height) / 2,
-        width,
-        height,
-    );
-    app.modal_area = Some(popup);
-    clear_overlay(frame, popup);
-    let block = Block::bordered()
-        .title(app.i18n.text("palette-title"))
-        .style(base)
-        .border_style(Style::default().fg(app.theme.colors().accent));
-    let inner = block.inner(popup);
-    frame.render_widget(block, popup);
-    let offset = (selected + 1).saturating_sub(inner.height as usize);
-    for (index, (action, key)) in app
-        .commands()
-        .into_iter()
-        .enumerate()
-        .skip(offset)
-        .take(inner.height as usize)
-    {
-        let row = Rect::new(inner.x, inner.y + (index - offset) as u16, inner.width, 1);
-        list_item(
-            frame,
-            app,
-            row,
-            &app.i18n.text(key),
-            action,
-            selected == index,
-        );
-    }
-}
-
 pub(crate) fn button(
     frame: &mut Frame<'_>,
     app: &mut App,
diff --git a/crates/tui/src/view/session.rs b/crates/tui/src/view/session.rs
index a771d45e49..0ae7083104 100644
--- a/crates/tui/src/view/session.rs
+++ b/crates/tui/src/view/session.rs
@@ -167,6 +167,12 @@ pub(super) fn draw(frame: &mut Frame<'_>, app: &mut App, area: Rect, id: &str) {
     let focused = app.focus == Focus::Composer
         && app.chat.view.search.is_none()
         && app.palette.is_none()
+        && app.theme.editor.is_none()
+        && app.attachments.dialog.is_none()
+        && app.skills.dialog.is_none()
+        && !app.branch.visible
+        && !app.revision.visible
+        && !app.recap.visible
         && !app.interactions.visible
         && app.management.dialog.is_none()
         && app.onboarding.dialog.is_none()
diff --git a/crates/tui/src/view/session/search.rs b/crates/tui/src/view/session/search.rs
index 1688bf02d0..8eeee256ce 100644
--- a/crates/tui/src/view/session/search.rs
+++ b/crates/tui/src/view/session/search.rs
@@ -85,7 +85,17 @@ pub(super) fn draw(frame: &mut Frame<'_>, app: &mut App, area: Rect) {
             .saturating_sub(prefix_width + count_width + controls + 1),
         1,
     );
-    let focused = app.palette.is_none() && !app.interactions.visible && app.queue.edit.is_none();
+    let focused = app.palette.is_none()
+        && app.theme.editor.is_none()
+        && app.attachments.dialog.is_none()
+        && app.skills.dialog.is_none()
+        && !app.branch.visible
+        && !app.revision.visible
+        && !app.recap.visible
+        && !app.interactions.visible
+        && app.management.dialog.is_none()
+        && app.onboarding.dialog.is_none()
+        && app.queue.edit.is_none();
     app.chat
         .view
         .search


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


(maka) 04/05: merge: integrate account reasoning policy with native TUI

Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 606de60b88ceaf72df8f1eeaacf57eae06de4912
Merge: d01501b44f 9c651da46a
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Thu Sep 24 05:00:56 2026 +0800

    merge: integrate account reasoning policy with native TUI
    
    Generated-by: Codex

 crates/model/src/connection/models.rs              |   5 +-
 crates/model/src/connection/models/subscription.rs | 119 +-----------
 crates/providers/src/codex.rs                      |   2 +
 crates/providers/src/codex/inventory.rs            | 199 +++++++++++++++++++++
 crates/providers/src/codex/provider.rs             |  90 +++-------
 crates/providers/tests/codex.rs                    |  25 ++-
 crates/providers/tests/codex/inventory.rs          | 145 +++++++++++++++
 crates/responses/src/request.rs                    |   7 -
 packages/plugin-sdk/src/providers.ts               |   5 +-
 9 files changed, 396 insertions(+), 201 deletions(-)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


(maka) 01/05: test(cli): verify candidate lifecycle through the native Client

Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git

commit dec572c3c9d40ee4fdccb02244905a5892acf6ec
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Thu Sep 24 04:42:02 2026 +0800

    test(cli): verify candidate lifecycle through the native Client
    
    Generated-by: Codex
---
 crates/cli/tests/integration/candidate.rs | 179 +++++++++++++++++++++++++-----
 1 file changed, 150 insertions(+), 29 deletions(-)

diff --git a/crates/cli/tests/integration/candidate.rs b/crates/cli/tests/integration/candidate.rs
index d0f5497ba3..684a60b64f 100644
--- a/crates/cli/tests/integration/candidate.rs
+++ b/crates/cli/tests/integration/candidate.rs
@@ -17,8 +17,14 @@
  * under the License.
  */
 
+use maka_client::{Client, Operations};
 use maka_event_log::root::{RootNamespaces, RootOwner};
+use maka_protocol::{
+    Operation,
+    handshake::{ClientHello, HostHandshake, Replacement, Takeover},
+};
 use serde_json::Value;
+use serde_json::json;
 use std::{
     fs,
     io::Write,
@@ -33,18 +39,29 @@ fn candidate_preserves_root_authority_and_drains_on_owner_loss_or_released_idle(
     let namespaces = RootNamespaces::for_current_account().unwrap();
     let mut fixture = CandidateFixture::new(directory.path().join("root"));
 
+    let runtime = tokio::runtime::Runtime::new().unwrap();
+    let mut predecessor = None;
     enum End {
         OwnerLoss,
         ReleasedIdle,
         Retirement,
         Standalone,
+        Takeover,
+        Successor,
     }
     for end in [
         End::OwnerLoss,
         End::ReleasedIdle,
         End::Retirement,
         End::Standalone,
+        End::Takeover,
+        End::Successor,
     ] {
+        let generation = if matches!(end, End::Successor) {
+            "native-cli-successor"
+        } else {
+            "native-cli-test"
+        };
         let mut command = Command::new(env!("CARGO_BIN_EXE_maka"));
         if matches!(end, End::Standalone) {
             command.args(["host", "serve", "--root"]).arg(&fixture.root);
@@ -58,7 +75,7 @@ fn candidate_preserves_root_authority_and_drains_on_owner_loss_or_released_idle(
                     "--startup-attempt-id",
                     &uuid::Uuid::new_v4().to_string(),
                     "--generation",
-                    "native-cli-test",
+                    generation,
                     "--idle-grace-ms",
                     "1000",
                     "--owner-stdin",
@@ -84,9 +101,12 @@ fn candidate_preserves_root_authority_and_drains_on_owner_loss_or_released_idle(
             }
         );
         if !matches!(end, End::Standalone) {
-            assert_eq!(registration["generation"], "native-cli-test");
+            assert_eq!(registration["generation"], generation);
         }
         assert!(RootOwner::open(&fixture.root, &namespaces).is_err());
+        if matches!(end, End::Successor) {
+            assert_ne!(predecessor.as_ref(), Some(&registration["hostEpoch"]));
+        }
 
         // Initialization verifies identity without competing for an active writer lease.
         let init = Command::new(env!("CARGO_BIN_EXE_maka"))
@@ -114,21 +134,18 @@ fn candidate_preserves_root_authority_and_drains_on_owner_loss_or_released_idle(
             fixture.child.as_mut().unwrap().stdin = Some(stdin);
         }
 
-        let probe = Command::new("node")
-            .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/client.mjs"))
-            .args([
-                "--socket",
-                registration["endpoint"].as_str().unwrap(),
-                "--root-id",
-                &fixture.root_id,
-            ])
-            .output()
-            .unwrap();
-        assert!(
-            probe.status.success(),
-            "{}",
-            String::from_utf8_lossy(&probe.stderr)
-        );
+        runtime.block_on(async {
+            let (client, _notifications) = connect(&fixture.root).await;
+            let status = client
+                .request(Operation::HostStatus, json!({}))
+                .await
+                .unwrap();
+            assert_eq!(status["state"], "ready");
+            assert_eq!(status["hostEpoch"], registration["hostEpoch"]);
+            assert_eq!(client.identity.root_id, fixture.root_id);
+            client.disconnect();
+            client.closed().await;
+        });
 
         let status = Command::new(env!("CARGO_BIN_EXE_maka"))
             .args(["host", "status", "--root"])
@@ -143,7 +160,7 @@ fn candidate_preserves_root_authority_and_drains_on_owner_loss_or_released_idle(
         let status: Value = serde_json::from_slice(&status.stdout).unwrap();
         assert_eq!(status["hostEpoch"], registration["hostEpoch"]);
         assert_eq!(status["state"], "ready");
-        if matches!(end, End::Retirement | End::Standalone) {
+        if matches!(end, End::Retirement | End::Standalone | End::Successor) {
             let stale = Command::new(env!("CARGO_BIN_EXE_maka"))
                 .args(["host", "retire", "--root"])
                 .arg(&fixture.root)
@@ -178,6 +195,10 @@ fn candidate_preserves_root_authority_and_drains_on_owner_loss_or_released_idle(
             assert_eq!(receipt["kind"], "prepared", "{receipt}");
             assert_eq!(receipt["pid"], registration["pid"]);
         }
+        if matches!(end, End::Takeover) {
+            runtime.block_on(verify_takeover(&fixture.root, &registration));
+            predecessor = Some(registration["hostEpoch"].clone());
+        }
         if matches!(end, End::OwnerLoss) {
             drop(fixture.child.as_mut().unwrap().stdin.take());
         }
@@ -196,21 +217,121 @@ fn candidate_preserves_root_authority_and_drains_on_owner_loss_or_released_idle(
         assert_eq!(reopened.root_id(), fixture.root_id);
         drop(reopened);
     }
-    let discovery = Command::new("node")
-        .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/client.mjs"))
-        .args(["--native-candidate", env!("CARGO_BIN_EXE_maka"), "--root"])
-        .arg(&fixture.root)
-        .output()
-        .unwrap();
-    assert!(
-        discovery.status.success(),
-        "{}",
-        String::from_utf8_lossy(&discovery.stderr)
-    );
     assert!(!fixture.registration.exists());
     drop(RootOwner::open(&fixture.root, &namespaces).unwrap());
 }
 
+async fn connect(
+    root: &Path,
+) -> (
+    Client,
+    tokio::sync::mpsc::Receiver<maka_client::Notification>,
+) {
+    let discovery = maka_client::local::read_discovery(root).unwrap();
+    Client::connect(
+        maka_client::local::open_stream(&discovery.endpoint)
+            .await
+            .unwrap(),
+        &discovery.root_id,
+        &discovery.host_epoch,
+        Operations,
+    )
+    .await
+    .unwrap()
+}
+
+async fn verify_takeover(root: &Path, registration: &Value) {
+    tokio::time::timeout(Duration::from_secs(10), async {
+        let (client, _notifications) = connect(root).await;
+        let (observer, _observer_notifications) = connect(root).await;
+        assert_eq!(client.identity.host_epoch, observer.identity.host_epoch);
+        let diagnostics = client
+            .request(Operation::HostDiagnosticsQuery, json!({}))
+            .await
+            .unwrap();
+        assert_eq!(diagnostics["hostEpoch"], registration["hostEpoch"]);
+        assert_eq!(diagnostics["pid"], registration["pid"]);
+        assert_eq!(diagnostics["activeOperations"], 0);
+        assert_eq!(diagnostics["activeResidencies"], 0);
+        assert_eq!(diagnostics["connections"], 2);
+        assert_eq!(diagnostics["upgradeBlockingActivity"], true);
+        assert!(
+            diagnostics["logs"]
+                .as_array()
+                .unwrap()
+                .iter()
+                .any(|v| v.as_str().unwrap().contains("Host ready"))
+        );
+        assert!(matches!(
+            client
+                .request(
+                    Operation::HostUpgradePrepare,
+                    json!({
+                        "expectedHostEpoch":"stale", "allowInterruptActiveTasks":true
+                    })
+                )
+                .await,
+            Err(maka_client::RequestFailure::Rejected(maka_client::ClientError::Rejected(error)))
+                if error.code == maka_protocol::OperationErrorCode::OperationConflict
+        ));
+        let busy = client.request(Operation::HostUpgradePrepare, json!({
+            "expectedHostEpoch":registration["hostEpoch"], "allowInterruptActiveTasks":false,
+            "allowCooperativeHandoff":true
+        })).await.unwrap();
+        assert_eq!(busy, json!({"kind":"active_tasks"}));
+        assert!(matches!(
+            challenge(registration).await,
+            HostHandshake::Incompatible {
+                replacement: Replacement::BlockedByResidency,
+                ..
+            }
+        ));
+        observer.disconnect();
+        observer.closed().await;
+        client.disconnect();
+        client.closed().await;
+        loop {
+            match challenge(registration).await {
+                HostHandshake::Draining { host_epoch, .. } => {
+                    assert_eq!(host_epoch, registration["hostEpoch"]);
+                    break;
+                }
+                HostHandshake::Incompatible {
+                    replacement: Replacement::BlockedByResidency,
+                    ..
+                } => tokio::task::yield_now().await,
+                other => panic!("Unexpected takeover: {other:?}"),
+            }
+        }
+    })
+    .await
+    .expect("native candidate takeover timed out");
+}
+
+async fn challenge(registration: &Value) -> HostHandshake {
+    let stream =
+        maka_client::local::open_stream(Path::new(registration["endpoint"].as_str().unwrap()))
+            .await
+            .unwrap();
+    let (mut reader, mut writer) =
+        maka_transport::ndjson::split(stream, tokio_util::sync::CancellationToken::new());
+    writer
+        .write(&ClientHello {
+            client_instance_id: uuid::Uuid::new_v4().to_string(),
+            protocol_min: maka_protocol::PROTOCOL_VERSION,
+            protocol_max: maka_protocol::PROTOCOL_VERSION,
+            compatibility_epoch: maka_protocol::COMPATIBILITY_EPOCH,
+            composition_id: maka_protocol::COMPOSITION_ID.into(),
+            generation: Some("native-cli-successor".into()),
+            takeover: Some(Takeover {
+                expected_host_epoch: registration["hostEpoch"].as_str().unwrap().into(),
+            }),
+        })
+        .await
+        .unwrap();
+    maka_protocol::handshake::decode_host_handshake(&reader.read().await.unwrap().unwrap()).unwrap()
+}
+
 /// Reap the exact test child before removing its fresh root's account-level lease files.
 pub(super) struct CandidateFixture {
     pub child: Option<Child>,


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org


(maka) 03/05: merge: integrate native TUI with Session bundle notifications

Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git

commit d01501b44f1dc4e46475ff171c20e07e46341679
Merge: 1d34d3da0a f9d8cb5f6d
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Thu Sep 24 04:55:34 2026 +0800

    merge: integrate native TUI with Session bundle notifications
    
    Preserve the remote Host and Desktop bundle implementation alongside command palette search and native candidate lifecycle tests.
    
    Generated-by: Codex

 Cargo.lock                                         |   1 +
 .../__tests__/import-tasks-settings-page.test.ts   |   1 +
 .../runtime-host-session-bundle-ipc-main.test.ts   |  49 +++--
 .../__tests__/session-bundle-export-tree.test.ts   |   9 +-
 apps/desktop/src/main/runtime-host-client.ts       |  13 +-
 .../src/main/runtime-host-desktop-candidate.ts     |  14 +-
 .../main/runtime-host-session-bundle-ipc-main.ts   |  45 +++--
 apps/desktop/src/preload/bridge-contract.d.ts      |   8 +-
 apps/desktop/src/preload/preload.ts                |  19 +-
 .../features/session-bundle/export-tree.tsx        |  47 +----
 .../src/renderer/features/session-bundle/ports.ts  |  17 +-
 .../session-bundle/session-bundle-tasks.tsx        |  32 +--
 .../locales/external-session-import-copy.ts        |   4 -
 .../desktop/create-session-bundle-services.ts      |   1 +
 apps/desktop/src/renderer/reference-shell.css      |   5 -
 .../stories/settings/settings-pages.stories.tsx    |  43 ++++-
 crates/client/src/notification.rs                  |   6 +-
 crates/event-log/src/bundle/export.rs              |   9 +-
 crates/event-log/src/bundle/stage.rs               |  14 +-
 crates/protocol/src/authority.rs                   |   1 +
 crates/protocol/src/lib.rs                         |   2 +-
 crates/protocol/src/operation.rs                   |   1 +
 crates/protocol/src/session.rs                     |   1 +
 crates/protocol/src/session/bundle.rs              | 151 +++++++++++++++
 crates/protocol/src/session/operations.rs          |  23 ++-
 crates/runtime-host/Cargo.toml                     |   1 +
 crates/runtime-host/src/server/catalog_feed.rs     |  21 +-
 crates/runtime-host/src/server/sessions.rs         |  22 +++
 crates/runtime-host/src/server/sessions/bundle.rs  | 186 ++++++++++++++++++
 .../src/server/sessions/bundle/binding.rs          | 115 +++++++++++
 .../src/server/sessions/bundle/file.rs             | 172 +++++++++++++++++
 crates/runtime-host/tests/integration/main.rs      |   1 +
 .../tests/integration/session_bundle.rs            | 214 +++++++++++++++++++++
 .../tests/integration/session_bundle/fixtures.rs   | 157 +++++++++++++++
 .../tests/integration/session_bundle/host.rs       |  80 ++++++++
 crates/tui/src/lib.rs                              |   9 +-
 docs/rust-parity.md                                |   2 +-
 docs/rust-parity.zh-CN.md                          |   2 +-
 .../src/__tests__/authenticated-websocket.test.ts  |   8 +-
 .../runtime-host/src/__tests__/host-kernel.test.ts |   4 +-
 .../session-catalog-two-client-uds.test.ts         |   4 +-
 packages/runtime-host/src/protocol/index.ts        |   4 +-
 packages/runtime-host/src/protocol/operations.ts   |   1 +
 .../runtime-host/src/protocol/session-bundle.ts    |  66 +++++--
 .../src/protocol/session-catalog-change.ts         |  20 +-
 .../src/server/session-bundle-coordinator.ts       |   4 +
 tests/fixtures/client-message-submit.mjs           |  24 +--
 47 files changed, 1416 insertions(+), 217 deletions(-)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org