From 796b3dd7358cb84c00b119e975ef1a713ee39272 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 7 Sep 2026 22:31:48 +0800 Subject: [PATCH 1/6] style(ui): present context compaction as a transcript checkpoint Generated-by: Codex --- apps/desktop/stories/app-shell.stories.tsx | 32 ++++- .../chat-view-empty-compaction.test.tsx | 116 ++++++++++++++++++ packages/ui/src/chat-turn.tsx | 66 ++++++---- packages/ui/src/materialize.ts | 9 +- packages/ui/src/styles.css | 4 +- 5 files changed, 191 insertions(+), 36 deletions(-) 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__/chat-view-empty-compaction.test.tsx b/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx index 61edf716a8..f89eb5dd03 100644 --- a/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx +++ b/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx @@ -19,6 +19,11 @@ import assert from 'node:assert/strict'; import test from 'node:test'; +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { parseHTML } from 'linkedom'; +import { TurnView } from '../chat-turn.js'; +import { materializeTurns, overlayLiveTurn } from '../materialize.js'; import { renderToStaticMarkup } from 'react-dom/server'; import type { SessionSummary } from '@maka/core/session'; import { ChatSurfaceLayout } from '../chat-surface-layout.js'; @@ -61,6 +66,10 @@ test('renders the live compaction row in a session with no settled messages', () // Before the fix, showEmptyState hid this overlaid row behind the empty hero // because it keyed off chat.length (0) and never saw the synthesized turn. assert.match(markup, /Compacting context/); + const { document } = parseHTML(markup); + const row = document.querySelector('[data-compaction-state="running"]'); + assert.equal(row?.getAttribute('data-variant'), 'divider'); + assert.equal(row?.querySelector('.astryx-spinner')?.getAttribute('aria-hidden'), 'true'); }); test('renders the empty hero when an empty session has no live compaction row', () => { @@ -68,3 +77,110 @@ test('renders the empty hero when an empty session has no live compaction row', assert.doesNotMatch(markup, /Compacting context/); }); + +for (const [kind, state, variant] of [ + ['context_compacted', 'compacted', 'divider'], + ['context_compaction_failed_open', 'failed', 'default'], +] as const) { + test(`durable ${kind} renders the appropriate compaction state`, () => { + const [turn] = materializeTurns( + [{ type: 'system_note', id: 'note', turnId: 'compact', ts: 1000, kind }], + 'en', + ); + const { document } = parseHTML( + renderToStaticMarkup( + + + , + ), + ); + const row = document.querySelector('[data-compaction-state]'); + assert.equal(row?.getAttribute('data-compaction-state'), state); + assert.equal(row?.getAttribute('data-variant'), variant); + assert.equal(row?.querySelector('.astryx-spinner'), null); + }); +} + +test('compaction clock uses the live Host start and disappears when the durable note takes over', async () => { + const previous = { window: globalThis.window, document: globalThis.document }; + const actGlobals = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }; + const previousAct = actGlobals.IS_REACT_ACT_ENVIRONMENT; + const { window, document } = parseHTML('
'); + Object.assign(globalThis, { window, document, IS_REACT_ACT_ENVIRONMENT: true }); + const container = document.querySelector('#root')!; + const root = createRoot(container); + try { + const [turn] = overlayLiveTurn( + materializeTurns( + [ + { + type: 'turn_state', + id: 'running', + turnId: 'compact', + ts: Date.now() - 60_000, + status: 'running', + partialOutputRetained: false, + }, + ], + 'en', + ), + { + turnId: 'compact', + phase: 'waiting', + steps: [], + rootExecutionKind: 'context_compact', + startedAt: Date.now() - 25_000, + }, + 'en', + ); + await act(() => + root.render( + + + , + ), + ); + const clock = container.querySelector('.maka-turn-elapsed')!; + assert.equal(clock.textContent, '25s'); + assert.equal(clock.getAttribute('aria-hidden'), 'true'); + await act(() => new Promise((resolve) => setTimeout(resolve, 1100))); + assert.equal(clock.textContent, '26s'); + const [done] = materializeTurns( + [ + { + type: 'system_note', + id: 'done', + turnId: 'compact', + ts: Date.now(), + kind: 'context_compacted', + }, + ], + 'en', + ); + await act(() => + root.render( + + + , + ), + ); + assert.equal(container.querySelector('.maka-turn-elapsed'), null); + assert.equal(container.querySelector('.astryx-spinner'), null); + assert.equal( + container.querySelector('[data-compaction-state]')?.getAttribute('data-variant'), + 'divider', + ); + container.setAttribute('data-maka-e2e-fixture', 'true'); + await act(() => + root.render( + + + , + ), + ); + assert.equal(container.querySelector('.maka-turn-elapsed')?.textContent, ''); + } finally { + await act(() => root.unmount()); + Object.assign(globalThis, previous, { IS_REACT_ACT_ENVIRONMENT: previousAct }); + } +}); diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx index 153503f4f5..67b55a627d 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/materialize.ts b/packages/ui/src/materialize.ts index 3cacfa985c..16a4410825 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,7 +480,8 @@ export function overlayLiveTurn( id: noteId, role: "system", text: getConversationCopy(locale).messages.systemNotes.contextCompacting, - ts: existing.startedAt, + compactionState: "running", + ts: liveTurn.startedAt ?? existing.startedAt, }; return turns.map((turn, index) => index === targetIndex ? { ...turn, notes: [...turn.notes, note] } : turn, @@ -496,7 +499,8 @@ export function overlayLiveTurn( id: noteId, role: "system", text: getConversationCopy(locale).messages.systemNotes.contextCompacting, - ts: startedAt, + compactionState: "running", + ts: liveTurn.startedAt, }, ], timeline: [], @@ -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 From f45aa88f1c1e8ed807c9c0f0009042408d2ff08e Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 7 Sep 2026 22:45:34 +0800 Subject: [PATCH 2/6] style(ui): simplify localized context compaction notices Generated-by: Codex Generated-by: Gemini 3.8 Flash (Antigravity) --- packages/ui/src/__tests__/materialize.test.ts | 6 +++--- .../ui/src/__tests__/transcript-projection.test.ts | 4 ++-- packages/ui/src/conversation-copy.ts | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) 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/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index 9fe705f1ff..e567076176 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; the session continued without a new summary.', 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) => From 10d5d86eea078d0f52b399ac4151b3fdee4ac94e Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 7 Sep 2026 22:50:03 +0800 Subject: [PATCH 3/6] style(ui): explain compaction failure in terms of the user request Generated-by: Codex --- packages/ui/src/conversation-copy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index e567076176..fdea12a792 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -554,7 +554,7 @@ const CONVERSATION_COPY = { systemNotes: { contextCompacting: '正在压缩上下文…', contextCompacted: '已压缩较早的上下文。', - contextCompactionFailedOpen: '上下文压缩失败;会话已继续,未生成新摘要。', + contextCompactionFailedOpen: '上下文压缩失败,Maka 已继续处理你的请求。', contextProviderDropping: (used, prior) => `供应商在丢弃或改写上下文:追加了内容,它报告的输入却是 ${used.toLocaleString('zh-CN')} tokens,与之前的 ${prior.toLocaleString('zh-CN')} 相比没有增长。在连接设置里为该模型声明上下文窗口,让 Maka 先行压缩。`, contextWindowSuggestion: (tokens, declared) => @@ -713,7 +713,7 @@ const CONVERSATION_COPY = { systemNotes: { contextCompacting: '正在壓縮上下文…', contextCompacted: '已壓縮較早的上下文。', - contextCompactionFailedOpen: '上下文壓縮失敗;會話已繼續,未產生新摘要。', + contextCompactionFailedOpen: '上下文壓縮失敗,Maka 已繼續處理你的請求。', contextProviderDropping: (used, prior) => `供應商在丟棄或改寫上下文:追加了內容,它報告的輸入卻是 ${used.toLocaleString('zh-TW')} tokens,與之前的 ${prior.toLocaleString('zh-TW')} 相比沒有成長。在連線設定裡為該模型宣告上下文視窗,讓 Maka 先行壓縮。`, contextWindowSuggestion: (tokens, declared) => @@ -898,7 +898,7 @@ const CONVERSATION_COPY = { systemNotes: { contextCompacting: 'Compacting context…', contextCompacted: 'Earlier context compacted.', - contextCompactionFailedOpen: 'Context compaction failed; the session continued without a new summary.', + contextCompactionFailedOpen: 'Context compaction failed. Maka continued with your request.', 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) => From df6dd4632cc5f9f34a7b9a52958d866dd2fe6f39 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 7 Sep 2026 23:41:55 +0800 Subject: [PATCH 4/6] fix(ui): preserve compaction note timestamps when showing elapsed time Generated-by: Codex --- .../ui/src/__tests__/chat-view-empty-compaction.test.tsx | 6 +++--- packages/ui/src/chat-turn.tsx | 2 +- packages/ui/src/materialize.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx b/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx index f89eb5dd03..103f936406 100644 --- a/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx +++ b/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx @@ -101,7 +101,7 @@ for (const [kind, state, variant] of [ }); } -test('compaction clock uses the live Host start and disappears when the durable note takes over', async () => { +test('compaction clock uses the recorded Turn start and disappears when the durable note takes over', async () => { const previous = { window: globalThis.window, document: globalThis.document }; const actGlobals = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }; const previousAct = actGlobals.IS_REACT_ACT_ENVIRONMENT; @@ -117,7 +117,7 @@ test('compaction clock uses the live Host start and disappears when the durable type: 'turn_state', id: 'running', turnId: 'compact', - ts: Date.now() - 60_000, + ts: Date.now() - 25_000, status: 'running', partialOutputRetained: false, }, @@ -129,7 +129,7 @@ test('compaction clock uses the live Host start and disappears when the durable phase: 'waiting', steps: [], rootExecutionKind: 'context_compact', - startedAt: Date.now() - 25_000, + startedAt: Date.now() - 10_000, }, 'en', ); diff --git a/packages/ui/src/chat-turn.tsx b/packages/ui/src/chat-turn.tsx index 67b55a627d..165fe6fd82 100644 --- a/packages/ui/src/chat-turn.tsx +++ b/packages/ui/src/chat-turn.tsx @@ -616,7 +616,7 @@ export const TurnView = memo(function TurnView(props: { {note.compactionState === "running" && ) : note.text} diff --git a/packages/ui/src/materialize.ts b/packages/ui/src/materialize.ts index 16a4410825..265001eccd 100644 --- a/packages/ui/src/materialize.ts +++ b/packages/ui/src/materialize.ts @@ -481,7 +481,7 @@ export function overlayLiveTurn( role: "system", text: getConversationCopy(locale).messages.systemNotes.contextCompacting, compactionState: "running", - ts: liveTurn.startedAt ?? existing.startedAt, + ts: existing.startedAt, }; return turns.map((turn, index) => index === targetIndex ? { ...turn, notes: [...turn.notes, note] } : turn, @@ -500,7 +500,7 @@ export function overlayLiveTurn( role: "system", text: getConversationCopy(locale).messages.systemNotes.contextCompacting, compactionState: "running", - ts: liveTurn.startedAt, + ts: startedAt, }, ], timeline: [], From 104596040ed44bdfa2410d40964ae82bb187a98e Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 7 Sep 2026 23:41:55 +0800 Subject: [PATCH 5/6] style(ui): avoid promising continuation after compaction failure Generated-by: Codex --- packages/ui/src/conversation-copy.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/conversation-copy.ts b/packages/ui/src/conversation-copy.ts index fdea12a792..6c7939ebb7 100644 --- a/packages/ui/src/conversation-copy.ts +++ b/packages/ui/src/conversation-copy.ts @@ -554,7 +554,7 @@ const CONVERSATION_COPY = { systemNotes: { contextCompacting: '正在压缩上下文…', contextCompacted: '已压缩较早的上下文。', - contextCompactionFailedOpen: '上下文压缩失败,Maka 已继续处理你的请求。', + contextCompactionFailedOpen: '上下文压缩失败。', contextProviderDropping: (used, prior) => `供应商在丢弃或改写上下文:追加了内容,它报告的输入却是 ${used.toLocaleString('zh-CN')} tokens,与之前的 ${prior.toLocaleString('zh-CN')} 相比没有增长。在连接设置里为该模型声明上下文窗口,让 Maka 先行压缩。`, contextWindowSuggestion: (tokens, declared) => @@ -713,7 +713,7 @@ const CONVERSATION_COPY = { systemNotes: { contextCompacting: '正在壓縮上下文…', contextCompacted: '已壓縮較早的上下文。', - contextCompactionFailedOpen: '上下文壓縮失敗,Maka 已繼續處理你的請求。', + contextCompactionFailedOpen: '上下文壓縮失敗。', contextProviderDropping: (used, prior) => `供應商在丟棄或改寫上下文:追加了內容,它報告的輸入卻是 ${used.toLocaleString('zh-TW')} tokens,與之前的 ${prior.toLocaleString('zh-TW')} 相比沒有成長。在連線設定裡為該模型宣告上下文視窗,讓 Maka 先行壓縮。`, contextWindowSuggestion: (tokens, declared) => @@ -898,7 +898,7 @@ const CONVERSATION_COPY = { systemNotes: { contextCompacting: 'Compacting context…', contextCompacted: 'Earlier context compacted.', - contextCompactionFailedOpen: 'Context compaction failed. Maka continued with your request.', + 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) => From b36e3ca15dabb5041c20419ac990a5658da3dc4c Mon Sep 17 00:00:00 2001 From: AstroHan Date: Mon, 7 Sep 2026 23:45:05 +0800 Subject: [PATCH 6/6] test(ui): remove redundant compaction presentation checks Generated-by: Codex --- .../chat-view-empty-compaction.test.tsx | 116 ------------------ 1 file changed, 116 deletions(-) diff --git a/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx b/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx index 103f936406..61edf716a8 100644 --- a/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx +++ b/packages/ui/src/__tests__/chat-view-empty-compaction.test.tsx @@ -19,11 +19,6 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { act } from 'react'; -import { createRoot } from 'react-dom/client'; -import { parseHTML } from 'linkedom'; -import { TurnView } from '../chat-turn.js'; -import { materializeTurns, overlayLiveTurn } from '../materialize.js'; import { renderToStaticMarkup } from 'react-dom/server'; import type { SessionSummary } from '@maka/core/session'; import { ChatSurfaceLayout } from '../chat-surface-layout.js'; @@ -66,10 +61,6 @@ test('renders the live compaction row in a session with no settled messages', () // Before the fix, showEmptyState hid this overlaid row behind the empty hero // because it keyed off chat.length (0) and never saw the synthesized turn. assert.match(markup, /Compacting context/); - const { document } = parseHTML(markup); - const row = document.querySelector('[data-compaction-state="running"]'); - assert.equal(row?.getAttribute('data-variant'), 'divider'); - assert.equal(row?.querySelector('.astryx-spinner')?.getAttribute('aria-hidden'), 'true'); }); test('renders the empty hero when an empty session has no live compaction row', () => { @@ -77,110 +68,3 @@ test('renders the empty hero when an empty session has no live compaction row', assert.doesNotMatch(markup, /Compacting context/); }); - -for (const [kind, state, variant] of [ - ['context_compacted', 'compacted', 'divider'], - ['context_compaction_failed_open', 'failed', 'default'], -] as const) { - test(`durable ${kind} renders the appropriate compaction state`, () => { - const [turn] = materializeTurns( - [{ type: 'system_note', id: 'note', turnId: 'compact', ts: 1000, kind }], - 'en', - ); - const { document } = parseHTML( - renderToStaticMarkup( - - - , - ), - ); - const row = document.querySelector('[data-compaction-state]'); - assert.equal(row?.getAttribute('data-compaction-state'), state); - assert.equal(row?.getAttribute('data-variant'), variant); - assert.equal(row?.querySelector('.astryx-spinner'), null); - }); -} - -test('compaction clock uses the recorded Turn start and disappears when the durable note takes over', async () => { - const previous = { window: globalThis.window, document: globalThis.document }; - const actGlobals = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }; - const previousAct = actGlobals.IS_REACT_ACT_ENVIRONMENT; - const { window, document } = parseHTML('
'); - Object.assign(globalThis, { window, document, IS_REACT_ACT_ENVIRONMENT: true }); - const container = document.querySelector('#root')!; - const root = createRoot(container); - try { - const [turn] = overlayLiveTurn( - materializeTurns( - [ - { - type: 'turn_state', - id: 'running', - turnId: 'compact', - ts: Date.now() - 25_000, - status: 'running', - partialOutputRetained: false, - }, - ], - 'en', - ), - { - turnId: 'compact', - phase: 'waiting', - steps: [], - rootExecutionKind: 'context_compact', - startedAt: Date.now() - 10_000, - }, - 'en', - ); - await act(() => - root.render( - - - , - ), - ); - const clock = container.querySelector('.maka-turn-elapsed')!; - assert.equal(clock.textContent, '25s'); - assert.equal(clock.getAttribute('aria-hidden'), 'true'); - await act(() => new Promise((resolve) => setTimeout(resolve, 1100))); - assert.equal(clock.textContent, '26s'); - const [done] = materializeTurns( - [ - { - type: 'system_note', - id: 'done', - turnId: 'compact', - ts: Date.now(), - kind: 'context_compacted', - }, - ], - 'en', - ); - await act(() => - root.render( - - - , - ), - ); - assert.equal(container.querySelector('.maka-turn-elapsed'), null); - assert.equal(container.querySelector('.astryx-spinner'), null); - assert.equal( - container.querySelector('[data-compaction-state]')?.getAttribute('data-variant'), - 'divider', - ); - container.setAttribute('data-maka-e2e-fixture', 'true'); - await act(() => - root.render( - - - , - ), - ); - assert.equal(container.querySelector('.maka-turn-elapsed')?.textContent, ''); - } finally { - await act(() => root.unmount()); - Object.assign(globalThis, previous, { IS_REACT_ACT_ENVIRONMENT: previousAct }); - } -});