Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/desktop/src/renderer/styles/chat-detail.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions packages/ui/src/__tests__/chat-turn-answer-identity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/chat-display-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
56 changes: 28 additions & 28 deletions packages/ui/src/chat-turn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
* 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';
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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 && (
Expand Down Expand Up @@ -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>
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/conversation-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`, providerRetryReason: { network: '网络中断', provider_capacity: '模型服务暂时满载', provider_unavailable: '模型服务暂时不可用', rate_limit: '触发模型速率限制', timeout: '请求超时', unknown: '模型请求失败' }, safeResumePending: '正在检查…', safeResume: '继续这一轮', thinking: '深度思考', truncated: '已截断', copied: '已复制', copying: '复制中', copyFailed: '复制失败', copy: '复制', editMessage: '编辑并重发', editMessageDisabledRunning: '当前回答仍在进行中,结束后再编辑', editMessageDisabledAttachments: '包含附件的历史消息暂不支持编辑并重发', editMessageDisabledQuotes: '包含引用的历史消息暂不支持编辑并重发', editMessageDisabledTransformedText: '通过显式技能发送的历史消息暂不支持编辑并重发',
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}`, legacyAutomationTriggered: '旧版自动化(仅历史)', legacyAutomationTitle: (id) => `由旧版自动化触发 · ${id} · 仅保留历史,不会再次执行`, goalContinued: 'Goal 自动继续', goalTitle: (id) => `由 Goal 继续执行 · ${id}`, agentGraphTriggered: 'Agent Graph 自动继续', agentGraphTitle: (graphId) => `由 Agent Graph 调度器触发 · ${graphId}`,
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: '上下文摘要失败;本轮已在未生成新摘要的情况下继续。',
Expand Down Expand Up @@ -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, maxAttempts) => `Waiting to retry (${attempt}/${maxAttempts})`, providerRetryReason: { network: 'Network interrupted', provider_capacity: 'The model service is temporarily at capacity', provider_unavailable: 'Model service temporarily unavailable', rate_limit: 'Model rate limit reached', timeout: 'Request timed out', unknown: 'Model request failed' }, safeResumePending: 'Checking…', safeResume: 'Continue this turn', thinking: 'Thinking', truncated: 'Truncated', copied: 'Copied', copying: 'Copying', copyFailed: 'Copy failed', copy: 'Copy', editMessage: 'Edit & resend', editMessageDisabledRunning: 'Wait for this answer to finish before editing', editMessageDisabledAttachments: 'Edit & resend does not yet support messages with attachments', editMessageDisabledQuotes: 'Edit & resend does not yet support messages with quotes', editMessageDisabledTransformedText: 'Edit & resend does not yet support messages sent with an explicit skill',
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 one', scheduledTaskTriggered: 'Triggered by scheduled task', scheduledTaskTitle: (id) => `Triggered by scheduled task · ${id}`, legacyAutomationTriggered: 'Legacy Automation (history only)', legacyAutomationTitle: (id) => `Triggered by legacy Automation · ${id} · Historical only; it will not run again`, goalContinued: 'Continued by Goal', goalTitle: (id) => `Continued by Goal · ${id}`, agentGraphTriggered: 'Continued by Agent Graph', agentGraphTitle: (graphId) => `Triggered by the Agent Graph scheduler · ${graphId}`,
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)', abortedByStop: '(Interrupted · Stop button)',
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', abortedByStop: 'Interrupted · Stop button',
systemNotes: {
contextCompacted: 'Context compacted to keep this session within the model window.',
contextCompactionFailedOpen: 'Context summary failed; the session continued without a new summary.',
Expand Down
8 changes: 3 additions & 5 deletions packages/ui/src/primitives/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -46,7 +46,6 @@ import { cn } from "../utils.js";
*
*/
export type MarkerVariant =
| "aborted"
| "host-origin"
| "lineage-row"
| "lineage-row-reverse"
Expand All @@ -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",
Expand Down
12 changes: 0 additions & 12 deletions packages/ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down