Skip to content

Commit 6c5a958

Browse files
committed
feat(ui): 将加载状态替换为动态思考气泡
1 parent d0fc3a0 commit 6c5a958

2 files changed

Lines changed: 91 additions & 39 deletions

File tree

resources/webview.css

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -905,28 +905,38 @@ body {
905905
color: var(--vscode-descriptionForeground);
906906
}
907907

908-
/* Loading Indicator */
909-
.loading {
910-
display: none;
911-
align-items: center;
912-
gap: 8px;
913-
color: var(--muted);
914-
font-size: 12px;
915-
padding: 16px 10px;
916-
flex-shrink: 0;
917-
}
918-
919-
.loading.active {
920-
display: flex;
908+
/* Spinner dot for live thinking bubble */
909+
.bubble-dot.spinner-dot {
910+
background: transparent !important;
921911
}
922912

923-
.spinner {
924-
width: 14px;
925-
height: 14px;
913+
/* 旋转环用 ::after 实现,不影响 ::before 连线 */
914+
.bubble-dot.spinner-dot::after {
915+
content: "";
916+
position: absolute;
917+
top: -2px;
918+
left: -2px;
919+
width: calc(100% + 4px);
920+
height: calc(100% + 4px);
926921
border-radius: 50%;
927922
border: 2px solid var(--vscode-progressBar-background);
928923
border-top-color: var(--accent);
929924
animation: spin 0.8s linear infinite;
925+
box-sizing: border-box;
926+
pointer-events: none;
927+
z-index: 1;
928+
}
929+
930+
/* Thinking bubble: 标题和内容同行显示 */
931+
.bubble[data-thinking-live="true"] .thinking-status {
932+
color: var(--vscode-descriptionForeground);
933+
font-weight: normal;
934+
font-size: 12px;
935+
white-space: nowrap;
936+
overflow: hidden;
937+
text-overflow: ellipsis;
938+
flex: 1;
939+
min-width: 0;
930940
}
931941

932942
@keyframes spin {

resources/webview.html

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@
3838
<!-- 聊天视图 -->
3939
<div class="chat-container" id="chatContainer">
4040
<div class="messages" id="messages"></div>
41-
<div class="loading" id="loading">
42-
<div class="spinner"></div>
43-
<span>Thinking...</span>
44-
</div>
4541
<div class="composer">
4642
<!-- Skills Popup -->
4743
<div class="skills-popup" id="skillsPopup">
@@ -97,7 +93,6 @@
9793
const promptMinRows = Number(promptInput?.getAttribute("rows")) || 3;
9894
const promptMaxRows = 10;
9995
const sendButton = document.getElementById("send");
100-
const loading = document.getElementById("loading");
10196
const sessionSelector = document.getElementById("sessionSelector");
10297
const sessionSelectorTitle = document.getElementById("sessionSelectorTitle");
10398
const sessionSelectorTitleText = document.getElementById("sessionSelectorTitleText");
@@ -1269,35 +1264,80 @@
12691264
return `${seconds}s`;
12701265
}
12711266

1272-
function updateLoadingText() {
1273-
const loadingSpan = loading.querySelector('span');
1274-
if (!loadingSpan) {
1275-
return;
1276-
}
1267+
let currentThinkingBubbleElement = null; // 当前 thinking 气泡 DOM 元素
1268+
1269+
function getThinkingContentText() {
12771270
if (currentRunningProcesses && Object.keys(currentRunningProcesses).length > 0) {
1278-
// 显示第一个运行中的进程(通常只有一个进程在运行)
12791271
const firstPid = Object.keys(currentRunningProcesses)[0];
12801272
const process = currentRunningProcesses[firstPid];
12811273
const elapsed = formatElapsedTime(process.startTime);
1282-
loadingSpan.textContent = `(${elapsed}) ${process.command}`;
1283-
return;
1274+
return `(${elapsed}) ${process.command}`;
12841275
}
12851276

12861277
if (currentLlmStreamProgress && currentLlmStreamProgress.startedAt) {
12871278
const startedAt = new Date(currentLlmStreamProgress.startedAt).getTime();
12881279
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));
1280+
const formattedTokens = currentLlmStreamProgress.formattedTokens || "0";
12891281
if (elapsedSeconds >= 3) {
1290-
const formattedTokens = currentLlmStreamProgress.formattedTokens || "0";
1291-
loadingSpan.textContent = `Thinking... (${elapsedSeconds}s) · ↓ ${formattedTokens} tokens`;
1292-
return;
1282+
return `(${elapsedSeconds}s) · ↓ ${formattedTokens} tokens`;
12931283
}
12941284
}
12951285

1296-
loadingSpan.textContent = 'Thinking...';
1286+
return "Processing...";
1287+
}
1288+
1289+
function updateThinkingBubbleContent() {
1290+
if (!currentThinkingBubbleElement) return;
1291+
const statusEl = currentThinkingBubbleElement.querySelector(".thinking-status");
1292+
if (statusEl) {
1293+
statusEl.textContent = getThinkingContentText();
1294+
}
1295+
}
1296+
1297+
function removeThinkingBubble() {
1298+
if (currentThinkingBubbleElement) {
1299+
currentThinkingBubbleElement.remove();
1300+
currentThinkingBubbleElement = null;
1301+
}
1302+
}
1303+
1304+
function createThinkingBubble(shouldConnectToPrev) {
1305+
removeThinkingBubble();
1306+
1307+
const bubble = document.createElement("div");
1308+
bubble.className = "bubble assistant";
1309+
bubble.dataset.thinkingLive = "true";
1310+
1311+
const header = document.createElement("div");
1312+
header.className = "bubble-collapsible-header";
1313+
1314+
const dotClass = shouldConnectToPrev ? "bubble-dot spinner-dot connect-to-prev" : "bubble-dot spinner-dot";
1315+
const statusText = getThinkingContentText();
1316+
header.innerHTML = `
1317+
<span class="${dotClass}"></span>
1318+
<span class="bubble-title">
1319+
<span class="bubble-title-text"><b>Thinking</b> <span class="thinking-status">${escapeHtml(statusText)}</span></span>
1320+
</span>
1321+
`;
1322+
1323+
bubble.appendChild(header);
1324+
messages.appendChild(bubble);
1325+
messages.scrollTop = messages.scrollHeight;
1326+
1327+
currentThinkingBubbleElement = bubble;
1328+
1329+
// 连接线
1330+
if (shouldConnectToPrev) {
1331+
requestAnimationFrame(() => {
1332+
updateConnectionLine(bubble);
1333+
});
1334+
}
1335+
requestAnimationFrame(() => {
1336+
updateAllConnectionLines();
1337+
});
12971338
}
12981339

12991340
function setLoading(isLoading, status) {
1300-
loading.classList.toggle("active", isLoading);
13011341
if (status !== undefined) {
13021342
currentSessionStatus = status;
13031343
}
@@ -1312,12 +1352,14 @@
13121352
}
13131353

13141354
if (isLoading) {
1315-
loadingUpdateTimer = setInterval(updateLoadingText, 1000);
1355+
// 创建 thinking bubble,连到上一条非 user 消息
1356+
const shouldConnect = lastMessageRole !== null && lastMessageRole !== "user";
1357+
createThinkingBubble(shouldConnect);
1358+
loadingUpdateTimer = setInterval(updateThinkingBubbleContent, 1000);
1359+
} else {
1360+
removeThinkingBubble();
13161361
}
13171362

1318-
// 更新初始文本
1319-
updateLoadingText();
1320-
13211363
// 切换图标
13221364
if (isLoading) {
13231365
sendIcon.style.display = "none";
@@ -1892,7 +1934,7 @@
18921934
} else {
18931935
currentLlmStreamProgress = progress;
18941936
}
1895-
updateLoadingText();
1937+
updateThinkingBubbleContent();
18961938
} else if (message.type === "userMessage") {
18971939
// 显示用户消息(纯文本)
18981940
addBubble(message.content || "", "user", message.meta, false);

0 commit comments

Comments
 (0)