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/10/22 22:06:44 UTC

(maka) 16/29: fix(terminal-shell): address rendered history by original source positions

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

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

commit 614d523831d3a721c6d78e692e642de878528bb7
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Fri Oct 23 04:38:14 2026 +0800

    fix(terminal-shell): address rendered history by original source positions
---
 products/maka/terminal-shell/app/MarkdownBlock.tsx |  42 ++-
 products/maka/terminal-shell/app/MarkdownBody.tsx  |   5 +-
 products/maka/terminal-shell/app/MessageRow.tsx    |  18 +-
 products/maka/terminal-shell/app/SourceText.tsx    |  37 ++
 products/maka/terminal-shell/src/markdown.rs       | 151 +++++++-
 .../maka/terminal-shell/src/markdown/source.rs     | 418 +++++++++++++++++++++
 products/maka/terminal-shell/tests/real_host.rs    |   5 +
 7 files changed, 648 insertions(+), 28 deletions(-)

diff --git a/products/maka/terminal-shell/app/MarkdownBlock.tsx b/products/maka/terminal-shell/app/MarkdownBlock.tsx
index 17e2f3c50e..0d56f76c6b 100644
--- a/products/maka/terminal-shell/app/MarkdownBlock.tsx
+++ b/products/maka/terminal-shell/app/MarkdownBlock.tsx
@@ -36,6 +36,7 @@ import { Icon } from './Icon';
 
 export function MarkdownBlockView(props: {
   block: () => MarkdownBlock;
+  sourceId: string;
   compact?: boolean;
   openLink(target: string, restoreFocus: () => boolean): Promise<void>;
   userScroll(): void;
@@ -51,6 +52,7 @@ export function MarkdownBlockView(props: {
       else={
         <MarkdownBlockContent
           block={block}
+          sourceId={props.sourceId}
           compact={props.compact}
           openLink={props.openLink}
           userScroll={props.userScroll}
@@ -76,6 +78,7 @@ export function MarkdownBlockView(props: {
         <View style={{ flex: 1, minWidth: 0 }}>
           <MarkdownBlockContent
             block={block}
+            sourceId={props.sourceId}
             compact={props.compact}
             openLink={props.openLink}
             userScroll={props.userScroll}
@@ -86,11 +89,12 @@ export function MarkdownBlockView(props: {
   );
 }
 
-function StyledRuns(props: { runs(): MarkdownRun[]; compact?: boolean }) {
+function StyledRuns(props: { runs(): MarkdownRun[]; sourceId: string; compact?: boolean }) {
   return (
     <Each of={props.runs()} key={(run) => run.key}>
       {(run) => (
         <Text
+          textSource={{ id: props.sourceId, segments: run().source }}
           style={{
             fontWeight: run().strong ? '700' : undefined,
             fontStyle: run().emphasis ? 'italic' : undefined,
@@ -109,6 +113,7 @@ function StyledRuns(props: { runs(): MarkdownRun[]; compact?: boolean }) {
 
 function InlineRuns(props: {
   runs(): MarkdownRun[];
+  sourceId: string;
   compact?: boolean;
   openLink(target: string, restoreFocus: () => boolean): Promise<void>;
 }) {
@@ -150,7 +155,13 @@ function InlineRuns(props: {
         return (
           <If
             when={group().link}
-            else={<StyledRuns runs={() => group().runs} compact={props.compact} />}
+            else={
+              <StyledRuns
+                runs={() => group().runs}
+                sourceId={props.sourceId}
+                compact={props.compact}
+              />
+            }
           >
             <Text
               ref={(value) => {
@@ -164,7 +175,11 @@ function InlineRuns(props: {
               onPress={inputAction(activate, { key: group().key, target: group().link!.target })}
               style={{ color: C.accentHover, textDecorationLine: 'underline' }}
             >
-              <StyledRuns runs={() => group().runs} compact={props.compact} />
+              <StyledRuns
+                runs={() => group().runs}
+                sourceId={props.sourceId}
+                compact={props.compact}
+              />
             </Text>
           </If>
         );
@@ -175,6 +190,7 @@ function InlineRuns(props: {
 
 function MarkdownBlockContent(props: {
   block: () => MarkdownBlock;
+  sourceId: string;
   compact?: boolean;
   openLink(target: string, restoreFocus: () => boolean): Promise<void>;
   userScroll(): void;
@@ -195,6 +211,7 @@ function MarkdownBlockContent(props: {
                   else={
                     <Text
                       selectable
+                      textSource={{ id: props.sourceId, segments: block().source }}
                       style={{
                         ...(props.compact ? secondaryTextStyle : {}),
                         color: props.compact ? C.muted : C.text,
@@ -230,6 +247,7 @@ function MarkdownBlockContent(props: {
               >
                 <InlineRuns
                   runs={() => block().runs}
+                  sourceId={props.sourceId}
                   compact={props.compact}
                   openLink={props.openLink}
                 />
@@ -237,12 +255,18 @@ function MarkdownBlockContent(props: {
             </If>
           }
         >
-          <CodeBlock block={block} compact={props.compact} userScroll={props.userScroll} />
+          <CodeBlock
+            block={block}
+            sourceId={props.sourceId}
+            compact={props.compact}
+            userScroll={props.userScroll}
+          />
         </If>
       }
     >
       <TableBlock
         table={() => block().table!}
+        sourceId={props.sourceId}
         compact={props.compact}
         openLink={props.openLink}
         userScroll={props.userScroll}
@@ -253,6 +277,7 @@ function MarkdownBlockContent(props: {
 
 function TableBlock(props: {
   table(): MarkdownTable;
+  sourceId: string;
   compact?: boolean;
   openLink(target: string, restoreFocus: () => boolean): Promise<void>;
   userScroll(): void;
@@ -313,6 +338,7 @@ function TableBlock(props: {
               >
                 <InlineRuns
                   runs={() => cell().runs}
+                  sourceId={props.sourceId}
                   compact={props.compact}
                   openLink={props.openLink}
                 />
@@ -330,7 +356,12 @@ type CopyState =
   | { kind: 'copying' | 'copied'; text: string }
   | { kind: 'failed'; text: string; message: string };
 
-function CodeBlock(props: { block: () => MarkdownBlock; compact?: boolean; userScroll(): void }) {
+function CodeBlock(props: {
+  block: () => MarkdownBlock;
+  sourceId: string;
+  compact?: boolean;
+  userScroll(): void;
+}) {
   const block = props.block;
   const key = computed(() => block().key);
   const text = computed(() => block().text);
@@ -417,6 +448,7 @@ function CodeBlock(props: { block: () => MarkdownBlock; compact?: boolean; userS
       <Scroll horizontal={!wrapped()} scrollable={!wrapped()} onScrollIntent={props.userScroll}>
         <Text
           selectable
+          textSource={{ id: props.sourceId, segments: block().source }}
           style={{
             ...codeTextStyle,
             width: wrapped() ? '100%' : undefined,
diff --git a/products/maka/terminal-shell/app/MarkdownBody.tsx b/products/maka/terminal-shell/app/MarkdownBody.tsx
index 2e39ecf893..8f87b38cb1 100644
--- a/products/maka/terminal-shell/app/MarkdownBody.tsx
+++ b/products/maka/terminal-shell/app/MarkdownBody.tsx
@@ -19,6 +19,7 @@
 
 import { errorText, renderText, type UiText } from './text';
 import { tr } from './locale';
+import { SourceText } from './SourceText';
 import { cleanup, computed, effect, signal, Text, View } from 'nsh';
 import { markdown } from 'maka/markdown';
 import type { MarkdownBlock } from 'maka/markdown';
@@ -38,6 +39,7 @@ const sameInput = (left: Input | null, right: Input | null) =>
 
 export function MarkdownBody(props: {
   row: () => Row;
+  sourceId: string;
   compact?: boolean;
   openLink(target: string, restoreFocus: () => boolean): Promise<void>;
   userScroll(): void;
@@ -120,7 +122,7 @@ export function MarkdownBody(props: {
               color: props.compact ? C.muted : C.text,
             }}
           >
-            {props.row().text}
+            <SourceText row={props.row} sourceId={props.sourceId} />
           </Text>
           <If when={failure()}>
             <Text style={{ ...(props.compact ? secondaryTextStyle : {}), color: C.danger }}>
@@ -135,6 +137,7 @@ export function MarkdownBody(props: {
           {(block) => (
             <MarkdownBlockView
               block={block}
+              sourceId={props.sourceId}
               compact={props.compact}
               openLink={props.openLink}
               userScroll={props.userScroll}
diff --git a/products/maka/terminal-shell/app/MessageRow.tsx b/products/maka/terminal-shell/app/MessageRow.tsx
index 87bd87ef61..8a6e2adb28 100644
--- a/products/maka/terminal-shell/app/MessageRow.tsx
+++ b/products/maka/terminal-shell/app/MessageRow.tsx
@@ -24,6 +24,7 @@ import { Button } from 'nsh/base';
 import { platform } from 'nsh/device';
 import { messageKey, type Row } from './history';
 import { MarkdownBody } from './MarkdownBody';
+import { SourceText } from './SourceText';
 import { C, bodyTextStyle, secondaryTextStyle, controlStyle } from './ui';
 import { Icon } from './Icon';
 import { HistoryAttachments } from './Attachments';
@@ -85,6 +86,7 @@ export function MessageRow({
           expanded.set(action.value);
           if (action.value) onExpand?.(action.key);
         };
+        const sourceId = () => `${sessionId}/${row().messageId}/${row().field}`;
         const text = computed(() => row().text);
         const preview = computed(() => /\S[^\r\n]*/.exec(text())?.[0].trim() ?? '');
         const thinking = () => row().field === 'thinking' || row().role === 'thinking';
@@ -117,16 +119,21 @@ export function MessageRow({
                             when={row().role === 'assistant' && row().field === 'body'}
                             else={
                               <Text selectable style={{ color: C.text, ...bodyTextStyle }}>
-                                {text()}
+                                <SourceText row={row} sourceId={sourceId()} />
                               </Text>
                             }
                           >
-                            <MarkdownBody row={row} openLink={openLink} userScroll={userScroll} />
+                            <MarkdownBody
+                              row={row}
+                              sourceId={sourceId()}
+                              openLink={openLink}
+                              userScroll={userScroll}
+                            />
                           </If>
                         }
                       >
                         <Text selectable style={{ color: C.text, ...bodyTextStyle }}>
-                          {text()}
+                          <SourceText row={row} sourceId={sourceId()} />
                         </Text>
                       </If>
                       <View
@@ -267,6 +274,7 @@ export function MessageRow({
                               else={
                                 <MarkdownBody
                                   row={row}
+                                  sourceId={sourceId()}
                                   openLink={openLink}
                                   userScroll={userScroll}
                                   compact
@@ -274,7 +282,7 @@ export function MessageRow({
                               }
                             >
                               <Text selectable style={{ ...secondaryTextStyle, color: C.muted }}>
-                                {text()}
+                                <SourceText row={row} sourceId={sourceId()} />
                               </Text>
                             </If>
                           </View>
@@ -313,7 +321,7 @@ export function MessageRow({
                 }}
               >
                 <Text selectable style={{ color: C.text, ...bodyTextStyle }}>
-                  {text()}
+                  <SourceText row={row} sourceId={sourceId()} />
                 </Text>
                 <If when={(row().attachments?.length ?? 0) > 0}>
                   <HistoryAttachments
diff --git a/products/maka/terminal-shell/app/SourceText.tsx b/products/maka/terminal-shell/app/SourceText.tsx
new file mode 100644
index 0000000000..403d041951
--- /dev/null
+++ b/products/maka/terminal-shell/app/SourceText.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+import { Text } from 'nsh';
+import type { Row } from './history';
+
+/** Canonical history and a live opening address the same original text. */
+export function SourceText(props: { row(): Row; sourceId: string }) {
+  return (
+    <Each
+      of={props.row().fragments ?? [{ offset: 0, text: props.row().text }]}
+      key={(fragment) => fragment.offset}
+    >
+      {(fragment) => (
+        <Text textSource={{ id: props.sourceId, offset: fragment().offset }}>
+          {fragment().text}
+        </Text>
+      )}
+    </Each>
+  );
+}
diff --git a/products/maka/terminal-shell/src/markdown.rs b/products/maka/terminal-shell/src/markdown.rs
index 572e2acc7b..8fd322684d 100644
--- a/products/maka/terminal-shell/src/markdown.rs
+++ b/products/maka/terminal-shell/src/markdown.rs
@@ -23,10 +23,30 @@ use ::markdown::{ParseOptions, mdast::AlignKind, mdast::Node};
 use nsh_core::ui::prelude::*;
 use std::collections::BTreeMap;
 
+mod source;
+
+/// A literal UTF-8 interval or one indivisible Markdown transformation.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PropEnum)]
+pub enum MarkdownSourceKind {
+    Literal,
+    Replacement,
+}
+
+/// Local rendered UTF-16 ranges address the original document in UTF-8 bytes.
+#[derive(Clone, Debug, PartialEq, JsObject)]
+pub struct MarkdownSourceSegment {
+    pub start: u32,
+    pub end: u32,
+    pub source_start: u64,
+    pub source_end: u64,
+    pub kind: MarkdownSourceKind,
+}
+
 /// Inline content; unsupported constructs retain their complete source spelling.
 #[derive(Clone, Debug, PartialEq, JsObject)]
 pub struct MarkdownRun {
     pub key: String,
+    pub source: Vec<MarkdownSourceSegment>,
     pub text: String,
     pub strong: bool,
     pub emphasis: bool,
@@ -83,6 +103,7 @@ pub struct MarkdownTable {
 #[derive(Clone, Debug, PartialEq, JsObject)]
 pub struct MarkdownBlock {
     pub key: String,
+    pub source: Vec<MarkdownSourceSegment>,
     pub kind: MarkdownBlockKind,
     pub indent: u32,
     pub quote: u32,
@@ -125,7 +146,7 @@ fn parse_source(source: &str) -> Result<Vec<MarkdownBlock>, String> {
         references: BTreeMap::new(),
     };
     output.collect_references(&root);
-    output.visit(&root, Context::default());
+    output.visit(&root, Context::default())?;
     Ok(output.blocks)
 }
 
@@ -163,11 +184,11 @@ impl Presentation<'_> {
         }
     }
 
-    fn visit(&mut self, node: &Node, context: Context) {
+    fn visit(&mut self, node: &Node, context: Context) -> Result<(), String> {
         match node {
             Node::Root(root) => {
                 for child in &root.children {
-                    self.visit(child, context);
+                    self.visit(child, context)?;
                 }
             }
             Node::Blockquote(quote) => {
@@ -178,7 +199,7 @@ impl Presentation<'_> {
                             quote: context.quote + 1,
                             ..context
                         },
-                    );
+                    )?;
                 }
             }
             Node::List(list) => {
@@ -214,7 +235,7 @@ impl Presentation<'_> {
                                 indent: context.indent + 1,
                                 ..context
                             },
-                        );
+                        )?;
                     }
                     if let Some(block) = self.blocks.get_mut(first) {
                         block.marker = marker;
@@ -228,7 +249,7 @@ impl Presentation<'_> {
                     MarkdownBlockKind::Paragraph,
                     0,
                     context,
-                );
+                )?;
             }
             Node::Heading(heading) => {
                 self.phrasing(
@@ -237,11 +258,12 @@ impl Presentation<'_> {
                     MarkdownBlockKind::Heading,
                     u32::from(heading.depth),
                     context,
-                );
+                )?;
             }
             Node::Code(code) => {
                 let mut block = self.block(node, MarkdownBlockKind::Code, context);
                 block.text.clone_from(&code.value);
+                block.source = source::code(self.source, node, &code.value)?;
                 block.language = code.lang.clone().unwrap_or_default();
                 self.blocks.push(block);
             }
@@ -273,8 +295,9 @@ impl Presentation<'_> {
                                 child,
                                 Marks::default(),
                                 None,
+                                context.quote,
                                 &mut runs,
-                            );
+                            )?;
                         }
                         cells.push(MarkdownTableCell {
                             key: format!("table-cell:{}", start(cell_node)),
@@ -293,12 +316,14 @@ impl Presentation<'_> {
                 }
                 let mut block = self.block(node, MarkdownBlockKind::Table, context);
                 block.text = raw(self.source, node).into();
+                block.source = source::literal(start(node), &block.text);
                 block.table = Some(MarkdownTable { columns, rows });
                 self.blocks.push(block);
             }
             Node::ThematicBreak(_) => {
                 let mut block = self.block(node, MarkdownBlockKind::Rule, context);
                 block.text = raw(self.source, node).into();
+                block.source = source::literal(start(node), &block.text);
                 self.blocks.push(block);
             }
             // Link definitions are metadata, not rendered document content.
@@ -307,14 +332,17 @@ impl Presentation<'_> {
             _ => {
                 let mut block = self.block(node, MarkdownBlockKind::Literal, context);
                 block.text = raw(self.source, node).into();
+                block.source = source::literal(start(node), &block.text);
                 self.blocks.push(block);
             }
         }
+        Ok(())
     }
 
     fn block(&self, node: &Node, kind: MarkdownBlockKind, context: Context) -> MarkdownBlock {
         MarkdownBlock {
             key: format!("{}:{}", kind.js_name(), start(node)),
+            source: Vec::new(),
             kind,
             indent: context.indent,
             quote: context.quote,
@@ -334,7 +362,7 @@ impl Presentation<'_> {
         kind: MarkdownBlockKind,
         level: u32,
         context: Context,
-    ) {
+    ) -> Result<(), String> {
         let mut block = self.block(node, kind, context);
         for child in children {
             inline(
@@ -343,11 +371,13 @@ impl Presentation<'_> {
                 child,
                 Marks::default(),
                 None,
+                context.quote,
                 &mut block.runs,
-            );
+            )?;
         }
         block.level = level;
         self.blocks.push(block);
+        Ok(())
     }
 }
 
@@ -365,8 +395,9 @@ fn inline(
     node: &Node,
     marks: Marks,
     link: Option<&MarkdownLink>,
+    quote: u32,
     runs: &mut Vec<MarkdownRun>,
-) {
+) -> Result<(), String> {
     let linked = match node {
         Node::Link(value) => Some((&value.children, &value.url)),
         Node::LinkReference(value) => references
@@ -380,9 +411,9 @@ fn inline(
             target: target.clone(),
         };
         for child in children {
-            inline(source, references, child, marks, Some(&target), runs);
+            inline(source, references, child, marks, Some(&target), quote, runs)?;
         }
-        return;
+        return Ok(());
     }
     let children = match node {
         Node::Strong(strong) => Some((
@@ -410,9 +441,9 @@ fn inline(
     };
     if let Some((children, marks)) = children {
         for child in children {
-            inline(source, references, child, marks, link, runs);
+            inline(source, references, child, marks, link, quote, runs)?;
         }
-        return;
+        return Ok(());
     }
     let (text, marks) = match node {
         Node::Text(text) => (&*text.value, marks),
@@ -427,8 +458,14 @@ fn inline(
         _ => (raw(source, node), marks),
     };
     if text.is_empty() {
-        return;
+        return Ok(());
     }
+    let mut mapped = match node {
+        Node::Text(_) => source::text(source, node, text, quote)?,
+        Node::InlineCode(_) => source::inline_code(source, node, text, quote)?,
+        Node::Break(_) => source::line_break(source, node),
+        _ => source::literal(start(node), text),
+    };
     if let Some(previous) = runs.last_mut()
         && previous.strong == marks.strong
         && previous.emphasis == marks.emphasis
@@ -436,11 +473,18 @@ fn inline(
         && previous.code == marks.code
         && previous.link.as_ref() == link
     {
+        let shift = previous.source.last().map_or(0, |segment| segment.end);
+        for segment in &mut mapped {
+            segment.start += shift;
+            segment.end += shift;
+        }
+        previous.source.extend(mapped);
         previous.text.push_str(text);
-        return;
+        return Ok(());
     }
     runs.push(MarkdownRun {
         key: format!("inline:{}", start(node)),
+        source: mapped,
         text: text.into(),
         strong: marks.strong,
         emphasis: marks.emphasis,
@@ -448,6 +492,7 @@ fn inline(
         code: marks.code,
         link: link.cloned(),
     });
+    Ok(())
 }
 
 fn start(node: &Node) -> usize {
@@ -513,10 +558,82 @@ mod tests {
         assert_eq!(first[0].key, next[0].key);
         assert_eq!(first[0].runs[0].key, next[0].runs[0].key);
         assert_eq!(first[0].runs[1].text, "粗体");
+        assert_eq!(
+            first[0].runs[0].source,
+            [MarkdownSourceSegment {
+                start: 0,
+                end: 3,
+                source_start: 0,
+                source_end: 7,
+                kind: MarkdownSourceKind::Literal,
+            }]
+        );
+        assert_eq!(
+            first[0].runs[1].source,
+            [MarkdownSourceSegment {
+                start: 0,
+                end: 2,
+                source_start: 9,
+                source_end: 15,
+                kind: MarkdownSourceKind::Literal,
+            }]
+        );
+        assert_eq!(
+            first[0].runs[2].source,
+            [MarkdownSourceSegment {
+                start: 0,
+                end: 11,
+                source_start: 17,
+                source_end: 37,
+                kind: MarkdownSourceKind::Literal,
+            }]
+        );
         assert_eq!(first[0].runs[2].text, " 👩🏽‍💻 e\u{301}");
         assert_eq!(next[0].runs[1].key, first[0].runs[1].key);
     }
 
+    #[test]
+    fn source_addresses_keep_entities_escapes_and_inline_code_atomic() {
+        let blocks = parse_source("A &amp; \\* &#x1F642; ` a🙂 `").unwrap();
+        assert_eq!(blocks[0].runs[0].text, "A & * 🙂 ");
+        let ranges: Vec<_> = blocks[0].runs[0]
+            .source
+            .iter()
+            .map(|segment| {
+                (
+                    segment.start,
+                    segment.end,
+                    segment.source_start,
+                    segment.source_end,
+                    segment.kind,
+                )
+            })
+            .collect();
+        assert_eq!(
+            ranges,
+            [
+                (0, 2, 0, 2, MarkdownSourceKind::Literal),
+                (2, 3, 2, 7, MarkdownSourceKind::Replacement),
+                (3, 4, 7, 8, MarkdownSourceKind::Literal),
+                (4, 5, 8, 10, MarkdownSourceKind::Replacement),
+                (5, 6, 10, 11, MarkdownSourceKind::Literal),
+                (6, 8, 11, 20, MarkdownSourceKind::Replacement),
+                (8, 9, 20, 21, MarkdownSourceKind::Literal),
+            ]
+        );
+        assert_eq!(blocks[0].runs[1].text, "a🙂");
+        assert_eq!(
+            blocks[0].runs[1].source,
+            [MarkdownSourceSegment {
+                start: 0,
+                end: 3,
+                source_start: 23,
+                source_end: 28,
+                kind: MarkdownSourceKind::Literal,
+            }]
+        );
+    }
+
     #[test]
     fn tables_keep_columns_rows_empty_cells_and_nested_inline_marks() {
         let source = "| 左 **列** | *中* | 右 | 默认 |\n| :--- | :---: | ---: | --- |\n| **粗 *斜*** | ~~删~~ | `码🙂` | |\n| 👩🏽‍💻 e\u{301} | | [原](https://example.test) ![图](image.png) | 尾 |";
diff --git a/products/maka/terminal-shell/src/markdown/source.rs b/products/maka/terminal-shell/src/markdown/source.rs
new file mode 100644
index 0000000000..e5ac5954f4
--- /dev/null
+++ b/products/maka/terminal-shell/src/markdown/source.rs
@@ -0,0 +1,418 @@
+/*
+ * 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.
+ */
+
+//! Source addresses for the presentation produced by the existing Markdown AST.
+//! Literal ranges retain exact UTF-8 addresses; lexical replacements are atomic.
+
+use super::{MarkdownSourceKind, MarkdownSourceSegment, raw, start};
+use ::markdown::mdast::Node;
+
+pub(super) fn literal(offset: usize, text: &str) -> Vec<MarkdownSourceSegment> {
+    if text.is_empty() {
+        return Vec::new();
+    }
+    vec![MarkdownSourceSegment {
+        start: 0,
+        end: text.encode_utf16().count() as u32,
+        source_start: offset as u64,
+        source_end: (offset + text.len()) as u64,
+        kind: MarkdownSourceKind::Literal,
+    }]
+}
+
+struct Projection<'a> {
+    expected: &'a str,
+    bytes: usize,
+    units: u32,
+    segments: Vec<MarkdownSourceSegment>,
+}
+
+impl<'a> Projection<'a> {
+    fn new(expected: &'a str) -> Self {
+        Self {
+            expected,
+            bytes: 0,
+            units: 0,
+            segments: Vec::new(),
+        }
+    }
+
+    fn remaining(&self) -> &'a str {
+        &self.expected[self.bytes..]
+    }
+
+    fn push(
+        &mut self,
+        text: &str,
+        source_start: usize,
+        source_end: usize,
+        kind: MarkdownSourceKind,
+    ) -> Result<(), String> {
+        if !self.remaining().starts_with(text) {
+            return Err(format!(
+                "Markdown source projection disagrees with parsed text at byte {source_start}"
+            ));
+        }
+        let end = self.units + text.encode_utf16().count() as u32;
+        if !text.is_empty() {
+            if kind == MarkdownSourceKind::Literal
+                && let Some(previous) = self.segments.last_mut()
+                && previous.kind == kind
+                && previous.source_end == source_start as u64
+            {
+                previous.end = end;
+                previous.source_end = source_end as u64;
+            } else {
+                self.segments.push(MarkdownSourceSegment {
+                    start: self.units,
+                    end,
+                    source_start: source_start as u64,
+                    source_end: source_end as u64,
+                    kind,
+                });
+            }
+        }
+        self.bytes += text.len();
+        self.units = end;
+        Ok(())
+    }
+
+    fn finish(self) -> Result<Vec<MarkdownSourceSegment>, String> {
+        if self.bytes != self.expected.len() {
+            return Err("Markdown source projection did not cover the parsed text".into());
+        }
+        Ok(self.segments)
+    }
+}
+
+/// A Text leaf can contain escaped punctuation, entities and continuation
+/// container prefixes. Replay those lexical transformations, never search the
+/// document for a rendered substring (repeated text has a different address).
+pub(super) fn text(
+    source: &str,
+    node: &Node,
+    expected: &str,
+    quote: u32,
+) -> Result<Vec<MarkdownSourceSegment>, String> {
+    let input = raw(source, node);
+    if input == expected {
+        return Ok(literal(start(node), input));
+    }
+    let mut result = Projection::new(expected);
+    let mut offset = 0;
+    let mut line_start = false;
+    while offset < input.len() && !result.remaining().is_empty() {
+        let rest = &input[offset..];
+        let base = start(node) + offset;
+        let ch = rest.chars().next().unwrap();
+        if line_start {
+            let consumed = continuation(&mut result, rest, base, quote)?;
+            line_start = false;
+            if consumed != 0 {
+                offset += consumed;
+                continue;
+            }
+        }
+        if ch == '\\'
+            && let Some(escaped) = rest.chars().nth(1)
+            && escaped.is_ascii_punctuation()
+        {
+            let consumed = 1 + escaped.len_utf8();
+            result.push(
+                &rest[1..consumed],
+                base,
+                base + consumed,
+                MarkdownSourceKind::Replacement,
+            )?;
+            offset += consumed;
+            continue;
+        }
+        if ch == '&'
+            && let Some((consumed, decoded)) = reference(rest)
+            && result.remaining().starts_with(&decoded)
+        {
+            result.push(
+                &decoded,
+                base,
+                base + consumed,
+                MarkdownSourceKind::Replacement,
+            )?;
+            offset += consumed;
+            continue;
+        }
+        if ch == '\0' && result.remaining().starts_with('�') {
+            result.push("�", base, base + 1, MarkdownSourceKind::Replacement)?;
+            offset += 1;
+            continue;
+        }
+        if ch == '\r' && result.remaining().starts_with('\n') {
+            let consumed = if rest.starts_with("\r\n") { 2 } else { 1 };
+            result.push("\n", base, base + consumed, MarkdownSourceKind::Replacement)?;
+            offset += consumed;
+            line_start = true;
+            continue;
+        }
+        result.push(
+            &rest[..ch.len_utf8()],
+            base,
+            base + ch.len_utf8(),
+            MarkdownSourceKind::Literal,
+        )?;
+        offset += ch.len_utf8();
+        if matches!(ch, '\n' | '\r') {
+            line_start = true;
+        }
+    }
+    result.finish()
+}
+
+fn reference(input: &str) -> Option<(usize, String)> {
+    let end = 1 + input.as_bytes()[1..]
+        .iter()
+        .position(|byte| !byte.is_ascii_alphanumeric() && *byte != b'#')?;
+    if input.as_bytes()[end] != b';' {
+        return None;
+    }
+    let name = &input[1..end];
+    let decoded = if let Some(number) = name.strip_prefix("#x").or_else(|| name.strip_prefix("#X"))
+    {
+        (!number.is_empty() && number.bytes().all(|byte| byte.is_ascii_hexdigit()))
+            .then(|| ::markdown::decode_numeric(number, 16))?
+    } else if let Some(number) = name.strip_prefix('#') {
+        (!number.is_empty() && number.bytes().all(|byte| byte.is_ascii_digit()))
+            .then(|| ::markdown::decode_numeric(number, 10))?
+    } else {
+        ::markdown::decode_named(name, true)?
+    };
+    Some((end + 1, decoded))
+}
+
+pub(super) fn inline_code(
+    source: &str,
+    node: &Node,
+    expected: &str,
+    quote: u32,
+) -> Result<Vec<MarkdownSourceSegment>, String> {
+    let input = raw(source, node);
+    let fence = input.bytes().take_while(|byte| *byte == b'`').count();
+    let mut first = fence;
+    let mut last = input.len() - fence;
+    let body = &input[first..last];
+    // CommonMark removes one surrounding space, except for all-space code.
+    if body.starts_with(' ') && body.ends_with(' ') && body.chars().any(|ch| ch != ' ') {
+        first += 1;
+        last -= 1;
+    }
+    if &input[first..last] == expected {
+        return Ok(literal(start(node) + first, expected));
+    }
+    let mut result = Projection::new(expected);
+    let mut offset = first;
+    let mut line_start = false;
+    while offset < last {
+        let rest = &input[offset..last];
+        let ch = rest.chars().next().unwrap();
+        let base = start(node) + offset;
+        if line_start {
+            let consumed = continuation(&mut result, rest, base, quote)?;
+            line_start = false;
+            if consumed != 0 {
+                offset += consumed;
+                continue;
+            }
+        }
+        let consumed = if rest.starts_with("\r\n") {
+            2
+        } else {
+            ch.len_utf8()
+        };
+        if matches!(ch, '\r' | '\n') && result.remaining().starts_with(' ') {
+            result.push(" ", base, base + consumed, MarkdownSourceKind::Replacement)?;
+        } else if rest.starts_with("\\|") && result.remaining().starts_with('|') {
+            result.push("|", base, base + 2, MarkdownSourceKind::Replacement)?;
+            offset += 2;
+            continue;
+        } else {
+            result.push(
+                &rest[..consumed],
+                base,
+                base + consumed,
+                MarkdownSourceKind::Literal,
+            )?;
+        }
+        offset += consumed;
+        line_start = matches!(ch, '\r' | '\n');
+    }
+    result.finish()
+}
+
+/// Container syntax and leading whitespace form one indentation token. Any
+/// whitespace retained by the parser belongs to that token, rather than to a
+/// guessed occurrence among several identical source spaces.
+fn continuation(
+    result: &mut Projection<'_>,
+    input: &str,
+    base: usize,
+    quote: u32,
+) -> Result<usize, String> {
+    let mut quotes = 0;
+    let mut consumed = 0;
+    for ch in input.chars() {
+        if matches!(ch, ' ' | '\t') || (ch == '>' && quotes < quote) {
+            if ch == '>' {
+                quotes += 1;
+            }
+            consumed += ch.len_utf8();
+        } else {
+            break;
+        }
+    }
+    if consumed != 0 {
+        let remaining = result.remaining();
+        let end = remaining.len() - remaining.trim_start_matches([' ', '\t']).len();
+        result.push(
+            &remaining[..end],
+            base,
+            base + consumed,
+            MarkdownSourceKind::Replacement,
+        )?;
+    }
+    Ok(consumed)
+}
+
+pub(super) fn line_break(source: &str, node: &Node) -> Vec<MarkdownSourceSegment> {
+    vec![MarkdownSourceSegment {
+        start: 0,
+        end: 1,
+        source_start: start(node) as u64,
+        source_end: (start(node) + raw(source, node).len()) as u64,
+        kind: MarkdownSourceKind::Replacement,
+    }]
+}
+
+/// The parser's code value consists of source lines with their container,
+/// indentation and fence removed. Match only that line's exact retained suffix;
+/// the discarded prefix must consist of container/indentation characters.
+pub(super) fn code(
+    source: &str,
+    node: &Node,
+    expected: &str,
+) -> Result<Vec<MarkdownSourceSegment>, String> {
+    let input = raw(source, node);
+    let mut source_offset = 0;
+    let first_line = &input[..line_end(input)];
+    let fence = first_line.trim_start_matches([' ', '\t']);
+    let fenced = fence.starts_with("```") || fence.starts_with("~~~");
+    if fenced {
+        source_offset += first_line.len();
+    }
+    let mut result = Projection::new(expected);
+    while !result.remaining().is_empty() {
+        let rest = &input[source_offset..];
+        let end = line_end(rest);
+        if end == 0 {
+            return Err("Markdown code source ended before its parsed value".into());
+        }
+        let source_line = &rest[..end];
+        let content = source_line.trim_end_matches(['\r', '\n']);
+        let rendered = result.remaining();
+        let rendered_end = rendered.find(['\r', '\n']).unwrap_or(rendered.len());
+        let rendered_line = &rendered[..rendered_end];
+        let normalized = content.replace('\0', "�");
+        let (prefix, leading) = if let Some(prefix) = normalized.strip_suffix(rendered_line) {
+            (prefix, "")
+        } else {
+            // A tab consumed partly by indentation can leave rendered spaces.
+            // Keep that indentation as one lexical replacement, never the code.
+            let tail = rendered_line.trim_start_matches([' ', '\t']);
+            let prefix = normalized
+                .strip_suffix(tail)
+                .ok_or("Markdown code line is not an exact source suffix")?;
+            (prefix, &rendered_line[..rendered_line.len() - tail.len()])
+        };
+        if !prefix.chars().all(|ch| matches!(ch, ' ' | '\t' | '>')) {
+            return Err("Markdown code discarded a non-container source prefix".into());
+        }
+        let base = start(node) + source_offset;
+        if !leading.is_empty() {
+            result.push(
+                leading,
+                base,
+                base + prefix.len(),
+                MarkdownSourceKind::Replacement,
+            )?;
+        }
+        let kept = &content[prefix.len()..];
+        if !kept.contains('\0') {
+            result.push(
+                kept,
+                base + prefix.len(),
+                base + content.len(),
+                MarkdownSourceKind::Literal,
+            )?;
+        } else {
+            for (at, ch) in kept.char_indices() {
+                let base = base + prefix.len() + at;
+                if ch == '\0' && result.remaining().starts_with('�') {
+                    result.push("�", base, base + 1, MarkdownSourceKind::Replacement)?;
+                } else {
+                    let text = &source[base..base + ch.len_utf8()];
+                    result.push(
+                        text,
+                        base,
+                        base + ch.len_utf8(),
+                        MarkdownSourceKind::Literal,
+                    )?;
+                }
+            }
+        }
+        if rendered_end < rendered.len() {
+            let ending = if rendered[rendered_end..].starts_with("\r\n") {
+                "\r\n"
+            } else {
+                &rendered[rendered_end..rendered_end + 1]
+            };
+            let raw_ending = &source_line[content.len()..];
+            let kind = if raw_ending == ending {
+                MarkdownSourceKind::Literal
+            } else {
+                MarkdownSourceKind::Replacement
+            };
+            let base = start(node) + source_offset + content.len();
+            result.push(
+                ending,
+                base,
+                start(node) + source_offset + source_line.len(),
+                kind,
+            )?;
+        }
+        source_offset += source_line.len();
+    }
+    result.finish()
+}
+
+fn line_end(source: &str) -> usize {
+    source.find(['\r', '\n']).map_or(source.len(), |at| {
+        at + if source[at..].starts_with("\r\n") {
+            2
+        } else {
+            1
+        }
+    })
+}
diff --git a/products/maka/terminal-shell/tests/real_host.rs b/products/maka/terminal-shell/tests/real_host.rs
index 9084c69fa3..1065197ac2 100644
--- a/products/maka/terminal-shell/tests/real_host.rs
+++ b/products/maka/terminal-shell/tests/real_host.rs
@@ -1896,6 +1896,11 @@ async fn real_agent_history_and_live_stream_render_and_the_app_sends_once() {
         support::evidence_cells("history-home-wheel-final-page-published", &cancelled);
         assert_eq!(retained.lines().position(|line| line.contains(&wheel_marker)), Some(wheel_row), "a completed, cancelled Home moved the user's wheel anchor:\n{retained}");
         assert!(!retained.contains("History question 00"), "the cancelled Home snapped to canonical start:\n{retained}");
+        let boundary_tail = format!("History {boundary_turn:02} paragraph 29:");
+        let formatted = wait_view_state(&mut cancelled, |view, _| view.accessibility_snapshot().iter().any(|node| node.label.starts_with(&boundary_tail) && !node.label.contains("**bold**"))).await;
+        assert_eq!(formatted.lines().position(|line| line.contains(&wheel_marker)), Some(wheel_row), "formatting the newly completed prefix moved the same original reading anchor:\n{formatted}");
+        assert!(!formatted.contains("History question 00"), "formatting restored an obsolete Home target:\n{formatted}");
+        evidence("history-home-wheel-after-formatting", &formatted);
         support::wait_draft(&cancel_client, &fixture.session, HOME_DRAFT).await;
         assert_eq!(client.query("agent", "observe", json!({"sessionId":fixture.session})).await.unwrap()["through"], observed["through"], "history navigation must not change the Host range");
         evidence("history-home-wheel-cancels-late-reply", &retained);


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