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
32 changes: 26 additions & 6 deletions apps/desktop/stories/app-shell.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3343,29 +3343,34 @@ 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 (
<ComposedShell
motionEnabled={props.motionEnabled}
session={{ status: 'running', streaming: true }}
chat={{
runningStatus: true,
messages: [
user('msg-c-1', 'turn-c1', 6, '继续把上下文压缩那个功能实现完。'),
assistant('msg-c-2', 'turn-c1', 5, '好的,我先梳理一下现有实现,再动手。'),
{ type: 'turn_state', id: 'state-c1', turnId: 'turn-c1', ts: NOW - 300_000, status: 'completed' },
{ type: 'turn_state', id: 'state-compact', turnId: 'turn-compact', ts: NOW - 2_000, status: 'running' },
{ type: 'turn_state', id: 'state-compact', turnId: 'turn-compact', ts: startedAt, status: 'running' },
],
liveTurn: {
turnId: 'turn-compact',
phase: 'waiting',
steps: [],
rootExecutionKind: 'context_compact',
startedAt: NOW - 2_000,
startedAt,
},
}}
/>
),
};
);
}

export const ContextCompactionRunning: Story = { render: () => <CompactionRunningScene /> };
export const ContextCompactionLive: Story = { render: () => <CompactionRunningScene motionEnabled /> };

// Real path (#3587): the compaction Turn ends. The live row settles into the
// durable `context_compacted` system note, rendered in transcript order.
Expand All @@ -3384,3 +3389,18 @@ export const ContextCompactionCompacted: Story = {
/>
),
};

export const ContextCompactionFailed: Story = {
render: () => (
<ComposedShell
chat={{
messages: [
user('msg-c-1', 'turn-c1', 6, '继续把上下文压缩那个功能实现完。'),
assistant('msg-c-2', 'turn-c1', 5, '好的,我先梳理一下现有实现,再动手。'),
{ type: 'system_note', id: 'note-compact', turnId: 'turn-c1', ts: NOW - 1_000, kind: 'context_compaction_failed_open' },
{ type: 'turn_state', id: 'state-c1', turnId: 'turn-c1', ts: NOW - 1_000, status: 'completed' },
],
}}
/>
),
};
6 changes: 3 additions & 3 deletions packages/ui/src/__tests__/materialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
"已压缩较早的对话内容,以适应模型上下文窗口。",
"已压缩较早的上下文。",
);
});

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/__tests__/transcript-projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
);
Comment thread
Astro-Han marked this conversation as resolved.
assert.equal(
chinese[0]?.notes[0]?.text,
'已压缩较早的对话内容,以适应模型上下文窗口。',
'已压缩较早的上下文。',
);
assert.notStrictEqual(chinese, english);
});
Expand Down
66 changes: 39 additions & 27 deletions packages/ui/src/chat-turn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -608,9 +608,17 @@ export const TurnView = memo(function TurnView(props: {
<ChatSystemMessage
key={note.id}
className="maka-chat-system-message"
aria-label={copy.systemAriaLabel}
variant={note.compactionState === "running" || note.compactionState === "compacted" ? "divider" : "default"}
data-compaction-state={note.compactionState}
aria-label={note.compactionState === "running" ? note.text : copy.systemAriaLabel}
>
{note.text}
{note.compactionState ? (
<span className="maka-compaction-status">
{note.compactionState === "running" && <Spinner size="sm" shade="subtle" aria-hidden="true" />}
<span>{note.text}</span>
{note.compactionState === "running" && <TurnElapsedTime startedAt={turn.startedAt} />}
</span>
) : note.text}
</ChatSystemMessage>
))}
{conversationSegments.map((segment, segmentIndex) => {
Expand Down Expand Up @@ -992,8 +1000,30 @@ export function TurnRunningStatus(props: {
activityLabel?: string;
}) {
const copy = getConversationCopy(useUiLocale()).messages;

return (
<div
className="maka-turn-processing"
role="status"
aria-label={props.activityLabel ?? copy.awaitingModelOutput}
>
{props.showSpinner !== false && (
<Spinner size="md" shade="subtle" aria-hidden="true" />
)}
{/* Name the activity once; the clock must not announce each second. */}
<span className="maka-turn-indicator-text" aria-hidden="true">
<span className="maka-turn-status-label">
{props.activityLabel ?? copy.awaitingModelOutput}
</span>
<TurnElapsedTime startedAt={props.startedAt} separator />
</span>
</div>
);
}

function TurnElapsedTime(props: { startedAt?: number; separator?: boolean }) {
const { startedAt } = props;
const rootRef = useRef<HTMLDivElement>(null);
const rootRef = useRef<HTMLSpanElement>(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.
Expand All @@ -1013,30 +1043,12 @@ export function TurnRunningStatus(props: {
}, [startedAt]);

return (
<div
className="maka-turn-processing"
role="status"
aria-label={props.activityLabel ?? copy.awaitingModelOutput}
ref={rootRef}
>
{props.showSpinner !== false && (
<Spinner size="md" shade="subtle" aria-hidden="true" />
)}
{/* 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. */}
<span className="maka-turn-indicator-text" aria-hidden="true">
<span className="maka-turn-status-label">
{props.activityLabel ?? copy.awaitingModelOutput}
</span>
{elapsedMs !== undefined && (
<>
<span className="maka-turn-status-separator">·</span>
<span className="maka-turn-elapsed">{formatTurnDuration(elapsedMs)}</span>
</>
)}
</span>
</div>
<span className="maka-turn-elapsed" aria-hidden="true" ref={rootRef}>
{elapsedMs !== undefined && <>
{props.separator && <span className="maka-turn-status-separator">·</span>}
{formatTurnDuration(elapsedMs)}
</>}
</span>
);
}

Expand Down
12 changes: 6 additions & 6 deletions packages/ui/src/conversation-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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) =>
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/materialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
});
}
Expand Down Expand Up @@ -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) =>
Expand All @@ -496,6 +499,7 @@ export function overlayLiveTurn(
id: noteId,
role: "system",
text: getConversationCopy(locale).messages.systemNotes.contextCompacting,
compactionState: "running",
ts: startedAt,
},
],
Expand Down Expand Up @@ -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") {
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down