@bolloon/bolloon-agent 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,8 @@ if (typeof marked === 'undefined') {
4
4
  }
5
5
 
6
6
  const messagesEl = document.getElementById('messages');
7
+ const agentStatusEl = document.getElementById('agent-status');
8
+ const agentStatusTextEl = document.getElementById('agent-status-text');
7
9
  const input = document.getElementById('input');
8
10
  const sendBtn = document.getElementById('send');
9
11
  const sidebar = document.getElementById('sidebar');
@@ -900,20 +902,45 @@ function addMessage(content, type, save = true, container) {
900
902
  msgContainer.scrollTop = msgContainer.scrollHeight;
901
903
  }
902
904
 
905
+ // Agent status bar — sits between the message list and the input box.
906
+ // Two visual states: "planning" (spinner) and "executing" (glowing icon).
907
+ // The text alternates to convey the action loop.
908
+ let agentStatusState = null; // 'planning' | 'executing' | null
909
+ let agentStatusTextIdx = 0;
910
+
911
+ const AGENT_STATUS_TEXTS = {
912
+ planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
913
+ executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
914
+ };
915
+
916
+ function setAgentStatus(state) {
917
+ if (!agentStatusEl || !agentStatusTextEl) return;
918
+ if (state === null) {
919
+ agentStatusEl.hidden = true;
920
+ agentStatusEl.removeAttribute('data-mode');
921
+ agentStatusState = null;
922
+ return;
923
+ }
924
+ agentStatusEl.hidden = false;
925
+ agentStatusEl.setAttribute('data-mode', state);
926
+ agentStatusState = state;
927
+ // 重排一下文本, 避免长时间停留过于单调
928
+ agentStatusTextIdx = (agentStatusTextIdx + 1) % AGENT_STATUS_TEXTS[state].length;
929
+ agentStatusTextEl.textContent = AGENT_STATUS_TEXTS[state][agentStatusTextIdx];
930
+ }
931
+
903
932
  function showTyping(container) {
904
- const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
905
933
  hideTyping();
906
- const div = document.createElement('div');
907
- div.className = 'message message-ai';
908
- div.id = 'typing';
909
- div.innerHTML = '<div class="typing"><div class="typing-spinner"></div><span class="typing-text">思考中...</span></div>';
910
- msgContainer.appendChild(div);
911
- msgContainer.scrollTop = msgContainer.scrollHeight;
934
+ // 兼容旧路径: container 参数保留但不再使用, status bar 是全局唯一的
935
+ void container;
936
+ setAgentStatus('planning');
912
937
  }
913
938
 
914
939
  function hideTyping() {
915
- const typing = document.getElementById('typing');
916
- if (typing) typing.remove();
940
+ setAgentStatus(null);
941
+ // 兜底: 旧版本的 #typing 元素可能还残留在 DOM 里, 顺手清掉
942
+ const old = document.getElementById('typing');
943
+ if (old) old.remove();
917
944
  hideStreaming();
918
945
  }
919
946
 
@@ -1346,8 +1373,10 @@ function connect(channelId) {
1346
1373
  showUserCommand(data.content, container);
1347
1374
  } else if (data.type === 'ai') {
1348
1375
  addMessage(data.content, 'ai', true, container);
1376
+ hideTyping();
1349
1377
  } else if (data.type === 'stream') {
1350
1378
  handleStreamEvent(data, container);
1379
+ setAgentStatus('executing');
1351
1380
  } else if (data.type === 'regenerating') {
1352
1381
  const messages = container.querySelectorAll('.message-ai');
1353
1382
  if (messages.length > 0) {
@@ -1357,6 +1386,7 @@ function connect(channelId) {
1357
1386
  showTyping(container);
1358
1387
  } else if (data.type === 'status') {
1359
1388
  handleStatusEvent(data, container);
1389
+ setAgentStatus('executing');
1360
1390
  } else if (data.type === 'done') {
1361
1391
  hideTyping();
1362
1392
  // AI 回复完, 把最后一条 ai 消息落盘 (兜底, 避免 server saveSession 漏写)
@@ -261,12 +261,12 @@
261
261
  </div>
262
262
  </div>
263
263
 
264
- <div class="messages" id="messages">
265
- <div class="message message-ai">
266
- <div class="bubble bubble-ai">
267
- <span class="loading-dots">加载中<span>.</span><span>.</span><span>.</span></span>
268
- </div>
269
- </div>
264
+ <div class="messages" id="messages"></div>
265
+
266
+ <div class="agent-status" id="agent-status" hidden aria-live="polite">
267
+ <span class="agent-status-spinner" aria-hidden="true"></span>
268
+ <span class="agent-status-icon" aria-hidden="true"></span>
269
+ <span class="agent-status-text" id="agent-status-text">正在计划下一步</span>
270
270
  </div>
271
271
 
272
272
  <div class="input-area">
@@ -1462,21 +1462,96 @@ body {
1462
1462
  50% { opacity: 1; transform: scale(1.2); }
1463
1463
  }
1464
1464
 
1465
- /* Loading Dots */
1466
- .loading-dots {
1465
+ /* Agent status bar (sits between .messages and .input-area) */
1466
+ .agent-status {
1467
+ display: flex;
1468
+ align-items: center;
1469
+ gap: 10px;
1470
+ padding: 10px 24px;
1471
+ background: linear-gradient(
1472
+ 180deg,
1473
+ transparent 0%,
1474
+ var(--accent-glow) 100%
1475
+ );
1476
+ border-top: 1px solid var(--border);
1477
+ font-size: 13px;
1478
+ color: var(--text-secondary);
1467
1479
  font-family: 'JetBrains Mono', monospace;
1480
+ letter-spacing: 0.3px;
1481
+ animation: statusFadeIn 0.3s ease-out;
1482
+ min-height: 36px;
1468
1483
  }
1469
1484
 
1470
- .loading-dots span {
1471
- animation: blink 1.4s infinite;
1485
+ .agent-status[hidden] {
1486
+ display: none;
1472
1487
  }
1473
1488
 
1474
- .loading-dots span:nth-child(2) { animation-delay: 0.2s; }
1475
- .loading-dots span:nth-child(3) { animation-delay: 0.4s; }
1489
+ @keyframes statusFadeIn {
1490
+ from { opacity: 0; transform: translateY(4px); }
1491
+ to { opacity: 1; transform: translateY(0); }
1492
+ }
1476
1493
 
1477
- @keyframes blink {
1478
- 0%, 20% { opacity: 0; }
1479
- 40%, 100% { opacity: 1; }
1494
+ .agent-status-spinner {
1495
+ display: inline-block;
1496
+ width: 14px;
1497
+ height: 14px;
1498
+ border: 2px solid var(--border-light);
1499
+ border-top-color: var(--accent);
1500
+ border-right-color: var(--accent-hover);
1501
+ border-radius: 50%;
1502
+ animation: spin 0.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
1503
+ will-change: transform;
1504
+ flex-shrink: 0;
1505
+ }
1506
+
1507
+ .agent-status-icon {
1508
+ font-size: 14px;
1509
+ flex-shrink: 0;
1510
+ /* 默认不显示, 仅在切换为"执行"状态时显示 */
1511
+ display: none;
1512
+ }
1513
+
1514
+ .agent-status[data-mode="executing"] .agent-status-spinner { display: none; }
1515
+ .agent-status[data-mode="executing"] .agent-status-icon { display: inline-block; }
1516
+ .agent-status[data-mode="executing"] .agent-status-icon { animation: pulseGlow 1.4s ease-in-out infinite; }
1517
+
1518
+ .agent-status-text {
1519
+ flex: 1;
1520
+ background: linear-gradient(
1521
+ 90deg,
1522
+ var(--text-secondary) 0%,
1523
+ var(--accent) 50%,
1524
+ var(--text-secondary) 100%
1525
+ );
1526
+ background-size: 200% 100%;
1527
+ -webkit-background-clip: text;
1528
+ background-clip: text;
1529
+ color: transparent;
1530
+ -webkit-text-fill-color: transparent;
1531
+ animation: textShimmer 2.4s linear infinite;
1532
+ }
1533
+
1534
+ @keyframes textShimmer {
1535
+ 0% { background-position: 200% 0; }
1536
+ 100% { background-position: -200% 0; }
1537
+ }
1538
+
1539
+ @keyframes pulseGlow {
1540
+ 0%, 100% { transform: scale(1); filter: drop-shadow(0 0 2px var(--accent-glow)); }
1541
+ 50% { transform: scale(1.15); filter: drop-shadow(0 0 6px var(--accent)); }
1542
+ }
1543
+
1544
+ @media (prefers-reduced-motion: reduce) {
1545
+ .agent-status-spinner,
1546
+ .agent-status-text,
1547
+ .agent-status-icon,
1548
+ .agent-status {
1549
+ animation: none;
1550
+ }
1551
+ .agent-status-text {
1552
+ color: var(--text-secondary);
1553
+ -webkit-text-fill-color: var(--text-secondary);
1554
+ }
1480
1555
  }
1481
1556
 
1482
1557
  /* Input Area */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",
package/src/web/client.js CHANGED
@@ -4,6 +4,8 @@ if (typeof marked === 'undefined') {
4
4
  }
5
5
 
6
6
  const messagesEl = document.getElementById('messages');
7
+ const agentStatusEl = document.getElementById('agent-status');
8
+ const agentStatusTextEl = document.getElementById('agent-status-text');
7
9
  const input = document.getElementById('input');
8
10
  const sendBtn = document.getElementById('send');
9
11
  const sidebar = document.getElementById('sidebar');
@@ -900,20 +902,45 @@ function addMessage(content, type, save = true, container) {
900
902
  msgContainer.scrollTop = msgContainer.scrollHeight;
901
903
  }
902
904
 
905
+ // Agent status bar — sits between the message list and the input box.
906
+ // Two visual states: "planning" (spinner) and "executing" (glowing icon).
907
+ // The text alternates to convey the action loop.
908
+ let agentStatusState = null; // 'planning' | 'executing' | null
909
+ let agentStatusTextIdx = 0;
910
+
911
+ const AGENT_STATUS_TEXTS = {
912
+ planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
913
+ executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
914
+ };
915
+
916
+ function setAgentStatus(state) {
917
+ if (!agentStatusEl || !agentStatusTextEl) return;
918
+ if (state === null) {
919
+ agentStatusEl.hidden = true;
920
+ agentStatusEl.removeAttribute('data-mode');
921
+ agentStatusState = null;
922
+ return;
923
+ }
924
+ agentStatusEl.hidden = false;
925
+ agentStatusEl.setAttribute('data-mode', state);
926
+ agentStatusState = state;
927
+ // 重排一下文本, 避免长时间停留过于单调
928
+ agentStatusTextIdx = (agentStatusTextIdx + 1) % AGENT_STATUS_TEXTS[state].length;
929
+ agentStatusTextEl.textContent = AGENT_STATUS_TEXTS[state][agentStatusTextIdx];
930
+ }
931
+
903
932
  function showTyping(container) {
904
- const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
905
933
  hideTyping();
906
- const div = document.createElement('div');
907
- div.className = 'message message-ai';
908
- div.id = 'typing';
909
- div.innerHTML = '<div class="typing"><div class="typing-spinner"></div><span class="typing-text">思考中...</span></div>';
910
- msgContainer.appendChild(div);
911
- msgContainer.scrollTop = msgContainer.scrollHeight;
934
+ // 兼容旧路径: container 参数保留但不再使用, status bar 是全局唯一的
935
+ void container;
936
+ setAgentStatus('planning');
912
937
  }
913
938
 
914
939
  function hideTyping() {
915
- const typing = document.getElementById('typing');
916
- if (typing) typing.remove();
940
+ setAgentStatus(null);
941
+ // 兜底: 旧版本的 #typing 元素可能还残留在 DOM 里, 顺手清掉
942
+ const old = document.getElementById('typing');
943
+ if (old) old.remove();
917
944
  hideStreaming();
918
945
  }
919
946
 
@@ -1346,8 +1373,10 @@ function connect(channelId) {
1346
1373
  showUserCommand(data.content, container);
1347
1374
  } else if (data.type === 'ai') {
1348
1375
  addMessage(data.content, 'ai', true, container);
1376
+ hideTyping();
1349
1377
  } else if (data.type === 'stream') {
1350
1378
  handleStreamEvent(data, container);
1379
+ setAgentStatus('executing');
1351
1380
  } else if (data.type === 'regenerating') {
1352
1381
  const messages = container.querySelectorAll('.message-ai');
1353
1382
  if (messages.length > 0) {
@@ -1357,6 +1386,7 @@ function connect(channelId) {
1357
1386
  showTyping(container);
1358
1387
  } else if (data.type === 'status') {
1359
1388
  handleStatusEvent(data, container);
1389
+ setAgentStatus('executing');
1360
1390
  } else if (data.type === 'done') {
1361
1391
  hideTyping();
1362
1392
  // AI 回复完, 把最后一条 ai 消息落盘 (兜底, 避免 server saveSession 漏写)
@@ -261,12 +261,12 @@
261
261
  </div>
262
262
  </div>
263
263
 
264
- <div class="messages" id="messages">
265
- <div class="message message-ai">
266
- <div class="bubble bubble-ai">
267
- <span class="loading-dots">加载中<span>.</span><span>.</span><span>.</span></span>
268
- </div>
269
- </div>
264
+ <div class="messages" id="messages"></div>
265
+
266
+ <div class="agent-status" id="agent-status" hidden aria-live="polite">
267
+ <span class="agent-status-spinner" aria-hidden="true"></span>
268
+ <span class="agent-status-icon" aria-hidden="true"></span>
269
+ <span class="agent-status-text" id="agent-status-text">正在计划下一步</span>
270
270
  </div>
271
271
 
272
272
  <div class="input-area">
package/src/web/style.css CHANGED
@@ -1462,21 +1462,96 @@ body {
1462
1462
  50% { opacity: 1; transform: scale(1.2); }
1463
1463
  }
1464
1464
 
1465
- /* Loading Dots */
1466
- .loading-dots {
1465
+ /* Agent status bar (sits between .messages and .input-area) */
1466
+ .agent-status {
1467
+ display: flex;
1468
+ align-items: center;
1469
+ gap: 10px;
1470
+ padding: 10px 24px;
1471
+ background: linear-gradient(
1472
+ 180deg,
1473
+ transparent 0%,
1474
+ var(--accent-glow) 100%
1475
+ );
1476
+ border-top: 1px solid var(--border);
1477
+ font-size: 13px;
1478
+ color: var(--text-secondary);
1467
1479
  font-family: 'JetBrains Mono', monospace;
1480
+ letter-spacing: 0.3px;
1481
+ animation: statusFadeIn 0.3s ease-out;
1482
+ min-height: 36px;
1468
1483
  }
1469
1484
 
1470
- .loading-dots span {
1471
- animation: blink 1.4s infinite;
1485
+ .agent-status[hidden] {
1486
+ display: none;
1472
1487
  }
1473
1488
 
1474
- .loading-dots span:nth-child(2) { animation-delay: 0.2s; }
1475
- .loading-dots span:nth-child(3) { animation-delay: 0.4s; }
1489
+ @keyframes statusFadeIn {
1490
+ from { opacity: 0; transform: translateY(4px); }
1491
+ to { opacity: 1; transform: translateY(0); }
1492
+ }
1476
1493
 
1477
- @keyframes blink {
1478
- 0%, 20% { opacity: 0; }
1479
- 40%, 100% { opacity: 1; }
1494
+ .agent-status-spinner {
1495
+ display: inline-block;
1496
+ width: 14px;
1497
+ height: 14px;
1498
+ border: 2px solid var(--border-light);
1499
+ border-top-color: var(--accent);
1500
+ border-right-color: var(--accent-hover);
1501
+ border-radius: 50%;
1502
+ animation: spin 0.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
1503
+ will-change: transform;
1504
+ flex-shrink: 0;
1505
+ }
1506
+
1507
+ .agent-status-icon {
1508
+ font-size: 14px;
1509
+ flex-shrink: 0;
1510
+ /* 默认不显示, 仅在切换为"执行"状态时显示 */
1511
+ display: none;
1512
+ }
1513
+
1514
+ .agent-status[data-mode="executing"] .agent-status-spinner { display: none; }
1515
+ .agent-status[data-mode="executing"] .agent-status-icon { display: inline-block; }
1516
+ .agent-status[data-mode="executing"] .agent-status-icon { animation: pulseGlow 1.4s ease-in-out infinite; }
1517
+
1518
+ .agent-status-text {
1519
+ flex: 1;
1520
+ background: linear-gradient(
1521
+ 90deg,
1522
+ var(--text-secondary) 0%,
1523
+ var(--accent) 50%,
1524
+ var(--text-secondary) 100%
1525
+ );
1526
+ background-size: 200% 100%;
1527
+ -webkit-background-clip: text;
1528
+ background-clip: text;
1529
+ color: transparent;
1530
+ -webkit-text-fill-color: transparent;
1531
+ animation: textShimmer 2.4s linear infinite;
1532
+ }
1533
+
1534
+ @keyframes textShimmer {
1535
+ 0% { background-position: 200% 0; }
1536
+ 100% { background-position: -200% 0; }
1537
+ }
1538
+
1539
+ @keyframes pulseGlow {
1540
+ 0%, 100% { transform: scale(1); filter: drop-shadow(0 0 2px var(--accent-glow)); }
1541
+ 50% { transform: scale(1.15); filter: drop-shadow(0 0 6px var(--accent)); }
1542
+ }
1543
+
1544
+ @media (prefers-reduced-motion: reduce) {
1545
+ .agent-status-spinner,
1546
+ .agent-status-text,
1547
+ .agent-status-icon,
1548
+ .agent-status {
1549
+ animation: none;
1550
+ }
1551
+ .agent-status-text {
1552
+ color: var(--text-secondary);
1553
+ -webkit-text-fill-color: var(--text-secondary);
1554
+ }
1480
1555
  }
1481
1556
 
1482
1557
  /* Input Area */