diff --git a/mateclaw-ui/src/components/chat/MessageBubble.vue b/mateclaw-ui/src/components/chat/MessageBubble.vue
index 4514aec5..6a525c2a 100644
--- a/mateclaw-ui/src/components/chat/MessageBubble.vue
+++ b/mateclaw-ui/src/components/chat/MessageBubble.vue
@@ -35,6 +35,15 @@
paths, so plan-mode progress is never buried in a collapsed panel. -->
+
+
@@ -1433,6 +1442,24 @@ onBeforeUnmount(() => {
}
})
+/**
+ * First-token wait state: the assistant bubble exists (placeholder created
+ * synchronously on send) but nothing renderable has arrived — no segments, no
+ * thinking, no content, no phase-driven execution panel. Without this the
+ * bubble sits blank for the whole context-prep + LLM-prefill window (multiple
+ * seconds) and reads as a hang; the placeholder yields automatically the
+ * moment any real block renders.
+ */
+const showPendingPlaceholder = computed(() =>
+ role.value === 'assistant'
+ && isGenerating.value
+ && segments.value.length === 0
+ && !thinkingContent.value
+ && !displayContent.value
+ && !showExecutionPanel.value
+ && !planMeta.value
+)
+
const phaseElapsed = computed(() => {
// Only meaningful while waiting on the model with nothing else to show.
if (!isGenerating.value || toolCallsMeta.value.length) return ''
@@ -1565,6 +1592,48 @@ watch(isGenerating, (generating) => {
font-size: 11px;
}
+.pending-placeholder {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ align-self: flex-start;
+ padding: 8px 14px;
+ margin: 4px 0;
+ font-family: var(--mc-font-body, sans-serif);
+ font-size: var(--mc-text-sm, 13px);
+ color: var(--mc-text-secondary, #665245);
+ background: var(--mc-bg-muted, #f1e8df);
+ border-radius: var(--mc-radius-md, 12px);
+}
+
+.pending-placeholder__dots {
+ display: inline-flex;
+ gap: 4px;
+}
+
+.pending-placeholder__dots i {
+ width: 6px;
+ height: 6px;
+ border-radius: var(--mc-radius-full, 9999px);
+ background: var(--mc-primary, #d96d46);
+ opacity: 0.35;
+ animation: pending-dot-pulse 1.2s ease-in-out infinite;
+}
+
+.pending-placeholder__dots i:nth-child(2) { animation-delay: 0.2s; }
+.pending-placeholder__dots i:nth-child(3) { animation-delay: 0.4s; }
+
+@keyframes pending-dot-pulse {
+ 0%, 60%, 100% { opacity: 0.35; transform: translateY(0); }
+ 30% { opacity: 1; transform: translateY(-3px); }
+}
+
+.pending-placeholder__elapsed {
+ font-size: var(--mc-text-xs, 11px);
+ color: var(--mc-text-tertiary, #9b7d6c);
+ font-variant-numeric: tabular-nums;
+}
+
.superseded-toggle {
display: inline-flex;
align-items: center;
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index fb6e7c43..98b768fd 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -531,7 +531,8 @@ export default {
// Per-iteration grouping
iterationEmpty: 'Iteration {index} interrupted (no output)',
contentRepetitionWarning: 'Repetitive content detected near the end (model artifact)',
- supersededPreviewCollapsed: 'Model preview replaced by the actual tool result',
+ supersededPreviewCollapsed: 'Collapsed: content drafted before tool execution (may not match actual results)',
+ pendingReply: 'Preparing a reply…',
expand: 'Expand',
// INCOMPLETE truncation card (finishReason=incomplete)
incompleteTitle: 'Answer auto-truncated after repeated output was detected',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 5b1e679f..d38ce858 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -531,7 +531,8 @@ export default {
// 按轮次分组渲染
iterationEmpty: '第 {index} 轮被中断(无输出)',
contentRepetitionWarning: '检测到内容尾部重复(疑似模型输出 artifact)',
- supersededPreviewCollapsed: '过程预演已被实际工具结果替换',
+ supersededPreviewCollapsed: '已折叠:模型在工具执行前预写的内容(可能与实际结果不符)',
+ pendingReply: '正在准备回复…',
expand: '展开',
// INCOMPLETE 截断卡片(finishReason=incomplete)
incompleteTitle: '回答因检测到重复输出已被自动截断',