You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/08/28 19:07:02 UTC

(maka) branch main updated: fix(ui): state an aborted turn's outcome after its timeline (#4090)

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

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 2c0663165 fix(ui): state an aborted turn's outcome after its timeline (#4090)
2c0663165 is described below

commit 2c066316545e432459e327734c634dd2cb61fa40
Author: AstroHan <le...@outlook.com>
AuthorDate: Sat Aug 29 03:06:56 2026 +0800

    fix(ui): state an aborted turn's outcome after its timeline (#4090)
    
    * fix(ui): state an aborted turn's outcome after its timeline
    
    The aborted marker rendered at the top of the assistant answer, above
    the reasoning disclosures and tool calls, where it read as a header on
    work that had in fact all completed. PR #4073 moved the failed banner
    below the timeline for exactly this reason but left the aborted marker
    behind; both are terminal outcome markers and share the same slot, so
    move the aborted marker next to the failed banner and match its block
    rhythm.
    
    Generated-by: Maka
    
    * fix(ui): use Astryx badge for aborted turns
    
    Aborted turns are settled facts, but their outcome was rendered through a Maka-only Marker variant and bespoke visual recipe. Render the terminal outcome with Astryx's neutral Badge while preserving its position after the timeline, retain only the flex alignment needed by the transcript container, and cover both the component contract and ordering.
    
    Generated-by: Codex
    
    * fix(ui): render aborts as chat status notices
    
    An aborted turn is a short, settled status change with no recovery action. Astryx documents ChatSystemMessage as the Chat-family primitive for inline status notices, while Badge is reserved for states users need to notice or act on. Render the outcome as a system-message sibling after the assistant timeline, remove the marker-era parentheses and layout hook, and pin the component, copy, and ordering contracts.
    
    Generated-by: Codex
    
    * docs(ui): remove stale abort marker reference
    
    Generated-by: Codex
---
 apps/desktop/src/renderer/styles/chat-detail.css   |  8 ++--
 .../__tests__/chat-turn-answer-identity.test.tsx   | 28 +++++++++++
 packages/ui/src/chat-display-helpers.ts            |  2 +-
 packages/ui/src/chat-turn.tsx                      | 56 +++++++++++-----------
 packages/ui/src/conversation-copy.ts               |  4 +-
 packages/ui/src/primitives/chat.tsx                |  8 ++--
 packages/ui/src/styles.css                         | 12 -----
 7 files changed, 66 insertions(+), 52 deletions(-)

diff --git a/apps/desktop/src/renderer/styles/chat-detail.css b/apps/desktop/src/renderer/styles/chat-detail.css
index ec108d60b..4e59976ac 100644
--- a/apps/desktop/src/renderer/styles/chat-detail.css
+++ b/apps/desktop/src/renderer/styles/chat-detail.css
@@ -35,10 +35,10 @@
   background: oklch(from var(--accent) l c h / 0.045);
 }
 
-/* Turn aborted marker / lineage rows + badges / footer + footer actions
- * retired to the `@maka/ui` Marker chat primitive
- * (`aborted` / `lineage-row` / `lineage-row-reverse` / `lineage-badge` /
- * `footer` / `footer-action`), issue #332 PR2 — packages/ui/src/primitives/chat.tsx.
+/* Lineage rows + badges / footer + footer actions retired to the `@maka/ui`
+ * Marker chat primitive (`lineage-row` / `lineage-row-reverse` /
+ * `lineage-badge` / `footer` / `footer-action`), issue #332 PR2 —
+ * packages/ui/src/primitives/chat.tsx.
  * The failed-turn banner is an Astryx `Banner`, not a Marker variant.
  * The measure-column geometry the `tool-output.css` re-anchor used to add
  * to the summary / lineage rows / footer is folded into those Marker
diff --git a/packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx b/packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx
index ce6052f94..f5290c968 100644
--- a/packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx
+++ b/packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx
@@ -106,6 +106,34 @@ const RUNNING_TOOL: TurnTimelineItem = {
   items: [{ toolUseId: 'tool-1', toolName: 'read', status: 'running', args: {} }],
 };
 
+test('renders an aborted turn outcome as an inline system status notice', async () => {
+  const { container, root } = domRoot();
+  await renderTurn(root, {
+    ...turnWith([{ ...ANSWER, live: false }]),
+    status: 'aborted',
+    abortSource: 'renderer.stop_button',
+  });
+
+  const outcome = container.querySelector('.astryx-chat-system-message[role="status"]');
+  assert.ok(outcome, 'the aborted outcome is announced through the Chat status-notice primitive');
+  assert.equal(outcome.getAttribute('data-variant'), 'default');
+  assert.equal(outcome.textContent, 'Interrupted \u00b7 Stop button');
+});
+
+test('places the aborted turn outcome after its timeline content', async () => {
+  const { container, root } = domRoot();
+  await renderTurn(root, {
+    ...turnWith([{ ...ANSWER, live: false }]),
+    status: 'aborted',
+  });
+
+  const answer = container.querySelector('.maka-chat-message-bubble-assistant');
+  const assistantMessage = container.querySelector('.maka-assistant-answer');
+  const outcome = container.querySelector('.astryx-chat-system-message[role="status"]');
+  assert.ok(answer && assistantMessage && outcome);
+  assert.equal(assistantMessage.nextElementSibling?.isSameNode(outcome), true);
+});
+
 /**
  * Keying the answer by its first timeline entry made the key change whenever
  * that entry did, so React unmounted the answer and mounted a copy — taking
diff --git a/packages/ui/src/chat-display-helpers.ts b/packages/ui/src/chat-display-helpers.ts
index 555cb4101..5831a26ff 100644
--- a/packages/ui/src/chat-display-helpers.ts
+++ b/packages/ui/src/chat-display-helpers.ts
@@ -84,7 +84,7 @@ export function formatTurnDuration(ms: number): string {
   return `${Math.floor(totalSeconds / 60)}m ${totalSeconds % 60}s`;
 }
 
-export function turnAbortMarkerLabel(abortSource: string | undefined, locale: UiLocale): string {
+export function turnAbortStatusLabel(abortSource: string | undefined, locale: UiLocale): string {
   const copy = getConversationCopy(locale).messages;
   switch (abortSource) {
     case 'renderer.stop_button': return copy.abortedByStop;
diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx
index c2559e624..a3fd9fa89 100644
--- a/packages/ui/src/chat-turn.tsx
+++ b/packages/ui/src/chat-turn.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { memo, useEffect, useMemo, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react';
+import { Fragment, memo, useEffect, useMemo, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react';
 import { useMountedRef } from './use-mounted-ref.js';
 import { ICON_SIZE, Ban, Check, Copy, GitBranch, Info, Pencil, RefreshCcw, Timer } from './icons.js';
 import { type ClipboardCopyPhase, useClipboardCopyFeedback } from './clipboard-feedback.js';
@@ -25,7 +25,7 @@ import { Markdown } from './markdown.js';
 import {
   formatAbsoluteTimestamp,
   formatTurnDuration,
-  turnAbortMarkerLabel,
+  turnAbortStatusLabel,
 } from './chat-display-helpers.js';
 import { isTimeDrivenMotionEnabled } from './streaming-presentation.js';
 import { computerRunningLabel } from './tool-activity/computer-action-label.js';
@@ -645,27 +645,21 @@ export const TurnView = memo(function TurnView(props: {
           );
         }
         const ownsTurnChrome = segmentIndex === conversationSegments.length - 1;
+        // Disjoint namespaces: a steering id is any string, so a bare
+        // sentinel could collide with a real one.
+        const assistantKey =
+          segment.repliesTo === undefined
+            ? 'assistant-opening'
+            : `assistant-after-${segment.repliesTo}`;
         return (
-          <LocalizedChatMessage
-            // Disjoint namespaces: a steering id is any string, so a bare
-            // sentinel could collide with a real one.
-            key={
-              segment.repliesTo === undefined
-                ? 'assistant-opening'
-                : `assistant-after-${segment.repliesTo}`
-            }
-            accessibleLabel={copy.assistantAriaLabel}
-            sender="assistant"
-            data-turn-status={turn.status}
-            className="maka-chat-message maka-assistant-answer"
-          >
+          <Fragment key={assistantKey}>
+            <LocalizedChatMessage
+              accessibleLabel={copy.assistantAriaLabel}
+              sender="assistant"
+              data-turn-status={turn.status}
+              className="maka-chat-message maka-assistant-answer"
+            >
             <div className="maka-assistant-answer-content">
-              {ownsTurnChrome && turn.status === 'aborted' && (
-                <Marker variant="aborted" role="status">
-                  <Ban size={ICON_SIZE.meta} aria-hidden="true" />
-                  <em>{turnAbortMarkerLabel(turn.abortSource, locale)}</em>
-                </Marker>
-              )}
               {/* The turn timeline is the rendering source of truth
                 (materialize.ts): each step's 深度思考 disclosure, answer bubble,
                 and Astryx tool group in the order the model produced them.
@@ -699,12 +693,9 @@ export const TurnView = memo(function TurnView(props: {
                   />
                 ),
               )}
-              {/* A failed turn's banner states the OUTCOME, so it belongs after
-                  the work it is the outcome of. It used to render above the
-                  timeline, where it read as a header on reasoning and tool
-                  calls that had in fact all succeeded.
-
-                  `description` carries the parked-resume diagnostic when there
+              {/* A failed turn's banner states the OUTCOME of the turn, so it
+                  belongs after the work it is the outcome of. `description`
+                  carries the parked-resume diagnostic when there
                   is one — it explains why the button did nothing, which
                   outranks execution state on the one turn that can have both. */}
               {ownsTurnChrome && turn.status === 'failed' && props.failedReasonLabel && (
@@ -799,7 +790,16 @@ export const TurnView = memo(function TurnView(props: {
                   />
                 )
               ))}
-          </LocalizedChatMessage>
+            </LocalizedChatMessage>
+            {/* An abort is a short, settled status change rather than sender
+                content or a recovery error. Keep Astryx's system notice as a
+                sibling of the assistant message, after the work it closes. */}
+            {ownsTurnChrome && turn.status === 'aborted' && (
+              <ChatSystemMessage icon={<Ban size={ICON_SIZE.meta} aria-hidden="true" />}>
+                {turnAbortStatusLabel(turn.abortSource, locale)}
+              </ChatSystemMessage>
+            )}
+          </Fragment>
         );
       })}
     </section>
diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts
index 73e32dab4..e8f2b94e1 100644
--- a/packages/ui/src/conversation-copy.ts
+++ b/packages/ui/src/conversation-copy.ts
@@ -499,7 +499,7 @@ const CONVERSATION_COPY = {
     messages: {
       you: '你', assistant: 'Maka', processing: '正在处理…', continuing: '继续中…', workingPhrases: ['正在琢磨…', '正在推敲…', '正在盘算…', '正在钻研…', '正在忙活…', '正在梳理…', '正在打磨…', '正在鼓捣…', '正在酝酿…', '正在攻坚…', '正在权衡…', '正在拾掇…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `${formatRetryDelay(seconds, 'zh')}后重试(${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `正在重试(${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, maxAttempts) => `等待重试(${attempt}/${maxAttempts}) [...]
       userAriaLabel: '你发送的消息', systemAriaLabel: '系统消息', assistantAriaLabel: 'Maka 的回答', answerActionsAriaLabel: (context) => `回答操作${context ? `:${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action}回答${context ? `:${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action}消息${context ? `:${context}` : ''}`, sourceAriaLabel: '本轮回答的来源', derivativesAriaLabel: '本轮回答的衍生', scheduledTaskTriggered: '定时任务触发', scheduledTaskTitle: (id) => `由定时任务触发 · ${id}`, legac [...]
-      thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的任务日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '(已中断)', abortedByStop: '(已中断 · 由停止按钮触发)',
+      thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的任务日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '已中断', abortedByStop: '已中断 · 由停止按钮触发',
       systemNotes: {
         contextCompacted: '已压缩较早的对话内容,以适应模型上下文窗口。',
         contextCompactionFailedOpen: '上下文摘要失败;本轮已在未生成新摘要的情况下继续。',
@@ -647,7 +647,7 @@ const CONVERSATION_COPY = {
     messages: {
       you: 'You', assistant: 'Maka', processing: 'Working…', continuing: 'Continuing…', workingPhrases: ['Pondering…', 'Tinkering…', 'Untangling…', 'Digging in…', 'Mulling…', 'Chewing on it…', 'Wrangling…', 'Piecing it together…'], providerRetryScheduled: (seconds, attempt, maxAttempts) => `Retrying in ${formatRetryDelay(seconds, 'en')} (${attempt}/${maxAttempts})`, providerRetryStarted: (attempt, maxAttempts) => `Retrying (${attempt}/${maxAttempts})`, providerRetryWaiting: (attempt, max [...]
       userAriaLabel: 'Your message', systemAriaLabel: 'System message', assistantAriaLabel: "Maka's response", answerActionsAriaLabel: (context) => `Response actions${context ? `: ${context}` : ''}`, answerActionAriaLabel: (action, context) => `${action} response${context ? `: ${context}` : ''}`, messageActionAriaLabel: (action, context) => `${action} message${context ? `: ${context}` : ''}`, sourceAriaLabel: 'Source of this response', derivativesAriaLabel: 'Responses derived from this o [...]
-      thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted task log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: '(Interrupted)', [...]
+      thinkingTruncatedTitle: 'Some reasoning was truncated; showing the most recent content', outputTruncatedTitle: 'The assistant output exceeded the per-turn limit. Regenerate it or inspect the persisted task log for the complete content.', removeAttachmentAriaLabel: (name) => `Remove ${name}`, quoteLabel: 'Quote', quoteExpandAriaLabel: 'Show the full quoted excerpt', quoteCollapseAriaLabel: 'Collapse the quoted excerpt', removeQuoteAriaLabel: 'Remove quote', aborted: 'Interrupted', a [...]
       systemNotes: {
         contextCompacted: 'Context compacted to keep this session within the model window.',
         contextCompactionFailedOpen: 'Context summary failed; the session continued without a new summary.',
diff --git a/packages/ui/src/primitives/chat.tsx b/packages/ui/src/primitives/chat.tsx
index f14a0b223..87e237772 100644
--- a/packages/ui/src/primitives/chat.tsx
+++ b/packages/ui/src/primitives/chat.tsx
@@ -23,10 +23,10 @@ import type React from "react";
 import { cn } from "../utils.js";
 
 /**
- * `Marker` — the per-turn status / lineage / footer chrome (issue #332, PR2).
+ * `Marker` — the per-turn lineage / footer chrome (issue #332, PR2).
  *
- * Retires the bespoke `.maka-turn-summary*`, `.maka-turn-aborted-marker`,
- * `.maka-turn-lineage-*`, and `.maka-turn-footer*` shell
+ * Retires the bespoke `.maka-turn-summary*`, `.maka-turn-lineage-*`, and
+ * `.maka-turn-footer*` shell
  * CSS (spread across `maka-tokens.css`, `styles/settings/models.css`, and the
  * re-anchored measure-column block in `styles/tool-output.css`), moving each
  * onto package-owned semantic classes.
@@ -46,7 +46,6 @@ import { cn } from "../utils.js";
  *
  */
 export type MarkerVariant =
-  | "aborted"
   | "host-origin"
   | "lineage-row"
   | "lineage-row-reverse"
@@ -55,7 +54,6 @@ export type MarkerVariant =
   | "footer-action";
 
 const MARKER_CLASSES: Record<MarkerVariant, string> = {
-  aborted: "maka-turn-aborted-marker",
   "host-origin": "maka-turn-host-origin",
   "lineage-row": "maka-turn-lineage-row",
   "lineage-row-reverse": "maka-turn-lineage-row maka-turn-lineage-row-reverse",
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index 3bfe551df..be419c8c3 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -752,7 +752,6 @@
 .maka-sandbox-blocked-copy[data-copy-feedback="failed"] { border-color: oklch(from var(--destructive) l c h / 0.35); color: var(--destructive); }
 .maka-turn-status-icon { flex: 0 0 auto; }
 
-.maka-turn-aborted-marker,
 .maka-turn-host-origin {
   font: var(--maka-text-supporting);
   display: inline-flex;
@@ -762,17 +761,6 @@
   border-radius: var(--radius-control);
 }
 
-.maka-turn-aborted-marker {
-  gap: var(--space-1);
-  margin-block: var(--space-0-5) var(--space-1);
-  padding: var(--space-0-5) var(--space-1-5);
-  background: var(--foreground-5);
-  color: var(--foreground-secondary);
-  font-style: italic;
-}
-
-.maka-turn-aborted-marker em { font-style: italic; }
-
 .maka-turn-host-origin {
   align-self: flex-end;
   gap: var(--space-1);


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