psyclaw 0.29.9 → 0.29.11

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.
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>PsyClaw Panel · 学术科研智能体控制台</title>
6
+ <title>PsyClaw Panel · 科研工作台</title>
7
7
  <style>
8
8
  :root {
9
9
  color-scheme: light;
@@ -60,6 +60,7 @@
60
60
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
61
61
  --font-mono: ui-monospace, SFMono-Regular, "Cascadia Code", Menlo, Monaco, Consolas, monospace;
62
62
  --font-serif: "Charter", "Bitstream Charter", Cambria, Georgia, serif;
63
+ --accent: var(--brand-primary);
63
64
  }
64
65
 
65
66
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -938,7 +939,7 @@
938
939
  background: var(--brand-primary-hover);
939
940
  }
940
941
 
941
- /* Interactive Web Console Drawer */
942
+ /* Chat drawer — light app chrome (not terminal) */
942
943
  .console-fab {
943
944
  position: fixed;
944
945
  bottom: 24px;
@@ -948,20 +949,20 @@
948
949
  align-items: center;
949
950
  gap: 8px;
950
951
  padding: 10px 18px;
951
- background: #0f172a;
952
- color: #f8fafc;
953
- border: 1px solid #334155;
954
- border-radius: 999px;
952
+ background: var(--bg-surface);
953
+ color: var(--brand-primary);
954
+ border: 1px solid var(--brand-primary-border);
955
+ border-radius: var(--radius-lg);
955
956
  font-size: 13px;
956
957
  font-weight: 600;
957
958
  cursor: pointer;
958
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
959
- transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
959
+ box-shadow: var(--shadow-md);
960
+ transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
960
961
  }
961
962
  .console-fab:hover {
962
- background: #1e293b;
963
- transform: translateY(-2px);
964
- box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
963
+ background: var(--brand-primary-light);
964
+ border-color: var(--brand-primary);
965
+ transform: translateY(-1px);
965
966
  }
966
967
  .console-drawer {
967
968
  position: fixed;
@@ -971,34 +972,44 @@
971
972
  max-width: calc(100vw - 40px);
972
973
  height: 560px;
973
974
  max-height: calc(100vh - 80px);
974
- background: #0f172a;
975
- color: #e2e8f0;
976
- border: 1px solid #334155;
975
+ background: var(--bg-surface);
976
+ color: var(--text-main);
977
+ border: 1px solid var(--border-subtle);
977
978
  border-bottom: 0;
978
- border-radius: 12px 12px 0 0;
979
- box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.4);
979
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
980
+ box-shadow: var(--shadow-md);
980
981
  display: flex;
981
982
  flex-direction: column;
982
983
  z-index: 1000;
983
984
  transform: translateY(105%);
984
985
  transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
986
+ font-family: var(--font-sans);
985
987
  }
986
988
  .console-drawer.open {
987
989
  transform: translateY(0);
988
990
  }
991
+ .console-drawer.embedded {
992
+ border: none;
993
+ border-radius: 0;
994
+ box-shadow: none;
995
+ transform: none;
996
+ }
989
997
  .console-header {
990
998
  display: flex;
991
999
  align-items: center;
992
1000
  justify-content: space-between;
993
1001
  padding: 12px 16px;
994
- border-bottom: 1px solid #1e293b;
995
- background: #0b1120;
996
- border-radius: 12px 12px 0 0;
1002
+ border-bottom: 1px solid var(--border-subtle);
1003
+ background: var(--bg-surface);
1004
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
1005
+ }
1006
+ .console-drawer.embedded .console-header {
1007
+ border-radius: 0;
997
1008
  }
998
1009
  .console-title {
999
1010
  font-size: 13px;
1000
1011
  font-weight: 650;
1001
- color: #f8fafc;
1012
+ color: var(--text-main);
1002
1013
  display: flex;
1003
1014
  align-items: center;
1004
1015
  gap: 8px;
@@ -1007,10 +1018,10 @@
1007
1018
  font-size: 10px;
1008
1019
  font-weight: 600;
1009
1020
  padding: 1px 6px;
1010
- border-radius: 999px;
1011
- background: #064e3b;
1012
- color: #34d399;
1013
- border: 1px solid #047857;
1021
+ border-radius: var(--radius-xs);
1022
+ background: var(--ok-bg);
1023
+ color: var(--ok-text);
1024
+ border: 1px solid var(--ok-border);
1014
1025
  }
1015
1026
  .console-actions {
1016
1027
  display: flex;
@@ -1020,103 +1031,126 @@
1020
1031
  .console-btn-icon {
1021
1032
  background: transparent;
1022
1033
  border: none;
1023
- color: #94a3b8;
1034
+ color: var(--text-muted);
1024
1035
  cursor: pointer;
1025
1036
  padding: 4px;
1026
- border-radius: 4px;
1037
+ border-radius: var(--radius-sm);
1027
1038
  display: flex;
1028
1039
  }
1029
1040
  .console-btn-icon:hover {
1030
- color: #f8fafc;
1031
- background: #1e293b;
1041
+ color: var(--text-main);
1042
+ background: var(--bg-surface-subtle);
1032
1043
  }
1033
1044
  .console-quick-chips {
1034
1045
  display: flex;
1035
1046
  align-items: center;
1036
1047
  gap: 6px;
1037
1048
  padding: 8px 14px;
1038
- background: #141e33;
1039
- border-bottom: 1px solid #1e293b;
1049
+ background: var(--bg-surface-subtle);
1050
+ border-bottom: 1px solid var(--border-subtle);
1040
1051
  overflow-x: auto;
1041
1052
  }
1042
1053
  .chip {
1043
1054
  font-family: var(--font-mono);
1044
1055
  font-size: 11px;
1045
- background: #1e293b;
1046
- color: #38bdf8;
1047
- border: 1px solid #334155;
1056
+ background: var(--bg-surface);
1057
+ color: var(--text-secondary);
1058
+ border: 1px solid var(--border-default);
1048
1059
  padding: 3px 8px;
1049
- border-radius: 4px;
1060
+ border-radius: var(--radius-sm);
1050
1061
  cursor: pointer;
1051
1062
  white-space: nowrap;
1052
- transition: all 0.12s ease;
1063
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
1053
1064
  }
1054
1065
  .chip:hover {
1055
- background: #334155;
1056
- color: #fff;
1066
+ background: var(--brand-primary-light);
1067
+ border-color: var(--brand-primary-border);
1068
+ color: var(--brand-primary);
1057
1069
  }
1058
1070
  .console-log {
1059
1071
  flex: 1;
1060
1072
  overflow-y: auto;
1061
- padding: 14px 16px;
1062
- font-family: var(--font-mono);
1063
- font-size: 12px;
1064
- line-height: 1.6;
1073
+ padding: 16px;
1074
+ font-family: var(--font-sans);
1075
+ font-size: 13px;
1076
+ line-height: 1.55;
1065
1077
  display: flex;
1066
1078
  flex-direction: column;
1067
1079
  gap: 12px;
1080
+ background: var(--bg-app);
1068
1081
  }
1069
1082
  .log-entry {
1070
1083
  display: flex;
1071
1084
  flex-direction: column;
1072
1085
  gap: 4px;
1086
+ max-width: 92%;
1087
+ }
1088
+ .log-entry.user {
1089
+ align-self: flex-end;
1090
+ align-items: flex-end;
1091
+ }
1092
+ .log-entry.agent,
1093
+ .log-entry.sys {
1094
+ align-self: flex-start;
1095
+ align-items: flex-start;
1073
1096
  }
1074
1097
  .log-user {
1075
- color: #38bdf8;
1076
- font-weight: 600;
1098
+ color: #fff;
1099
+ font-weight: 500;
1100
+ background: var(--brand-primary);
1101
+ padding: 10px 12px;
1102
+ border-radius: 12px 12px 4px 12px;
1103
+ white-space: pre-wrap;
1104
+ word-break: break-word;
1105
+ box-shadow: var(--shadow-xs);
1077
1106
  }
1078
1107
  .log-agent {
1079
- color: #e2e8f0;
1080
- background: #1e293b;
1108
+ color: var(--text-main);
1109
+ background: var(--bg-surface);
1081
1110
  padding: 10px 12px;
1082
- border-radius: 6px;
1083
- border-left: 3px solid #2dd4bf;
1111
+ border-radius: 12px 12px 12px 4px;
1112
+ border: 1px solid var(--border-subtle);
1084
1113
  white-space: pre-wrap;
1085
1114
  word-break: break-word;
1115
+ box-shadow: var(--shadow-xs);
1086
1116
  }
1087
1117
  .log-sys {
1088
- color: #94a3b8;
1089
- font-style: italic;
1090
- font-size: 11px;
1118
+ color: var(--text-muted);
1119
+ font-style: normal;
1120
+ font-size: 12px;
1121
+ background: transparent;
1122
+ padding: 2px 0;
1091
1123
  }
1092
1124
  .console-input-box {
1093
- padding: 12px;
1094
- background: #0b1120;
1095
- border-top: 1px solid #1e293b;
1125
+ padding: 12px 14px;
1126
+ background: var(--bg-surface);
1127
+ border-top: 1px solid var(--border-subtle);
1096
1128
  display: flex;
1097
1129
  gap: 8px;
1098
1130
  }
1099
1131
  .console-input {
1100
1132
  flex: 1;
1101
- background: #1e293b;
1102
- border: 1px solid #334155;
1103
- color: #f8fafc;
1104
- padding: 8px 12px;
1105
- font-size: 12.5px;
1106
- border-radius: 6px;
1133
+ background: var(--bg-surface-subtle);
1134
+ border: 1px solid var(--border-default);
1135
+ color: var(--text-main);
1136
+ padding: 10px 12px;
1137
+ font-size: 13px;
1138
+ border-radius: var(--radius-md);
1107
1139
  outline: none;
1108
- font-family: var(--font-mono);
1140
+ font-family: var(--font-sans);
1109
1141
  }
1110
1142
  .console-input:focus {
1111
- border-color: #38bdf8;
1143
+ border-color: var(--brand-primary);
1144
+ background: var(--bg-surface);
1145
+ box-shadow: 0 0 0 3px var(--brand-primary-border);
1112
1146
  }
1113
1147
  .console-send-btn {
1114
1148
  background: var(--brand-primary);
1115
1149
  color: #fff;
1116
1150
  border: none;
1117
1151
  padding: 8px 14px;
1118
- border-radius: 6px;
1119
- font-size: 12.5px;
1152
+ border-radius: var(--radius-md);
1153
+ font-size: 13px;
1120
1154
  font-weight: 600;
1121
1155
  cursor: pointer;
1122
1156
  display: flex;
@@ -1126,6 +1160,54 @@
1126
1160
  .console-send-btn:hover {
1127
1161
  background: var(--brand-primary-hover);
1128
1162
  }
1163
+ .log-agent.streaming::after {
1164
+ content: "▍";
1165
+ animation: blink 1s steps(1) infinite;
1166
+ color: var(--brand-primary);
1167
+ margin-left: 2px;
1168
+ }
1169
+ @keyframes blink { 50% { opacity: 0; } }
1170
+ .wake-option-list {
1171
+ display: grid;
1172
+ gap: 8px;
1173
+ max-height: 360px;
1174
+ overflow-y: auto;
1175
+ }
1176
+ .wake-option {
1177
+ display: flex;
1178
+ align-items: flex-start;
1179
+ gap: 10px;
1180
+ padding: 10px 12px;
1181
+ border: 1px solid var(--border-subtle);
1182
+ border-radius: var(--radius-md);
1183
+ background: var(--bg-surface);
1184
+ cursor: pointer;
1185
+ text-align: left;
1186
+ }
1187
+ .wake-option:hover {
1188
+ border-color: var(--brand-primary-border);
1189
+ background: var(--brand-primary-light);
1190
+ }
1191
+ .wake-option.selected {
1192
+ border-color: var(--brand-primary);
1193
+ background: var(--brand-primary-light);
1194
+ box-shadow: 0 0 0 2px var(--brand-primary-border);
1195
+ }
1196
+ .wake-option .wake-mark {
1197
+ font-family: var(--font-mono);
1198
+ color: var(--brand-primary);
1199
+ font-weight: 700;
1200
+ min-width: 1.5rem;
1201
+ }
1202
+ .wake-option .wake-label {
1203
+ font-weight: 600;
1204
+ color: var(--text-main);
1205
+ }
1206
+ .wake-option .wake-desc {
1207
+ font-size: 12px;
1208
+ color: var(--text-muted);
1209
+ margin-top: 2px;
1210
+ }
1129
1211
 
1130
1212
  /* Command Palette Modal */
1131
1213
  .modal-overlay {
@@ -1514,7 +1596,7 @@
1514
1596
  <div class="page-head">
1515
1597
  <div>
1516
1598
  <h1>交互对话</h1>
1517
- <p>整页对话视图(不再只依赖右下角抽屉)。打开本页时会把控制台嵌到此处。</p>
1599
+ <p>提问会转入当前 PsyClaw 会话;助手回复经 SSE 同步到此处。模型可调用「唤醒选项」弹窗。</p>
1518
1600
  </div>
1519
1601
  </div>
1520
1602
  <div id="chat-console-host" class="panel section" style="min-height:62vh;padding:0;overflow:hidden;display:flex;flex-direction:column"></div>
@@ -1523,22 +1605,22 @@
1523
1605
  </div>
1524
1606
  </div>
1525
1607
 
1526
- <!-- 悬浮控制台入口 Floating Action Button -->
1527
- <button id="console-fab" class="console-fab" title="打开/折叠 PsyClaw 交互控制台">
1528
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
1529
- <span>PsyClaw 控制台</span>
1608
+ <!-- 悬浮对话入口 -->
1609
+ <button id="console-fab" class="console-fab" title="打开/折叠对话">
1610
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
1611
+ <span>对话</span>
1530
1612
  </button>
1531
1613
 
1532
- <!-- 抽屉式交互控制台 Console Drawer -->
1614
+ <!-- 抽屉式对话 -->
1533
1615
  <div id="console-drawer" class="console-drawer">
1534
1616
  <div class="console-header">
1535
1617
  <div class="console-title">
1536
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:#38bdf8"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
1537
- <span>科研会话终端</span>
1538
- <span id="console-status-badge" class="console-status-tag">连线中</span>
1618
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--brand-primary)"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
1619
+ <span>对话</span>
1620
+ <span id="console-status-badge" class="console-status-tag">已连接</span>
1539
1621
  </div>
1540
1622
  <div class="console-actions">
1541
- <button id="console-clear-btn" class="console-btn-icon" title="清空终端历史">
1623
+ <button id="console-clear-btn" class="console-btn-icon" title="清空对话">
1542
1624
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
1543
1625
  </button>
1544
1626
  <button id="console-close-btn" class="console-btn-icon" title="最小化">
@@ -1547,7 +1629,6 @@
1547
1629
  </div>
1548
1630
  </div>
1549
1631
 
1550
- <!-- 快捷指令快速贴入栏 -->
1551
1632
  <div class="console-quick-chips">
1552
1633
  <span class="chip" data-cmd="/plan status">/plan status</span>
1553
1634
  <span class="chip" data-cmd="/verify list">/verify list</span>
@@ -1556,16 +1637,14 @@
1556
1637
  <span class="chip" data-cmd="/agents">/agents 角色</span>
1557
1638
  </div>
1558
1639
 
1559
- <!-- 终端流式日志输出区域 -->
1560
1640
  <div id="console-log" class="console-log">
1561
- <div class="log-entry">
1562
- <div class="log-sys">ψ PsyClaw 交互控制台已就绪。支持直接提问或发送斜杠指令 (/init, /plan, /grill, /review, /verify 等)。</div>
1641
+ <div class="log-entry sys">
1642
+ <div class="log-sys">对话已就绪(SSE)。提问转入当前会话,助手回复会流式同步到此处。支持 /init、/plan、/grill 等指令;模型可弹出「唤醒选项」。</div>
1563
1643
  </div>
1564
1644
  </div>
1565
1645
 
1566
- <!-- 输入与触发栏 -->
1567
1646
  <div class="console-input-box">
1568
- <input type="text" id="console-input" class="console-input" placeholder="输入科研指令或研究问题 (Enter 发送)..." autocomplete="off" />
1647
+ <input type="text" id="console-input" class="console-input" placeholder="输入研究问题或指令,Enter 发送…" autocomplete="off" />
1569
1648
  <button id="console-send-btn" class="console-send-btn">
1570
1649
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
1571
1650
  发送
@@ -1573,6 +1652,26 @@
1573
1652
  </div>
1574
1653
  </div>
1575
1654
 
1655
+ <!-- 唤醒选项弹窗 -->
1656
+ <div id="wake-modal" class="modal-overlay hidden">
1657
+ <div class="modal-box">
1658
+ <div class="modal-head">
1659
+ <h3 id="wake-modal-title">唤醒选项</h3>
1660
+ <button id="wake-modal-close" class="console-btn-icon" style="color:var(--text-muted)" title="取消">
1661
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
1662
+ </button>
1663
+ </div>
1664
+ <div class="modal-body">
1665
+ <p id="wake-modal-prompt" style="color:var(--text-secondary);margin:0"></p>
1666
+ <div id="wake-option-list" class="wake-option-list"></div>
1667
+ </div>
1668
+ <div class="modal-foot">
1669
+ <button id="wake-cancel-btn" class="button">取消</button>
1670
+ <button id="wake-submit-btn" class="button primary">确认</button>
1671
+ </div>
1672
+ </div>
1673
+ </div>
1674
+
1576
1675
  <!-- /init 项目向导模态框 -->
1577
1676
  <div id="init-modal" class="modal-overlay hidden">
1578
1677
  <div class="modal-box">
@@ -1681,7 +1780,7 @@ function mountConsoleInChat() {
1681
1780
  const drawer = $("console-drawer");
1682
1781
  if (!host || !drawer) return;
1683
1782
  host.appendChild(drawer);
1684
- drawer.classList.add("open");
1783
+ drawer.classList.add("open", "embedded");
1685
1784
  drawer.style.position = "relative";
1686
1785
  drawer.style.inset = "auto";
1687
1786
  drawer.style.width = "100%";
@@ -1689,12 +1788,14 @@ function mountConsoleInChat() {
1689
1788
  drawer.style.minHeight = "58vh";
1690
1789
  drawer.style.boxShadow = "none";
1691
1790
  drawer.style.borderRadius = "0";
1791
+ drawer.style.transform = "none";
1692
1792
  }
1693
1793
 
1694
1794
  function restoreConsoleDrawer() {
1695
1795
  const drawer = $("console-drawer");
1696
1796
  if (!drawer || drawer.parentElement === document.body) return;
1697
1797
  document.body.appendChild(drawer);
1798
+ drawer.classList.remove("embedded");
1698
1799
  drawer.style.position = "";
1699
1800
  drawer.style.inset = "";
1700
1801
  drawer.style.width = "";
@@ -1702,6 +1803,7 @@ function restoreConsoleDrawer() {
1702
1803
  drawer.style.minHeight = "";
1703
1804
  drawer.style.boxShadow = "";
1704
1805
  drawer.style.borderRadius = "";
1806
+ drawer.style.transform = "";
1705
1807
  }
1706
1808
 
1707
1809
  async function loadChecklist() {
@@ -2240,11 +2342,14 @@ document.querySelectorAll(".step-item").forEach(item => {
2240
2342
  });
2241
2343
  });
2242
2344
 
2243
- /* --- 交互控制台 Console Drawer 交互 --- */
2345
+ /* --- 交互对话 + SSE + 唤醒选项 --- */
2244
2346
  const consoleDrawer = $("console-drawer");
2245
2347
  const consoleFab = $("console-fab");
2246
2348
  const consoleLog = $("console-log");
2247
2349
  const consoleInput = $("console-input");
2350
+ const streamBubbles = new Map();
2351
+ let lastLocalUserText = "";
2352
+ let wakeState = null;
2248
2353
 
2249
2354
  function openConsole() {
2250
2355
  consoleDrawer.classList.add("open");
@@ -2262,31 +2367,192 @@ function openConsoleWithMessage(text) {
2262
2367
 
2263
2368
  function appendLog(role, text) {
2264
2369
  const entry = document.createElement("div");
2265
- entry.className = "log-entry";
2370
+ entry.className = `log-entry ${role}`;
2266
2371
  if (role === "user") {
2267
- entry.innerHTML = `<div class="log-user">❯ ${esc(text)}</div>`;
2372
+ entry.innerHTML = `<div class="log-user">${esc(text)}</div>`;
2268
2373
  } else if (role === "agent") {
2269
2374
  entry.innerHTML = `<div class="log-agent">${renderMarkdown(text)}</div>`;
2270
2375
  } else {
2271
- entry.innerHTML = `<div class="log-sys">ℹ ${esc(text)}</div>`;
2376
+ entry.innerHTML = `<div class="log-sys">${esc(text)}</div>`;
2272
2377
  }
2273
2378
  consoleLog.appendChild(entry);
2274
2379
  consoleLog.scrollTop = consoleLog.scrollHeight;
2380
+ return entry;
2381
+ }
2382
+
2383
+ function ensureStreamBubble(messageId) {
2384
+ let entry = streamBubbles.get(messageId);
2385
+ if (entry) return entry;
2386
+ entry = document.createElement("div");
2387
+ entry.className = "log-entry agent";
2388
+ entry.dataset.messageId = messageId;
2389
+ entry.innerHTML = `<div class="log-agent streaming"></div>`;
2390
+ consoleLog.appendChild(entry);
2391
+ streamBubbles.set(messageId, entry);
2392
+ consoleLog.scrollTop = consoleLog.scrollHeight;
2393
+ return entry;
2394
+ }
2395
+
2396
+ function updateStreamBubble(messageId, text, done) {
2397
+ const entry = ensureStreamBubble(messageId);
2398
+ const bubble = entry.querySelector(".log-agent");
2399
+ if (!bubble) return;
2400
+ bubble.classList.toggle("streaming", !done);
2401
+ bubble.innerHTML = renderMarkdown(text || (done ? "" : "…"));
2402
+ consoleLog.scrollTop = consoleLog.scrollHeight;
2403
+ }
2404
+
2405
+ function setConsoleStatus(text, ok = true) {
2406
+ const badge = $("console-status-badge");
2407
+ if (!badge) return;
2408
+ badge.textContent = text;
2409
+ badge.style.background = ok ? "var(--ok-bg)" : "var(--warn-bg)";
2410
+ badge.style.color = ok ? "var(--ok-text)" : "var(--warn-text)";
2411
+ badge.style.borderColor = ok ? "var(--ok-border)" : "var(--warn-border)";
2412
+ }
2413
+
2414
+ function connectAssistantStream() {
2415
+ if (typeof EventSource === "undefined") return;
2416
+ const es = new EventSource("/api/assistant/stream");
2417
+ es.addEventListener("ready", () => setConsoleStatus("已连接", true));
2418
+ es.addEventListener("ping", () => setConsoleStatus("已连接", true));
2419
+ es.addEventListener("user_echo", (ev) => {
2420
+ try {
2421
+ const data = JSON.parse(ev.data);
2422
+ if (data.text && data.text !== lastLocalUserText) appendLog("user", data.text);
2423
+ } catch { /* ignore */ }
2424
+ });
2425
+ es.addEventListener("assistant_start", (ev) => {
2426
+ try {
2427
+ const data = JSON.parse(ev.data);
2428
+ ensureStreamBubble(data.messageId || "assistant");
2429
+ } catch { /* ignore */ }
2430
+ });
2431
+ es.addEventListener("assistant_delta", (ev) => {
2432
+ try {
2433
+ const data = JSON.parse(ev.data);
2434
+ updateStreamBubble(data.messageId || "assistant", data.text || "", false);
2435
+ } catch { /* ignore */ }
2436
+ });
2437
+ es.addEventListener("assistant_end", (ev) => {
2438
+ try {
2439
+ const data = JSON.parse(ev.data);
2440
+ updateStreamBubble(data.messageId || "assistant", data.text || "", true);
2441
+ streamBubbles.delete(data.messageId || "assistant");
2442
+ } catch { /* ignore */ }
2443
+ });
2444
+ es.addEventListener("agent_settled", () => {
2445
+ for (const [id, entry] of streamBubbles) {
2446
+ entry.querySelector(".log-agent")?.classList.remove("streaming");
2447
+ streamBubbles.delete(id);
2448
+ }
2449
+ });
2450
+ es.addEventListener("wake_options", (ev) => {
2451
+ try {
2452
+ const data = JSON.parse(ev.data);
2453
+ if (data.prompt) showWakeModal(data.prompt);
2454
+ } catch { /* ignore */ }
2455
+ });
2456
+ es.addEventListener("wake_options_resolved", (ev) => {
2457
+ try {
2458
+ const data = JSON.parse(ev.data);
2459
+ if (wakeState && wakeState.id === data.promptId) hideWakeModal();
2460
+ appendLog("sys", `唤醒选项已关闭(${data.answer?.source || "resolved"})`);
2461
+ if (data.answer?.source === "panel" || data.answer?.selectedIds?.length) {
2462
+ loadChecklist().catch(() => {});
2463
+ }
2464
+ } catch { /* ignore */ }
2465
+ });
2466
+ es.onerror = () => setConsoleStatus("重连中", false);
2275
2467
  }
2276
2468
 
2469
+ function showWakeModal(prompt, { switchChecklist = true } = {}) {
2470
+ const firstOpen = !wakeState || wakeState.id !== prompt.id;
2471
+ wakeState = {
2472
+ id: prompt.id,
2473
+ mode: prompt.mode,
2474
+ allowMultiple: Boolean(prompt.allowMultiple),
2475
+ selected: wakeState && wakeState.id === prompt.id
2476
+ ? wakeState.selected
2477
+ : new Set((prompt.options || []).filter(o => o.checked).map(o => o.id)),
2478
+ options: prompt.options || [],
2479
+ };
2480
+ if (wakeState.mode === "choice" && wakeState.selected.size === 0 && wakeState.options[0]) {
2481
+ wakeState.selected.add(wakeState.options[0].id);
2482
+ }
2483
+ $("wake-modal-title").textContent = prompt.title || "唤醒选项";
2484
+ $("wake-modal-prompt").textContent = prompt.prompt || (prompt.mode === "checklist" ? "勾选需要确认的项目后提交。" : "请选择一项。");
2485
+ const list = $("wake-option-list");
2486
+ list.innerHTML = "";
2487
+ for (const option of wakeState.options) {
2488
+ const btn = document.createElement("button");
2489
+ btn.type = "button";
2490
+ btn.className = "wake-option" + (wakeState.selected.has(option.id) ? " selected" : "");
2491
+ btn.dataset.id = option.id;
2492
+ const mark = wakeState.mode === "checklist" || wakeState.allowMultiple
2493
+ ? (wakeState.selected.has(option.id) ? "[x]" : "[ ]")
2494
+ : (wakeState.selected.has(option.id) ? "(•)" : "( )");
2495
+ btn.innerHTML = `<span class="wake-mark">${mark}</span><span><div class="wake-label">${esc(option.label)}</div>${option.description ? `<div class="wake-desc">${esc(option.description)}</div>` : ""}</span>`;
2496
+ btn.addEventListener("click", () => {
2497
+ if (wakeState.mode === "choice" && !wakeState.allowMultiple) {
2498
+ wakeState.selected.clear();
2499
+ wakeState.selected.add(option.id);
2500
+ } else if (wakeState.selected.has(option.id)) {
2501
+ wakeState.selected.delete(option.id);
2502
+ } else {
2503
+ wakeState.selected.add(option.id);
2504
+ }
2505
+ showWakeModal(prompt, { switchChecklist: false });
2506
+ });
2507
+ list.appendChild(btn);
2508
+ }
2509
+ $("wake-modal").classList.remove("hidden");
2510
+ openConsole();
2511
+ if (firstOpen && switchChecklist && prompt.mode === "checklist") setView("checklist");
2512
+ }
2513
+
2514
+ function hideWakeModal() {
2515
+ $("wake-modal").classList.add("hidden");
2516
+ wakeState = null;
2517
+ }
2518
+
2519
+ async function submitWake(cancel = false) {
2520
+ if (!wakeState) return;
2521
+ const promptId = wakeState.id;
2522
+ const selectedIds = cancel ? [] : [...wakeState.selected];
2523
+ try {
2524
+ await json("/api/wake-options/respond", {
2525
+ method: "POST",
2526
+ headers: { "content-type": "application/json" },
2527
+ body: JSON.stringify({ promptId, selectedIds, cancel: Boolean(cancel) }),
2528
+ });
2529
+ hideWakeModal();
2530
+ appendLog("sys", cancel ? "已取消唤醒选项" : `已提交选择:${selectedIds.join(", ") || "(空)"}`);
2531
+ await loadChecklist().catch(() => {});
2532
+ } catch (err) {
2533
+ appendLog("sys", `[唤醒选项] ${err.message}`);
2534
+ }
2535
+ }
2536
+
2537
+ $("wake-modal-close")?.addEventListener("click", () => submitWake(true));
2538
+ $("wake-cancel-btn")?.addEventListener("click", () => submitWake(true));
2539
+ $("wake-submit-btn")?.addEventListener("click", () => submitWake(false));
2540
+
2277
2541
  consoleFab.addEventListener("click", () => {
2278
2542
  if (consoleDrawer.classList.contains("open")) closeConsole();
2279
2543
  else openConsole();
2280
2544
  });
2281
2545
  $("console-close-btn").addEventListener("click", closeConsole);
2282
2546
  $("console-clear-btn").addEventListener("click", () => {
2283
- consoleLog.innerHTML = `<div class="log-entry"><div class="log-sys">ψ 终端历史已清空。输入 /help 或提问即可开始。</div></div>`;
2547
+ consoleLog.innerHTML = `<div class="log-entry sys"><div class="log-sys">对话已清空。输入问题或 /help 即可继续。</div></div>`;
2548
+ streamBubbles.clear();
2284
2549
  });
2285
2550
 
2286
2551
  async function sendConsoleMessage(msg) {
2287
2552
  const text = (msg || consoleInput.value).trim();
2288
2553
  if (!text) return;
2289
2554
  consoleInput.value = "";
2555
+ lastLocalUserText = text;
2290
2556
  appendLog("user", text);
2291
2557
 
2292
2558
  try {
@@ -2295,12 +2561,15 @@ async function sendConsoleMessage(msg) {
2295
2561
  headers: { "content-type": "application/json" },
2296
2562
  body: JSON.stringify({ message: text })
2297
2563
  });
2298
- appendLog("agent", res.text || res.message || "✓ 执行成功");
2564
+ // Stub ack only — real assistant text arrives via SSE.
2565
+ if (res.text) appendLog("sys", res.text);
2299
2566
  } catch (err) {
2300
2567
  appendLog("sys", `[提示] ${err.message}`);
2301
2568
  }
2302
2569
  }
2303
2570
 
2571
+ connectAssistantStream();
2572
+
2304
2573
  $("console-send-btn").addEventListener("click", () => sendConsoleMessage());
2305
2574
  consoleInput.addEventListener("keydown", (e) => {
2306
2575
  if (e.key === "Enter" && !e.shiftKey) {