@bolloon/bolloon-agent 0.1.19 → 0.1.21
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.
- package/dist/web/client.js +66 -19
- package/dist/web/index.html +6 -6
- package/dist/web/style.css +121 -36
- package/package.json +1 -1
- package/src/web/client.js +66 -19
- package/src/web/index.html +6 -6
- package/src/web/style.css +121 -36
package/dist/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');
|
|
@@ -465,20 +467,13 @@ function renderChannels() {
|
|
|
465
467
|
</svg>
|
|
466
468
|
</button>
|
|
467
469
|
<button class="channel-delete" title="删除智能体">×</button>
|
|
468
|
-
<button class="agent-new-session" title="新建会话">
|
|
469
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
470
|
-
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
471
|
-
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
472
|
-
</svg>
|
|
473
|
-
</button>
|
|
474
470
|
</span>
|
|
475
471
|
`;
|
|
476
472
|
|
|
477
473
|
// 行点击:切换展开;点击名字/图标区域则切到该智能体
|
|
478
474
|
row.addEventListener('click', (ev) => {
|
|
479
|
-
//
|
|
475
|
+
// 如果点在删除/配置按钮上, 单独处理
|
|
480
476
|
if (ev.target.closest('.channel-delete')
|
|
481
|
-
|| ev.target.closest('.agent-new-session')
|
|
482
477
|
|| ev.target.closest('.agent-config-btn')) return;
|
|
483
478
|
if (ev.target.closest('.agent-caret')) {
|
|
484
479
|
toggleAgentExpand(ch.id, ev);
|
|
@@ -493,8 +488,6 @@ function renderChannels() {
|
|
|
493
488
|
|
|
494
489
|
// 智能体删除
|
|
495
490
|
row.querySelector('.channel-delete').addEventListener('click', (ev) => deleteChannel(ch.id, ev));
|
|
496
|
-
// 新会话按钮
|
|
497
|
-
row.querySelector('.agent-new-session').addEventListener('click', (ev) => createNewSessionForChannel(ch.id, ev));
|
|
498
491
|
// 配置按钮: 打开同一个 modal 编辑已有智能体
|
|
499
492
|
row.querySelector('.agent-config-btn').addEventListener('click', (ev) => {
|
|
500
493
|
ev.stopPropagation();
|
|
@@ -507,6 +500,32 @@ function renderChannels() {
|
|
|
507
500
|
const sessionUl = document.createElement('ul');
|
|
508
501
|
sessionUl.className = 'session-list';
|
|
509
502
|
if (isExpanded) {
|
|
503
|
+
// "新建会话" 按钮 — 放在 session 列表最前面, 始终可见
|
|
504
|
+
const newSessLi = document.createElement('li');
|
|
505
|
+
newSessLi.className = 'session-new-item';
|
|
506
|
+
newSessLi.setAttribute('role', 'button');
|
|
507
|
+
newSessLi.setAttribute('tabindex', '0');
|
|
508
|
+
newSessLi.title = '新建会话';
|
|
509
|
+
newSessLi.innerHTML = `
|
|
510
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
511
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
512
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
513
|
+
</svg>
|
|
514
|
+
<span>新建会话</span>
|
|
515
|
+
`;
|
|
516
|
+
const onNewSession = (ev) => {
|
|
517
|
+
ev.stopPropagation();
|
|
518
|
+
createNewSessionForChannel(ch.id, ev);
|
|
519
|
+
};
|
|
520
|
+
newSessLi.addEventListener('click', onNewSession);
|
|
521
|
+
newSessLi.addEventListener('keydown', (ev) => {
|
|
522
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
523
|
+
ev.preventDefault();
|
|
524
|
+
onNewSession(ev);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
sessionUl.appendChild(newSessLi);
|
|
528
|
+
|
|
510
529
|
const sessions = Array.isArray(ch.sessions) ? ch.sessions : [];
|
|
511
530
|
sessions.forEach(sess => {
|
|
512
531
|
const sessLi = document.createElement('li');
|
|
@@ -900,20 +919,45 @@ function addMessage(content, type, save = true, container) {
|
|
|
900
919
|
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
901
920
|
}
|
|
902
921
|
|
|
922
|
+
// Agent status bar — sits between the message list and the input box.
|
|
923
|
+
// Two visual states: "planning" (spinner) and "executing" (glowing icon).
|
|
924
|
+
// The text alternates to convey the action loop.
|
|
925
|
+
let agentStatusState = null; // 'planning' | 'executing' | null
|
|
926
|
+
let agentStatusTextIdx = 0;
|
|
927
|
+
|
|
928
|
+
const AGENT_STATUS_TEXTS = {
|
|
929
|
+
planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
|
|
930
|
+
executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
function setAgentStatus(state) {
|
|
934
|
+
if (!agentStatusEl || !agentStatusTextEl) return;
|
|
935
|
+
if (state === null) {
|
|
936
|
+
agentStatusEl.hidden = true;
|
|
937
|
+
agentStatusEl.removeAttribute('data-mode');
|
|
938
|
+
agentStatusState = null;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
agentStatusEl.hidden = false;
|
|
942
|
+
agentStatusEl.setAttribute('data-mode', state);
|
|
943
|
+
agentStatusState = state;
|
|
944
|
+
// 重排一下文本, 避免长时间停留过于单调
|
|
945
|
+
agentStatusTextIdx = (agentStatusTextIdx + 1) % AGENT_STATUS_TEXTS[state].length;
|
|
946
|
+
agentStatusTextEl.textContent = AGENT_STATUS_TEXTS[state][agentStatusTextIdx];
|
|
947
|
+
}
|
|
948
|
+
|
|
903
949
|
function showTyping(container) {
|
|
904
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
905
950
|
hideTyping();
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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;
|
|
951
|
+
// 兼容旧路径: container 参数保留但不再使用, status bar 是全局唯一的
|
|
952
|
+
void container;
|
|
953
|
+
setAgentStatus('planning');
|
|
912
954
|
}
|
|
913
955
|
|
|
914
956
|
function hideTyping() {
|
|
915
|
-
|
|
916
|
-
|
|
957
|
+
setAgentStatus(null);
|
|
958
|
+
// 兜底: 旧版本的 #typing 元素可能还残留在 DOM 里, 顺手清掉
|
|
959
|
+
const old = document.getElementById('typing');
|
|
960
|
+
if (old) old.remove();
|
|
917
961
|
hideStreaming();
|
|
918
962
|
}
|
|
919
963
|
|
|
@@ -1346,8 +1390,10 @@ function connect(channelId) {
|
|
|
1346
1390
|
showUserCommand(data.content, container);
|
|
1347
1391
|
} else if (data.type === 'ai') {
|
|
1348
1392
|
addMessage(data.content, 'ai', true, container);
|
|
1393
|
+
hideTyping();
|
|
1349
1394
|
} else if (data.type === 'stream') {
|
|
1350
1395
|
handleStreamEvent(data, container);
|
|
1396
|
+
setAgentStatus('executing');
|
|
1351
1397
|
} else if (data.type === 'regenerating') {
|
|
1352
1398
|
const messages = container.querySelectorAll('.message-ai');
|
|
1353
1399
|
if (messages.length > 0) {
|
|
@@ -1357,6 +1403,7 @@ function connect(channelId) {
|
|
|
1357
1403
|
showTyping(container);
|
|
1358
1404
|
} else if (data.type === 'status') {
|
|
1359
1405
|
handleStatusEvent(data, container);
|
|
1406
|
+
setAgentStatus('executing');
|
|
1360
1407
|
} else if (data.type === 'done') {
|
|
1361
1408
|
hideTyping();
|
|
1362
1409
|
// AI 回复完, 把最后一条 ai 消息落盘 (兜底, 避免 server saveSession 漏写)
|
package/dist/web/index.html
CHANGED
|
@@ -261,12 +261,12 @@
|
|
|
261
261
|
</div>
|
|
262
262
|
</div>
|
|
263
263
|
|
|
264
|
-
<div class="messages" id="messages">
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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/dist/web/style.css
CHANGED
|
@@ -576,33 +576,8 @@ body {
|
|
|
576
576
|
}
|
|
577
577
|
|
|
578
578
|
.agent-new-session {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
border-radius: 5px;
|
|
582
|
-
background: transparent;
|
|
583
|
-
border: none;
|
|
584
|
-
color: var(--text-muted);
|
|
585
|
-
cursor: pointer;
|
|
586
|
-
display: flex;
|
|
587
|
-
align-items: center;
|
|
588
|
-
justify-content: center;
|
|
589
|
-
opacity: 0;
|
|
590
|
-
transition: var(--transition);
|
|
591
|
-
flex-shrink: 0;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
.agent-row:hover .agent-new-session {
|
|
595
|
-
opacity: 1;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
.agent-new-session:hover {
|
|
599
|
-
background: var(--accent);
|
|
600
|
-
color: var(--bg);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
.agent-new-session svg {
|
|
604
|
-
width: 14px;
|
|
605
|
-
height: 14px;
|
|
579
|
+
/* 已废弃: 新建会话按钮迁移到 session 列表顶部, 见 .session-new-item */
|
|
580
|
+
display: none;
|
|
606
581
|
}
|
|
607
582
|
|
|
608
583
|
/* Session list nested under each agent */
|
|
@@ -662,6 +637,41 @@ body {
|
|
|
662
637
|
font-weight: 500;
|
|
663
638
|
}
|
|
664
639
|
|
|
640
|
+
/* "新建会话" 入口, 固定在 session 列表最前面, 始终可见 */
|
|
641
|
+
.session-new-item {
|
|
642
|
+
display: flex;
|
|
643
|
+
align-items: center;
|
|
644
|
+
gap: 8px;
|
|
645
|
+
padding: 6px 10px;
|
|
646
|
+
margin-bottom: 2px;
|
|
647
|
+
border-radius: 5px;
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
font-size: 12px;
|
|
650
|
+
color: var(--text-secondary);
|
|
651
|
+
background: transparent;
|
|
652
|
+
border: 1px dashed var(--border-light);
|
|
653
|
+
transition: var(--transition);
|
|
654
|
+
user-select: none;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.session-new-item:hover {
|
|
658
|
+
background: var(--accent);
|
|
659
|
+
color: var(--bg);
|
|
660
|
+
border-color: var(--accent);
|
|
661
|
+
border-style: solid;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.session-new-item:focus-visible {
|
|
665
|
+
outline: 2px solid var(--accent);
|
|
666
|
+
outline-offset: 1px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.session-new-item svg {
|
|
670
|
+
width: 12px;
|
|
671
|
+
height: 12px;
|
|
672
|
+
flex-shrink: 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
665
675
|
.session-name {
|
|
666
676
|
flex: 1;
|
|
667
677
|
font-size: 13px;
|
|
@@ -1462,21 +1472,96 @@ body {
|
|
|
1462
1472
|
50% { opacity: 1; transform: scale(1.2); }
|
|
1463
1473
|
}
|
|
1464
1474
|
|
|
1465
|
-
/*
|
|
1466
|
-
.
|
|
1475
|
+
/* Agent status bar (sits between .messages and .input-area) */
|
|
1476
|
+
.agent-status {
|
|
1477
|
+
display: flex;
|
|
1478
|
+
align-items: center;
|
|
1479
|
+
gap: 10px;
|
|
1480
|
+
padding: 10px 24px;
|
|
1481
|
+
background: linear-gradient(
|
|
1482
|
+
180deg,
|
|
1483
|
+
transparent 0%,
|
|
1484
|
+
var(--accent-glow) 100%
|
|
1485
|
+
);
|
|
1486
|
+
border-top: 1px solid var(--border);
|
|
1487
|
+
font-size: 13px;
|
|
1488
|
+
color: var(--text-secondary);
|
|
1467
1489
|
font-family: 'JetBrains Mono', monospace;
|
|
1490
|
+
letter-spacing: 0.3px;
|
|
1491
|
+
animation: statusFadeIn 0.3s ease-out;
|
|
1492
|
+
min-height: 36px;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.agent-status[hidden] {
|
|
1496
|
+
display: none;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
@keyframes statusFadeIn {
|
|
1500
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
1501
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.agent-status-spinner {
|
|
1505
|
+
display: inline-block;
|
|
1506
|
+
width: 14px;
|
|
1507
|
+
height: 14px;
|
|
1508
|
+
border: 2px solid var(--border-light);
|
|
1509
|
+
border-top-color: var(--accent);
|
|
1510
|
+
border-right-color: var(--accent-hover);
|
|
1511
|
+
border-radius: 50%;
|
|
1512
|
+
animation: spin 0.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
1513
|
+
will-change: transform;
|
|
1514
|
+
flex-shrink: 0;
|
|
1468
1515
|
}
|
|
1469
1516
|
|
|
1470
|
-
.
|
|
1471
|
-
|
|
1517
|
+
.agent-status-icon {
|
|
1518
|
+
font-size: 14px;
|
|
1519
|
+
flex-shrink: 0;
|
|
1520
|
+
/* 默认不显示, 仅在切换为"执行"状态时显示 */
|
|
1521
|
+
display: none;
|
|
1472
1522
|
}
|
|
1473
1523
|
|
|
1474
|
-
.
|
|
1475
|
-
.
|
|
1524
|
+
.agent-status[data-mode="executing"] .agent-status-spinner { display: none; }
|
|
1525
|
+
.agent-status[data-mode="executing"] .agent-status-icon { display: inline-block; }
|
|
1526
|
+
.agent-status[data-mode="executing"] .agent-status-icon { animation: pulseGlow 1.4s ease-in-out infinite; }
|
|
1476
1527
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1528
|
+
.agent-status-text {
|
|
1529
|
+
flex: 1;
|
|
1530
|
+
background: linear-gradient(
|
|
1531
|
+
90deg,
|
|
1532
|
+
var(--text-secondary) 0%,
|
|
1533
|
+
var(--accent) 50%,
|
|
1534
|
+
var(--text-secondary) 100%
|
|
1535
|
+
);
|
|
1536
|
+
background-size: 200% 100%;
|
|
1537
|
+
-webkit-background-clip: text;
|
|
1538
|
+
background-clip: text;
|
|
1539
|
+
color: transparent;
|
|
1540
|
+
-webkit-text-fill-color: transparent;
|
|
1541
|
+
animation: textShimmer 2.4s linear infinite;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
@keyframes textShimmer {
|
|
1545
|
+
0% { background-position: 200% 0; }
|
|
1546
|
+
100% { background-position: -200% 0; }
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
@keyframes pulseGlow {
|
|
1550
|
+
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 2px var(--accent-glow)); }
|
|
1551
|
+
50% { transform: scale(1.15); filter: drop-shadow(0 0 6px var(--accent)); }
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1555
|
+
.agent-status-spinner,
|
|
1556
|
+
.agent-status-text,
|
|
1557
|
+
.agent-status-icon,
|
|
1558
|
+
.agent-status {
|
|
1559
|
+
animation: none;
|
|
1560
|
+
}
|
|
1561
|
+
.agent-status-text {
|
|
1562
|
+
color: var(--text-secondary);
|
|
1563
|
+
-webkit-text-fill-color: var(--text-secondary);
|
|
1564
|
+
}
|
|
1480
1565
|
}
|
|
1481
1566
|
|
|
1482
1567
|
/* Input Area */
|
package/package.json
CHANGED
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');
|
|
@@ -465,20 +467,13 @@ function renderChannels() {
|
|
|
465
467
|
</svg>
|
|
466
468
|
</button>
|
|
467
469
|
<button class="channel-delete" title="删除智能体">×</button>
|
|
468
|
-
<button class="agent-new-session" title="新建会话">
|
|
469
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
470
|
-
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
471
|
-
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
472
|
-
</svg>
|
|
473
|
-
</button>
|
|
474
470
|
</span>
|
|
475
471
|
`;
|
|
476
472
|
|
|
477
473
|
// 行点击:切换展开;点击名字/图标区域则切到该智能体
|
|
478
474
|
row.addEventListener('click', (ev) => {
|
|
479
|
-
//
|
|
475
|
+
// 如果点在删除/配置按钮上, 单独处理
|
|
480
476
|
if (ev.target.closest('.channel-delete')
|
|
481
|
-
|| ev.target.closest('.agent-new-session')
|
|
482
477
|
|| ev.target.closest('.agent-config-btn')) return;
|
|
483
478
|
if (ev.target.closest('.agent-caret')) {
|
|
484
479
|
toggleAgentExpand(ch.id, ev);
|
|
@@ -493,8 +488,6 @@ function renderChannels() {
|
|
|
493
488
|
|
|
494
489
|
// 智能体删除
|
|
495
490
|
row.querySelector('.channel-delete').addEventListener('click', (ev) => deleteChannel(ch.id, ev));
|
|
496
|
-
// 新会话按钮
|
|
497
|
-
row.querySelector('.agent-new-session').addEventListener('click', (ev) => createNewSessionForChannel(ch.id, ev));
|
|
498
491
|
// 配置按钮: 打开同一个 modal 编辑已有智能体
|
|
499
492
|
row.querySelector('.agent-config-btn').addEventListener('click', (ev) => {
|
|
500
493
|
ev.stopPropagation();
|
|
@@ -507,6 +500,32 @@ function renderChannels() {
|
|
|
507
500
|
const sessionUl = document.createElement('ul');
|
|
508
501
|
sessionUl.className = 'session-list';
|
|
509
502
|
if (isExpanded) {
|
|
503
|
+
// "新建会话" 按钮 — 放在 session 列表最前面, 始终可见
|
|
504
|
+
const newSessLi = document.createElement('li');
|
|
505
|
+
newSessLi.className = 'session-new-item';
|
|
506
|
+
newSessLi.setAttribute('role', 'button');
|
|
507
|
+
newSessLi.setAttribute('tabindex', '0');
|
|
508
|
+
newSessLi.title = '新建会话';
|
|
509
|
+
newSessLi.innerHTML = `
|
|
510
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
511
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
512
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
513
|
+
</svg>
|
|
514
|
+
<span>新建会话</span>
|
|
515
|
+
`;
|
|
516
|
+
const onNewSession = (ev) => {
|
|
517
|
+
ev.stopPropagation();
|
|
518
|
+
createNewSessionForChannel(ch.id, ev);
|
|
519
|
+
};
|
|
520
|
+
newSessLi.addEventListener('click', onNewSession);
|
|
521
|
+
newSessLi.addEventListener('keydown', (ev) => {
|
|
522
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
523
|
+
ev.preventDefault();
|
|
524
|
+
onNewSession(ev);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
sessionUl.appendChild(newSessLi);
|
|
528
|
+
|
|
510
529
|
const sessions = Array.isArray(ch.sessions) ? ch.sessions : [];
|
|
511
530
|
sessions.forEach(sess => {
|
|
512
531
|
const sessLi = document.createElement('li');
|
|
@@ -900,20 +919,45 @@ function addMessage(content, type, save = true, container) {
|
|
|
900
919
|
msgContainer.scrollTop = msgContainer.scrollHeight;
|
|
901
920
|
}
|
|
902
921
|
|
|
922
|
+
// Agent status bar — sits between the message list and the input box.
|
|
923
|
+
// Two visual states: "planning" (spinner) and "executing" (glowing icon).
|
|
924
|
+
// The text alternates to convey the action loop.
|
|
925
|
+
let agentStatusState = null; // 'planning' | 'executing' | null
|
|
926
|
+
let agentStatusTextIdx = 0;
|
|
927
|
+
|
|
928
|
+
const AGENT_STATUS_TEXTS = {
|
|
929
|
+
planning: ['正在计划下一步行动', '正在规划任务路径', '正在分析当前状态'],
|
|
930
|
+
executing: ['正在执行下一步行动', '正在执行任务', '正在调用工具'],
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
function setAgentStatus(state) {
|
|
934
|
+
if (!agentStatusEl || !agentStatusTextEl) return;
|
|
935
|
+
if (state === null) {
|
|
936
|
+
agentStatusEl.hidden = true;
|
|
937
|
+
agentStatusEl.removeAttribute('data-mode');
|
|
938
|
+
agentStatusState = null;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
agentStatusEl.hidden = false;
|
|
942
|
+
agentStatusEl.setAttribute('data-mode', state);
|
|
943
|
+
agentStatusState = state;
|
|
944
|
+
// 重排一下文本, 避免长时间停留过于单调
|
|
945
|
+
agentStatusTextIdx = (agentStatusTextIdx + 1) % AGENT_STATUS_TEXTS[state].length;
|
|
946
|
+
agentStatusTextEl.textContent = AGENT_STATUS_TEXTS[state][agentStatusTextIdx];
|
|
947
|
+
}
|
|
948
|
+
|
|
903
949
|
function showTyping(container) {
|
|
904
|
-
const msgContainer = container || messagesContainers.get(currentChannelId) || messagesEl;
|
|
905
950
|
hideTyping();
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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;
|
|
951
|
+
// 兼容旧路径: container 参数保留但不再使用, status bar 是全局唯一的
|
|
952
|
+
void container;
|
|
953
|
+
setAgentStatus('planning');
|
|
912
954
|
}
|
|
913
955
|
|
|
914
956
|
function hideTyping() {
|
|
915
|
-
|
|
916
|
-
|
|
957
|
+
setAgentStatus(null);
|
|
958
|
+
// 兜底: 旧版本的 #typing 元素可能还残留在 DOM 里, 顺手清掉
|
|
959
|
+
const old = document.getElementById('typing');
|
|
960
|
+
if (old) old.remove();
|
|
917
961
|
hideStreaming();
|
|
918
962
|
}
|
|
919
963
|
|
|
@@ -1346,8 +1390,10 @@ function connect(channelId) {
|
|
|
1346
1390
|
showUserCommand(data.content, container);
|
|
1347
1391
|
} else if (data.type === 'ai') {
|
|
1348
1392
|
addMessage(data.content, 'ai', true, container);
|
|
1393
|
+
hideTyping();
|
|
1349
1394
|
} else if (data.type === 'stream') {
|
|
1350
1395
|
handleStreamEvent(data, container);
|
|
1396
|
+
setAgentStatus('executing');
|
|
1351
1397
|
} else if (data.type === 'regenerating') {
|
|
1352
1398
|
const messages = container.querySelectorAll('.message-ai');
|
|
1353
1399
|
if (messages.length > 0) {
|
|
@@ -1357,6 +1403,7 @@ function connect(channelId) {
|
|
|
1357
1403
|
showTyping(container);
|
|
1358
1404
|
} else if (data.type === 'status') {
|
|
1359
1405
|
handleStatusEvent(data, container);
|
|
1406
|
+
setAgentStatus('executing');
|
|
1360
1407
|
} else if (data.type === 'done') {
|
|
1361
1408
|
hideTyping();
|
|
1362
1409
|
// AI 回复完, 把最后一条 ai 消息落盘 (兜底, 避免 server saveSession 漏写)
|
package/src/web/index.html
CHANGED
|
@@ -261,12 +261,12 @@
|
|
|
261
261
|
</div>
|
|
262
262
|
</div>
|
|
263
263
|
|
|
264
|
-
<div class="messages" id="messages">
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
@@ -576,33 +576,8 @@ body {
|
|
|
576
576
|
}
|
|
577
577
|
|
|
578
578
|
.agent-new-session {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
border-radius: 5px;
|
|
582
|
-
background: transparent;
|
|
583
|
-
border: none;
|
|
584
|
-
color: var(--text-muted);
|
|
585
|
-
cursor: pointer;
|
|
586
|
-
display: flex;
|
|
587
|
-
align-items: center;
|
|
588
|
-
justify-content: center;
|
|
589
|
-
opacity: 0;
|
|
590
|
-
transition: var(--transition);
|
|
591
|
-
flex-shrink: 0;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
.agent-row:hover .agent-new-session {
|
|
595
|
-
opacity: 1;
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
.agent-new-session:hover {
|
|
599
|
-
background: var(--accent);
|
|
600
|
-
color: var(--bg);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
.agent-new-session svg {
|
|
604
|
-
width: 14px;
|
|
605
|
-
height: 14px;
|
|
579
|
+
/* 已废弃: 新建会话按钮迁移到 session 列表顶部, 见 .session-new-item */
|
|
580
|
+
display: none;
|
|
606
581
|
}
|
|
607
582
|
|
|
608
583
|
/* Session list nested under each agent */
|
|
@@ -662,6 +637,41 @@ body {
|
|
|
662
637
|
font-weight: 500;
|
|
663
638
|
}
|
|
664
639
|
|
|
640
|
+
/* "新建会话" 入口, 固定在 session 列表最前面, 始终可见 */
|
|
641
|
+
.session-new-item {
|
|
642
|
+
display: flex;
|
|
643
|
+
align-items: center;
|
|
644
|
+
gap: 8px;
|
|
645
|
+
padding: 6px 10px;
|
|
646
|
+
margin-bottom: 2px;
|
|
647
|
+
border-radius: 5px;
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
font-size: 12px;
|
|
650
|
+
color: var(--text-secondary);
|
|
651
|
+
background: transparent;
|
|
652
|
+
border: 1px dashed var(--border-light);
|
|
653
|
+
transition: var(--transition);
|
|
654
|
+
user-select: none;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.session-new-item:hover {
|
|
658
|
+
background: var(--accent);
|
|
659
|
+
color: var(--bg);
|
|
660
|
+
border-color: var(--accent);
|
|
661
|
+
border-style: solid;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.session-new-item:focus-visible {
|
|
665
|
+
outline: 2px solid var(--accent);
|
|
666
|
+
outline-offset: 1px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.session-new-item svg {
|
|
670
|
+
width: 12px;
|
|
671
|
+
height: 12px;
|
|
672
|
+
flex-shrink: 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
665
675
|
.session-name {
|
|
666
676
|
flex: 1;
|
|
667
677
|
font-size: 13px;
|
|
@@ -1462,21 +1472,96 @@ body {
|
|
|
1462
1472
|
50% { opacity: 1; transform: scale(1.2); }
|
|
1463
1473
|
}
|
|
1464
1474
|
|
|
1465
|
-
/*
|
|
1466
|
-
.
|
|
1475
|
+
/* Agent status bar (sits between .messages and .input-area) */
|
|
1476
|
+
.agent-status {
|
|
1477
|
+
display: flex;
|
|
1478
|
+
align-items: center;
|
|
1479
|
+
gap: 10px;
|
|
1480
|
+
padding: 10px 24px;
|
|
1481
|
+
background: linear-gradient(
|
|
1482
|
+
180deg,
|
|
1483
|
+
transparent 0%,
|
|
1484
|
+
var(--accent-glow) 100%
|
|
1485
|
+
);
|
|
1486
|
+
border-top: 1px solid var(--border);
|
|
1487
|
+
font-size: 13px;
|
|
1488
|
+
color: var(--text-secondary);
|
|
1467
1489
|
font-family: 'JetBrains Mono', monospace;
|
|
1490
|
+
letter-spacing: 0.3px;
|
|
1491
|
+
animation: statusFadeIn 0.3s ease-out;
|
|
1492
|
+
min-height: 36px;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
.agent-status[hidden] {
|
|
1496
|
+
display: none;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
@keyframes statusFadeIn {
|
|
1500
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
1501
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.agent-status-spinner {
|
|
1505
|
+
display: inline-block;
|
|
1506
|
+
width: 14px;
|
|
1507
|
+
height: 14px;
|
|
1508
|
+
border: 2px solid var(--border-light);
|
|
1509
|
+
border-top-color: var(--accent);
|
|
1510
|
+
border-right-color: var(--accent-hover);
|
|
1511
|
+
border-radius: 50%;
|
|
1512
|
+
animation: spin 0.9s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
1513
|
+
will-change: transform;
|
|
1514
|
+
flex-shrink: 0;
|
|
1468
1515
|
}
|
|
1469
1516
|
|
|
1470
|
-
.
|
|
1471
|
-
|
|
1517
|
+
.agent-status-icon {
|
|
1518
|
+
font-size: 14px;
|
|
1519
|
+
flex-shrink: 0;
|
|
1520
|
+
/* 默认不显示, 仅在切换为"执行"状态时显示 */
|
|
1521
|
+
display: none;
|
|
1472
1522
|
}
|
|
1473
1523
|
|
|
1474
|
-
.
|
|
1475
|
-
.
|
|
1524
|
+
.agent-status[data-mode="executing"] .agent-status-spinner { display: none; }
|
|
1525
|
+
.agent-status[data-mode="executing"] .agent-status-icon { display: inline-block; }
|
|
1526
|
+
.agent-status[data-mode="executing"] .agent-status-icon { animation: pulseGlow 1.4s ease-in-out infinite; }
|
|
1476
1527
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1528
|
+
.agent-status-text {
|
|
1529
|
+
flex: 1;
|
|
1530
|
+
background: linear-gradient(
|
|
1531
|
+
90deg,
|
|
1532
|
+
var(--text-secondary) 0%,
|
|
1533
|
+
var(--accent) 50%,
|
|
1534
|
+
var(--text-secondary) 100%
|
|
1535
|
+
);
|
|
1536
|
+
background-size: 200% 100%;
|
|
1537
|
+
-webkit-background-clip: text;
|
|
1538
|
+
background-clip: text;
|
|
1539
|
+
color: transparent;
|
|
1540
|
+
-webkit-text-fill-color: transparent;
|
|
1541
|
+
animation: textShimmer 2.4s linear infinite;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
@keyframes textShimmer {
|
|
1545
|
+
0% { background-position: 200% 0; }
|
|
1546
|
+
100% { background-position: -200% 0; }
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
@keyframes pulseGlow {
|
|
1550
|
+
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 2px var(--accent-glow)); }
|
|
1551
|
+
50% { transform: scale(1.15); filter: drop-shadow(0 0 6px var(--accent)); }
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1555
|
+
.agent-status-spinner,
|
|
1556
|
+
.agent-status-text,
|
|
1557
|
+
.agent-status-icon,
|
|
1558
|
+
.agent-status {
|
|
1559
|
+
animation: none;
|
|
1560
|
+
}
|
|
1561
|
+
.agent-status-text {
|
|
1562
|
+
color: var(--text-secondary);
|
|
1563
|
+
-webkit-text-fill-color: var(--text-secondary);
|
|
1564
|
+
}
|
|
1480
1565
|
}
|
|
1481
1566
|
|
|
1482
1567
|
/* Input Area */
|