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 12:54:17 UTC

(maka) branch feat/runtime-host-rust updated: fix(tui): keep imported observations out of branch actions

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


The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
     new 1f0eab6261 fix(tui): keep imported observations out of branch actions
1f0eab6261 is described below

commit 1f0eab6261687af40a686ce25b80767171b6d9b6
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Wed Sep 23 20:53:51 2026 +0800

    fix(tui): keep imported observations out of branch actions
    
    Use presentation provenance to offer branch boundaries only on local durable messages. Verify imported history, tool errors and command availability through the native Host and a real PTY.
    
    Generated-by: Codex
---
 Cargo.lock                                     |   1 +
 crates/cli/Cargo.toml                          |   1 +
 crates/cli/tests/integration/tui.rs            |   1 +
 crates/cli/tests/integration/tui/imports.rs    | 165 +++++++++++++++++++++++++
 crates/tui/src/pages/branch.rs                 |   2 +-
 crates/tui/src/pages/chat/render.rs            |  32 ++++-
 crates/tui/src/pages/chat/render/navigation.rs |   8 +-
 7 files changed, 203 insertions(+), 7 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index f354db3fd2..e315baaf99 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4668,6 +4668,7 @@ dependencies = [
  "maka-event-log",
  "maka-fs-tools",
  "maka-js-runtime",
+ "maka-plugins",
  "maka-process",
  "maka-protocol",
  "maka-runtime",
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index ed0ebf3fe9..b4b0faac2a 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -63,6 +63,7 @@ uuid.workspace = true
 tempfile.workspace = true
 
 [dev-dependencies]
+maka-plugins.workspace = true
 unicode-width.workspace = true
 libc.workspace = true
 tempfile.workspace = true
diff --git a/crates/cli/tests/integration/tui.rs b/crates/cli/tests/integration/tui.rs
index fb5c604b93..ff2591077f 100644
--- a/crates/cli/tests/integration/tui.rs
+++ b/crates/cli/tests/integration/tui.rs
@@ -28,6 +28,7 @@ mod enabled_models;
 mod environment;
 mod forms;
 mod frames;
+mod imports;
 mod input;
 mod management;
 mod model_fetch;
diff --git a/crates/cli/tests/integration/tui/imports.rs b/crates/cli/tests/integration/tui/imports.rs
new file mode 100644
index 0000000000..0c61e88a6e
--- /dev/null
+++ b/crates/cli/tests/integration/tui/imports.rs
@@ -0,0 +1,165 @@
+/*
+ * 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 super::*;
+use maka_event_log::{
+    EventLog,
+    root::{RootNamespaces, RootOwner},
+    sessions::PluginSession,
+};
+use maka_plugins::{composition::Scope, storage::Namespace};
+use maka_protocol::session::{SandboxMode, WorkspaceProjection, WorkspaceTarget};
+use maka_runtime::import::{Content, Record, Source};
+use maka_runtime_host::session::{PreparedSession, SessionTarget};
+use serde_json::json;
+use std::sync::Arc;
+
+#[test]
+fn imported_observations_remain_readable_without_offering_executable_turn_actions() {
+    let directory = tempfile::tempdir().unwrap();
+    let mut host = super::super::candidate::CandidateFixture::new(directory.path().join("root"));
+    let runtime = tokio::runtime::Runtime::new().unwrap();
+    // Seed through native import admission before starting the Host writer.
+    // Plugin authorization is covered by the Host's plugin_commands tests.
+    runtime.block_on(async {
+        let owner =
+            RootOwner::open(&host.root, &RootNamespaces::for_current_account().unwrap()).unwrap();
+        let log = EventLog::for_root(Arc::new(owner)).await.unwrap();
+        let cwd =
+            maka_fs_tools::workspace::project::host_path(&directory.path().canonicalize().unwrap())
+                .unwrap()
+                .to_owned();
+        let configuration = PreparedSession::new(
+            serde_json::from_value(json!({
+                "sessionId":"imported", "name":"Imported observations",
+                "workspace":{"kind":"host_path","path":cwd}, "executorId":"fixture"
+            }))
+            .unwrap(),
+        )
+        .unwrap()
+        .bind(
+            WorkspaceProjection {
+                target: WorkspaceTarget::HostPath { path: cwd.clone() },
+                host_cwd: cwd,
+            },
+            SessionTarget::Executor {
+                executor_id: "fixture".to_owned().try_into().unwrap(),
+                settings: Default::default(),
+            },
+            SandboxMode::ReadOnly,
+        );
+        log.begin_session_import(
+            &PluginSession {
+                session_id: "imported".into(),
+                creator: Namespace::new("tui.reader", Scope::Profile).unwrap(),
+                fingerprint: "fixture-import".into(),
+                managed: false,
+                authority_session_id: None,
+            },
+            &Source {
+                adapter: "fixture".into(),
+                session_id: "foreign".into(),
+            },
+            &configuration,
+            1,
+        )
+        .await
+        .unwrap();
+        let records: Vec<_> = [
+            Content::User {
+                text: "Original imported question".into(),
+            },
+            Content::ToolCall {
+                call_id: "missing".into(),
+                name: "foreign-read".into(),
+                input: None,
+            },
+            Content::ToolCall {
+                call_id: "failed".into(),
+                name: "foreign-edit".into(),
+                input: None,
+            },
+            Content::ToolResult {
+                call_id: "failed".into(),
+                output: json!("Original failure details"),
+                is_error: true,
+            },
+            Content::Assistant {
+                text: "Original imported answer".into(),
+                thinking: Some("Original imported reasoning".into()),
+                model: Some("foreign-model".into()),
+            },
+            Content::Note {
+                text: "Source ended without a terminal record.".into(),
+            },
+        ]
+        .into_iter()
+        .enumerate()
+        .map(|(index, content)| Record {
+            source_message_id: format!("record-{index}"),
+            source_turn_id: "foreign-turn".into(),
+            timestamp: None,
+            content,
+        })
+        .collect();
+        let count = records.len() as u64;
+        log.append_session_import("imported", 0, records)
+            .await
+            .unwrap();
+        log.publish_session_import("imported", count).await.unwrap();
+        log.close().await.unwrap();
+    });
+    host.child = Some(
+        Command::new(env!("CARGO_BIN_EXE_maka"))
+            .args(["host", "serve", "--root"])
+            .arg(&host.root)
+            .stdin(Stdio::null())
+            .stdout(Stdio::null())
+            .stderr(Stdio::inherit())
+            .spawn()
+            .unwrap(),
+    );
+    host.wait_for_registration();
+    let mut tui = Pty::spawn(&["--root", host.root.to_str().unwrap()]);
+    tui.wait_for("Imported observations");
+    tui.click_text("Imported observations");
+    tui.wait_for("Original imported answer");
+    tui.wait_for("Original imported reasoning");
+    tui.wait_for("Source ended without a terminal record.");
+    tui.wait_for("foreign-read · No result recorded");
+    tui.wait_for("foreign-edit · Tool error · Original failure details");
+    for message in ["Original imported question", "Original imported answer"] {
+        tui.click_text(message);
+        tui.send(b"\x10");
+        tui.wait_for("Commands · Esc closes");
+        assert!(
+            !tui.screen
+                .snapshot()
+                .unwrap()
+                .screen
+                .contains("Branch from this turn"),
+            "imported observations are not local Turn boundaries"
+        );
+        tui.send(b"\x1b");
+        tui.wait_until(|screen| !screen.contains("Commands · Esc closes"));
+    }
+    tui.send(b"\x11");
+    tui.finish();
+    host.retire_registered();
+}
diff --git a/crates/tui/src/pages/branch.rs b/crates/tui/src/pages/branch.rs
index cf54c38742..2129b4e6fd 100644
--- a/crates/tui/src/pages/branch.rs
+++ b/crates/tui/src/pages/branch.rs
@@ -195,7 +195,7 @@ impl App {
         if self.chat.view.search.is_some() {
             return None;
         }
-        let (turn, text) = self.chat.view.selected_durable_message()?;
+        let (turn, text) = self.chat.view.selected_branch_point()?;
         let super::sessions::Detail::Ready(item) = &self.sessions.detail else {
             return None;
         };
diff --git a/crates/tui/src/pages/chat/render.rs b/crates/tui/src/pages/chat/render.rs
index d3462feacd..d2f0101086 100644
--- a/crates/tui/src/pages/chat/render.rs
+++ b/crates/tui/src/pages/chat/render.rs
@@ -132,6 +132,8 @@ impl Kind {
 }
 struct Block {
     time: Option<String>,
+    // Imported observations are durable too, but are not local Turn boundaries.
+    branchable: bool,
     revision: Revision,
     kind: Kind,
     text: String,
@@ -384,6 +386,7 @@ impl Transcript {
                     key.clone(),
                     Block {
                         time: None,
+                        branchable: false,
                         revision,
                         kind,
                         text,
@@ -476,6 +479,7 @@ impl Transcript {
                 continue;
             }
             let mut key = MessageKey::durable(row);
+            let branchable = matches!(kind, "user" | "assistant") && row["imported"] != true;
             if kind == "assistant"
                 && let Some(thinking) = row["thinking"]["text"].as_str()
             {
@@ -486,6 +490,7 @@ impl Transcript {
                         Kind::Thinking,
                         || thinking.to_owned().into(),
                     );
+                    self.blocks.get_mut(&key).unwrap().branchable = branchable;
                 }
                 if row["text"]
                     .as_str()
@@ -504,9 +509,13 @@ impl Transcript {
                 "turn_state" | "token_usage" => Kind::Meta,
                 _ => Kind::Other,
             };
-            self.upsert(key, Revision::Durable(*sequence), presentation, || {
-                project(row, i18n, ascii).into()
-            });
+            self.upsert(
+                key.clone(),
+                Revision::Durable(*sequence),
+                presentation,
+                || project(row, i18n, ascii).into(),
+            );
+            self.blocks.get_mut(&key).unwrap().branchable = branchable;
             if matches!(presentation, Kind::User | Kind::Assistant) {
                 let block = self.blocks.get_mut(self.order.last().unwrap()).unwrap();
                 // Durable rows are immutable; do not format all history on every delta.
@@ -1435,6 +1444,23 @@ mod tests {
         assert!(screen.contains("Imported answer"));
         assert!(screen.contains("Source ended without a terminal record."));
         assert!(!view.timing_visible());
+        for key in view.order.clone() {
+            view.select(key);
+            assert!(
+                view.selected_branch_point().is_none(),
+                "imported messages are not executable Turn boundaries"
+            );
+        }
+        let mut local = rows.clone();
+        local.insert(5, json!({"type":"user","turnId":"import-is-just-an-id","id":"local","text":"A local Turn after import"}));
+        view.sync(&local, &[], 0, &locale(), false);
+        draw(&mut view, 80, 24);
+        view.select(view.order.last().unwrap().clone());
+        assert_eq!(
+            view.selected_branch_point(),
+            Some(("import-is-just-an-id", "A local Turn after import")),
+            "use provenance, not an identifier prefix or the whole Session's origin"
+        );
     }
 
     #[test]
diff --git a/crates/tui/src/pages/chat/render/navigation.rs b/crates/tui/src/pages/chat/render/navigation.rs
index 1c34081aa1..30bdf682a7 100644
--- a/crates/tui/src/pages/chat/render/navigation.rs
+++ b/crates/tui/src/pages/chat/render/navigation.rs
@@ -22,11 +22,13 @@ use super::*;
 impl Transcript {
     /// A branch boundary must come from an explicitly selected durable message,
     /// not the implicit first-visible fallback or a live-only stream block.
-    pub fn selected_durable_message(&self) -> Option<(&str, &str)> {
+    pub fn selected_branch_point(&self) -> Option<(&str, &str)> {
         let key = self.selected.as_ref()?;
         let block = self.blocks.get(key)?;
-        (self.order.contains(key) && matches!(block.revision, Revision::Durable(_)))
-            .then_some((key.turn.as_str(), block.text.as_str()))
+        (self.order.contains(key)
+            && block.branchable
+            && matches!(block.revision, Revision::Durable(_)))
+        .then_some((key.turn.as_str(), block.text.as_str()))
     }
 
     pub fn selection(&self) -> Option<MessageKey> {


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