diff --git a/apps/desktop/stories/app-shell.stories.tsx b/apps/desktop/stories/app-shell.stories.tsx
index 629a81c71c..eee701550c 100644
--- a/apps/desktop/stories/app-shell.stories.tsx
+++ b/apps/desktop/stories/app-shell.stories.tsx
@@ -3343,9 +3343,11 @@ export const NarrowWorkbarClearsTitlebarReserve: Story = {
// Real path (#3587): an explicit compaction runs as its own host Turn. The
// transcript shows a live "正在压缩上下文…" row driven by the live Turn snapshot
// (rootExecutionKind: 'context_compact'), with no assistant content of its own.
-export const ContextCompactionRunning: Story = {
- render: () => (
+function CompactionRunningScene(props: { motionEnabled?: boolean }) {
+ const [startedAt] = useState(() => props.motionEnabled ? Date.now() - 25_000 : NOW - 2_000);
+ return (
- ),
-};
+ );
+}
+
+export const ContextCompactionRunning: Story = { render: () => };
+export const ContextCompactionLive: Story = { render: () => };
// Real path (#3587): the compaction Turn ends. The live row settles into the
// durable `context_compacted` system note, rendered in transcript order.
@@ -3384,3 +3389,18 @@ export const ContextCompactionCompacted: Story = {
/>
),
};
+
+export const ContextCompactionFailed: Story = {
+ render: () => (
+
+ ),
+};
diff --git a/packages/ui/src/__tests__/materialize.test.ts b/packages/ui/src/__tests__/materialize.test.ts
index daf6390bd0..0bfbda1d24 100644
--- a/packages/ui/src/__tests__/materialize.test.ts
+++ b/packages/ui/src/__tests__/materialize.test.ts
@@ -204,15 +204,15 @@ describe("materializeChat message metadata", () => {
assert.equal(
materializeChat(messages, "en")[0]?.text,
- "Context compacted to keep this session within the model window.",
+ "Earlier context compacted.",
);
assert.equal(
materializeChat(messages, "zh-CN")[0]?.text,
- "已压缩较早的对话内容,以适应模型上下文窗口。",
+ "已压缩较早的上下文。",
);
assert.equal(
materializeTurns(messages, "zh-CN")[0]?.notes[0]?.text,
- "已压缩较早的对话内容,以适应模型上下文窗口。",
+ "已压缩较早的上下文。",
);
});
diff --git a/packages/ui/src/__tests__/transcript-projection.test.ts b/packages/ui/src/__tests__/transcript-projection.test.ts
index 1f154ed1c3..bfaa804e21 100644
--- a/packages/ui/src/__tests__/transcript-projection.test.ts
+++ b/packages/ui/src/__tests__/transcript-projection.test.ts
@@ -91,11 +91,11 @@ describe('incremental transcript projection', () => {
assert.equal(
english[0]?.notes[0]?.text,
- 'Context compacted to keep this session within the model window.',
+ 'Earlier context compacted.',
);
assert.equal(
chinese[0]?.notes[0]?.text,
- '已压缩较早的对话内容,以适应模型上下文窗口。',
+ '已压缩较早的上下文。',
);
assert.notStrictEqual(chinese, english);
});
diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx
index 153503f4f5..165fe6fd82 100644
--- a/packages/ui/src/chat-turn.tsx
+++ b/packages/ui/src/chat-turn.tsx
@@ -608,9 +608,17 @@ export const TurnView = memo(function TurnView(props: {
- {note.text}
+ {note.compactionState ? (
+
+ {note.compactionState === "running" && }
+ {note.text}
+ {note.compactionState === "running" && }
+
+ ) : note.text}
))}
{conversationSegments.map((segment, segmentIndex) => {
@@ -992,8 +1000,30 @@ export function TurnRunningStatus(props: {
activityLabel?: string;
}) {
const copy = getConversationCopy(useUiLocale()).messages;
+
+ return (
+
+ {props.showSpinner !== false && (
+
+ )}
+ {/* Name the activity once; the clock must not announce each second. */}
+
+
+ {props.activityLabel ?? copy.awaitingModelOutput}
+
+
+
+
+ );
+}
+
+function TurnElapsedTime(props: { startedAt?: number; separator?: boolean }) {
const { startedAt } = props;
- const rootRef = useRef(null);
+ const rootRef = useRef(null);
// Undefined until an effect measures it, which is also what keeps a static
// render deterministic: the clock is a client-only value, so server markup
// and the first paint carry the phrase alone.
@@ -1013,30 +1043,12 @@ export function TurnRunningStatus(props: {
}, [startedAt]);
return (
-
- {props.showSpinner !== false && (
-
- )}
- {/* Every visible token here moves on the clock. Announcing either would
- talk over the answer being streamed beside it, so the row's label is
- its whole accessible name and the text is decoration. */}
-
-
- {props.activityLabel ?? copy.awaitingModelOutput}
-
- {elapsedMs !== undefined && (
- <>
- ·
- {formatTurnDuration(elapsedMs)}
- >
- )}
-
-
+
+ {elapsedMs !== undefined && <>
+ {props.separator && ·}
+ {formatTurnDuration(elapsedMs)}
+ >}
+
);
}
diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts
index 9fe705f1ff..6c7939ebb7 100644
--- a/packages/ui/src/conversation-copy.ts
+++ b/packages/ui/src/conversation-copy.ts
@@ -553,8 +553,8 @@ const CONVERSATION_COPY = {
thinkingTruncatedTitle: '部分 reasoning 已截断;显示的是最近的内容', outputTruncatedTitle: '助手输出已超过单次回合上限,超出部分未渲染。如需完整内容请重新生成或查看持久化的任务日志。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展开引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '已中断', abortedByStop: '已中断 · 由停止按钮触发',
systemNotes: {
contextCompacting: '正在压缩上下文…',
- contextCompacted: '已压缩较早的对话内容,以适应模型上下文窗口。',
- contextCompactionFailedOpen: '上下文摘要失败;本轮已在未生成新摘要的情况下继续。',
+ contextCompacted: '已压缩较早的上下文。',
+ contextCompactionFailedOpen: '上下文压缩失败。',
contextProviderDropping: (used, prior) =>
`供应商在丢弃或改写上下文:追加了内容,它报告的输入却是 ${used.toLocaleString('zh-CN')} tokens,与之前的 ${prior.toLocaleString('zh-CN')} 相比没有增长。在连接设置里为该模型声明上下文窗口,让 Maka 先行压缩。`,
contextWindowSuggestion: (tokens, declared) =>
@@ -712,8 +712,8 @@ const CONVERSATION_COPY = {
thinkingTruncatedTitle: '部分 reasoning 已截斷;顯示的是最近的內容', outputTruncatedTitle: '助手輸出已超過單次回合上限,超出部分未渲染。如需完整內容請重新生成或檢視持久化的任務記錄。', removeAttachmentAriaLabel: (name) => `移除 ${name}`, quoteLabel: '引用', quoteExpandAriaLabel: '展開引用全文', quoteCollapseAriaLabel: '收起引用', removeQuoteAriaLabel: '移除引用', aborted: '(已中斷)', abortedByStop: '(已中斷 · 由停止按鈕觸發)',
systemNotes: {
contextCompacting: '正在壓縮上下文…',
- contextCompacted: '已壓縮較早的對話內容,以適應模型上下文視窗。',
- contextCompactionFailedOpen: '上下文摘要失敗;本輪已在未生成新摘要的情況下繼續。',
+ contextCompacted: '已壓縮較早的上下文。',
+ contextCompactionFailedOpen: '上下文壓縮失敗。',
contextProviderDropping: (used, prior) =>
`供應商在丟棄或改寫上下文:追加了內容,它報告的輸入卻是 ${used.toLocaleString('zh-TW')} tokens,與之前的 ${prior.toLocaleString('zh-TW')} 相比沒有成長。在連線設定裡為該模型宣告上下文視窗,讓 Maka 先行壓縮。`,
contextWindowSuggestion: (tokens, declared) =>
@@ -897,8 +897,8 @@ const CONVERSATION_COPY = {
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: {
contextCompacting: 'Compacting context…',
- contextCompacted: 'Context compacted to keep this session within the model window.',
- contextCompactionFailedOpen: 'Context summary failed; the session continued without a new summary.',
+ contextCompacted: 'Earlier context compacted.',
+ contextCompactionFailedOpen: 'Context compaction failed.',
contextProviderDropping: (used, prior) =>
`The provider is dropping or rewriting context: content was appended, and it counted ${used.toLocaleString('en-US')} input tokens against ${prior.toLocaleString('en-US')} before, which is no growth. Declare a context window for this model in the connection settings so Maka compacts first.`,
contextWindowSuggestion: (tokens, declared) =>
diff --git a/packages/ui/src/materialize.ts b/packages/ui/src/materialize.ts
index 3cacfa985c..265001eccd 100644
--- a/packages/ui/src/materialize.ts
+++ b/packages/ui/src/materialize.ts
@@ -49,6 +49,7 @@ import { getConversationCopy } from "./conversation-copy.js";
export { isCancelledToolResultContent, isInFlightToolStatus, toolResultActivityStatus } from '@maka/core/tool-result-status';
export interface ChatItem {
+ compactionState?: "running" | "compacted" | "failed";
id: string;
role: "user" | "assistant" | "system";
text: string;
@@ -227,6 +228,7 @@ export function materializeChat(
id: message.id,
role: "system",
text: systemNoteLabel(message.kind, message.data, locale),
+ compactionState: message.kind === "context_compacted" ? "compacted" : message.kind === "context_compaction_failed_open" ? "failed" : undefined,
ts: message.ts,
});
}
@@ -478,6 +480,7 @@ export function overlayLiveTurn(
id: noteId,
role: "system",
text: getConversationCopy(locale).messages.systemNotes.contextCompacting,
+ compactionState: "running",
ts: existing.startedAt,
};
return turns.map((turn, index) =>
@@ -496,6 +499,7 @@ export function overlayLiveTurn(
id: noteId,
role: "system",
text: getConversationCopy(locale).messages.systemNotes.contextCompacting,
+ compactionState: "running",
ts: startedAt,
},
],
@@ -855,6 +859,7 @@ export function materializeTurns(
id: message.id,
role: "system",
text: systemNoteLabel(message.kind, message.data, locale),
+ compactionState: message.kind === "context_compacted" ? "compacted" : message.kind === "context_compaction_failed_open" ? "failed" : undefined,
ts: message.ts,
});
} else if (message.type === "token_usage") {
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index ecbdf18e9b..c9881f00c9 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -243,7 +243,9 @@
.maka-turn-status-label { font-weight: var(--font-weight-medium); }
.maka-turn-status-separator { color: var(--foreground-alpha-16); }
/* Tabular figures so a ticking clock changes glyphs without changing width. */
-.maka-turn-elapsed { font-variant-numeric: tabular-nums; }
+.maka-turn-elapsed { display: inline-flex; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
+.maka-turn-elapsed:empty { display: none; }
+.maka-compaction-status { display: inline-flex; align-items: center; gap: 8px; font: var(--maka-text-body); vertical-align: top; }
/* #1879: an Astryx `Badge variant="yellow"` now — the warning tone it used to
draw by hand (hairline + 5% wash) is a variant, and the box comes with it.
Product CSS keeps only what is layout: it is a block-level note under the