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} ))} {conversationSegments.map((segment, segmentIndex) => { @@ -992,8 +1000,30 @@ export function TurnRunningStatus(props: { activityLabel?: string; }) { const copy = getConversationCopy(useUiLocale()).messages; + + return ( +
+ {props.showSpinner !== false && ( +
+ ); +} + +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 && ( -
+ ); } 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