@zhongqian97-code/ecode 0.5.29 → 0.5.31

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.
Files changed (2) hide show
  1. package/dist/index.js +445 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1448,6 +1448,59 @@ function generateTitle(firstUserMessage) {
1448
1448
  const oneLine = firstUserMessage.replace(/\n+/g, " ").trim();
1449
1449
  return oneLine.length > 50 ? oneLine.slice(0, 47) + "..." : oneLine;
1450
1450
  }
1451
+ function deleteSessionFiles(logDir, id) {
1452
+ const session = findSession(logDir, id);
1453
+ if (!session) return false;
1454
+ const logFilePath2 = path6.join(logDir, session.logFile);
1455
+ const metaFilePath = metadataPathFromLogFile(logFilePath2);
1456
+ let deleted = false;
1457
+ try {
1458
+ fs8.unlinkSync(logFilePath2);
1459
+ deleted = true;
1460
+ } catch {
1461
+ }
1462
+ try {
1463
+ fs8.unlinkSync(metaFilePath);
1464
+ deleted = true;
1465
+ } catch {
1466
+ }
1467
+ return deleted;
1468
+ }
1469
+ function loadMessagesFromJsonl(logFilePath2) {
1470
+ try {
1471
+ const content = fs8.readFileSync(logFilePath2, "utf-8");
1472
+ const lines = content.split("\n").filter((l) => l.trim());
1473
+ const messages = [];
1474
+ for (const line of lines) {
1475
+ try {
1476
+ const entry = JSON.parse(line);
1477
+ if (!entry.role || entry.role === "system") continue;
1478
+ if (entry.role === "user") {
1479
+ messages.push({ role: "user", content: entry.content ?? "" });
1480
+ } else if (entry.role === "assistant") {
1481
+ const msg = {
1482
+ role: "assistant",
1483
+ content: entry.content ?? null
1484
+ };
1485
+ if (entry.tool_calls) {
1486
+ msg.tool_calls = entry.tool_calls;
1487
+ }
1488
+ messages.push(msg);
1489
+ } else if (entry.role === "tool") {
1490
+ messages.push({
1491
+ role: "tool",
1492
+ tool_call_id: entry.tool_call_id ?? "",
1493
+ content: entry.content ?? ""
1494
+ });
1495
+ }
1496
+ } catch {
1497
+ }
1498
+ }
1499
+ return messages;
1500
+ } catch {
1501
+ return [];
1502
+ }
1503
+ }
1451
1504
 
1452
1505
  // src/tools/task.ts
1453
1506
  var TASK_TOOL = {
@@ -5353,6 +5406,13 @@ function generateAdminHtml(version2) {
5353
5406
  }
5354
5407
  .session-item .s-title { color: #c9d1d9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
5355
5408
  .session-item .s-id { color: #8b949e; font-size: 10px; }
5409
+ .s-delete {
5410
+ float: right; margin-top: -20px; background: none; border: none;
5411
+ color: #6e7681; cursor: pointer; font-size: 11px; padding: 0 2px; line-height: 1;
5412
+ display: none;
5413
+ }
5414
+ .session-item:hover .s-delete, .session-item.active .s-delete { display: inline; }
5415
+ .s-delete:hover { color: #f85149; }
5356
5416
  .sidebar-empty { color: #8b949e; font-size: 12px; font-style: italic; padding: 12px; }
5357
5417
 
5358
5418
  /* \u2500\u2500 Chat area \u2500\u2500 */
@@ -5447,6 +5507,17 @@ function generateAdminHtml(version2) {
5447
5507
  white-space: pre-wrap;
5448
5508
  word-break: break-word;
5449
5509
  }
5510
+ .thinking-block {
5511
+ border: 1px solid #21262d;
5512
+ border-radius: 4px;
5513
+ font-size: 12px;
5514
+ padding: 6px 10px;
5515
+ margin: 4px 0;
5516
+ color: #6e7681;
5517
+ font-style: italic;
5518
+ white-space: pre-wrap;
5519
+ word-break: break-word;
5520
+ }
5450
5521
  .cursor-blink::after {
5451
5522
  content: "\u258B";
5452
5523
  animation: blink 1s step-start infinite;
@@ -5629,6 +5700,21 @@ function generateAdminHtml(version2) {
5629
5700
  to { opacity: 1; transform: translateX(0); }
5630
5701
  }
5631
5702
 
5703
+ /* \u2500\u2500 Skill dropdown \u2500\u2500 */
5704
+ #skill-dropdown .skill-item {
5705
+ padding: 8px 12px;
5706
+ cursor: pointer;
5707
+ border-bottom: 1px solid #30363d;
5708
+ display: flex;
5709
+ gap: 8px;
5710
+ align-items: baseline;
5711
+ }
5712
+ #skill-dropdown .skill-item:last-child { border-bottom: none; }
5713
+ #skill-dropdown .skill-item:hover,
5714
+ #skill-dropdown .skill-item.selected { background: #2d333b; }
5715
+ #skill-dropdown .skill-name { color: #79c0ff; font-weight: 600; font-size: 0.9em; }
5716
+ #skill-dropdown .skill-desc { color: #8b949e; font-size: 0.8em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 300px; }
5717
+
5632
5718
  /* \u2500\u2500 Mobile \u2500\u2500 */
5633
5719
  @media (max-width: 600px) {
5634
5720
  #hamburger { display: block; }
@@ -5640,6 +5726,22 @@ function generateAdminHtml(version2) {
5640
5726
  transition: transform .2s ease;
5641
5727
  }
5642
5728
  #sidebar.open { transform: translateX(0); }
5729
+
5730
+ /* Fix input bar on mobile */
5731
+ #input-bar {
5732
+ position: fixed;
5733
+ bottom: 0;
5734
+ left: 0;
5735
+ right: 0;
5736
+ bottom: env(safe-area-inset-bottom, 0px);
5737
+ z-index: 40;
5738
+ padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
5739
+ }
5740
+
5741
+ /* Add bottom padding to chat so messages don't hide behind fixed input */
5742
+ #messages {
5743
+ padding-bottom: 80px;
5744
+ }
5643
5745
  }
5644
5746
  </style>
5645
5747
  </head>
@@ -5651,6 +5753,7 @@ function generateAdminHtml(version2) {
5651
5753
  <span id="topbar-model" class="version" style="display:none"></span>
5652
5754
  <button id="config-btn" class="btn">\u914D\u7F6E</button>
5653
5755
  <button id="upgrade-btn" class="btn">\u5347\u7EA7</button>
5756
+ <button id="toggle-tools-btn" class="btn">\u5DE5\u5177 \u25BE</button>
5654
5757
  </div>
5655
5758
 
5656
5759
  <div id="app">
@@ -5677,6 +5780,7 @@ function generateAdminHtml(version2) {
5677
5780
  <div class="empty-chat">\u2190 \u4ECE\u5DE6\u4FA7\u9009\u62E9\u4F1A\u8BDD\uFF0C\u6216\u70B9\u51FB"\u65B0\u5EFA\u4F1A\u8BDD"\u5F00\u59CB</div>
5678
5781
  </div>
5679
5782
  <div id="ws-status" class="ws-status"></div>
5783
+ <div id="skill-dropdown" style="display:none;position:fixed;bottom:60px;left:14px;right:14px;max-height:200px;overflow-y:auto;background:#1c2128;border:1px solid #30363d;border-radius:6px;z-index:60;"></div>
5680
5784
  <div id="input-bar">
5681
5785
  <textarea id="msg-input" rows="1" placeholder="\u8F93\u5165\u6D88\u606F\uFF0C\u6309 Enter \u53D1\u9001\uFF08Shift+Enter \u6362\u884C\uFF09\u2026" disabled></textarea>
5682
5786
  <button id="send-btn" disabled>\u53D1\u9001</button>
@@ -5740,6 +5844,9 @@ function generateAdminHtml(version2) {
5740
5844
  status: 'idle', // idle | thinking | tool_calling | awaiting_confirm
5741
5845
  wsRetries: 0,
5742
5846
  streamingMsgEl: null, // DOM element currently receiving delta tokens
5847
+ showTools: true,
5848
+ skills: [], // cached skills from /api/skills
5849
+ skillsLoaded: false,
5743
5850
  };
5744
5851
 
5745
5852
  // \u2500\u2500 Toast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -5848,15 +5955,45 @@ function generateAdminHtml(version2) {
5848
5955
  const title = (s.title || '(\u65E0\u6807\u9898)').replace(/</g, '&lt;');
5849
5956
  const id = s.id.slice(0, 8);
5850
5957
  return '<div class="session-item' + active + '" data-id="' + s.id + '">' +
5958
+ '<button class="s-delete" data-sid="' + s.id + '" title="\u5220\u9664\u4F1A\u8BDD">\u2715</button>' +
5851
5959
  '<div class="s-title">' + title + '</div>' +
5852
5960
  '<div class="s-id">' + id + '\u2026</div>' +
5853
5961
  '</div>';
5854
5962
  }).join('');
5855
5963
  listEl.querySelectorAll('.session-item').forEach(el => {
5856
- el.addEventListener('click', () => selectSession(el.dataset.id));
5964
+ el.addEventListener('click', (e) => {
5965
+ if (e.target.classList.contains('s-delete')) return;
5966
+ selectSession(el.dataset.id);
5967
+ });
5968
+ });
5969
+ listEl.querySelectorAll('.s-delete').forEach(btn => {
5970
+ btn.addEventListener('click', (e) => {
5971
+ e.stopPropagation();
5972
+ deleteSession(btn.dataset.sid);
5973
+ });
5857
5974
  });
5858
5975
  }
5859
5976
 
5977
+ async function deleteSession(id) {
5978
+ if (!confirm('\u786E\u8BA4\u5220\u9664\u8BE5\u4F1A\u8BDD\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002')) return;
5979
+ try {
5980
+ await apiFetch('/api/sessions/' + id, { method: 'DELETE' });
5981
+ state.sessions = state.sessions.filter(s => s.id !== id);
5982
+ if (state.activeSessionId === id) {
5983
+ state.activeSessionId = null;
5984
+ document.getElementById('messages').innerHTML =
5985
+ '<div class="empty-chat">\u2190 \u4ECE\u5DE6\u4FA7\u9009\u62E9\u4F1A\u8BDD\uFF0C\u6216\u70B9\u51FB"\u65B0\u5EFA\u4F1A\u8BDD"\u5F00\u59CB</div>';
5986
+ document.getElementById('chat-title').textContent = '\u9009\u62E9\u6216\u65B0\u5EFA\u4F1A\u8BDD';
5987
+ if (state.ws) { state.ws.onclose = null; state.ws.close(); state.ws = null; }
5988
+ setStatus('idle');
5989
+ }
5990
+ renderSidebar();
5991
+ showToast('\u4F1A\u8BDD\u5DF2\u5220\u9664', 'success');
5992
+ } catch (e) {
5993
+ showToast('\u5220\u9664\u5931\u8D25: ' + e.message, 'error');
5994
+ }
5995
+ }
5996
+
5860
5997
  async function loadSessions() {
5861
5998
  try {
5862
5999
  const data = await apiFetch('/api/sessions');
@@ -5963,7 +6100,12 @@ function generateAdminHtml(version2) {
5963
6100
  document.getElementById('ws-status').textContent = 'WebSocket \u9519\u8BEF';
5964
6101
  };
5965
6102
 
5966
- ws.onclose = () => {
6103
+ ws.onclose = (evt) => {
6104
+ // 4404 = \u4F1A\u8BDD\u4E0D\u5728\u5185\u5B58\u4E2D\uFF08\u5DF2\u7531 resume API \u5904\u7406\uFF09\uFF0C\u4E0D\u9700\u8981\u91CD\u8BD5
6105
+ if (evt.code === 4404) {
6106
+ document.getElementById('ws-status').textContent = '';
6107
+ return;
6108
+ }
5967
6109
  document.getElementById('ws-status').textContent = 'WebSocket \u5DF2\u65AD\u5F00';
5968
6110
  finalizeStreamingMsg();
5969
6111
  if (state.activeSessionId === sessionId && state.wsRetries < 5) {
@@ -6004,6 +6146,7 @@ function generateAdminHtml(version2) {
6004
6146
  boxEl.innerHTML =
6005
6147
  '<div class="tool-box-header">\u250C\u2500 ' + escHtml(name) + ' \u2500</div>' +
6006
6148
  '<div class="tool-box-body">\u2026</div>';
6149
+ if (!state.showTools) boxEl.style.display = 'none';
6007
6150
  msgsEl.appendChild(boxEl);
6008
6151
  msgsEl.scrollTop = msgsEl.scrollHeight;
6009
6152
  setStatus('tool_calling');
@@ -6028,6 +6171,16 @@ function generateAdminHtml(version2) {
6028
6171
  } else if (type === 'session.idle') {
6029
6172
  finalizeStreamingMsg();
6030
6173
  setStatus('idle');
6174
+ } else if (type === 'message.reasoning') {
6175
+ const msgsEl = document.getElementById('messages');
6176
+ const placeholder = msgsEl.querySelector('.empty-chat');
6177
+ if (placeholder) placeholder.remove();
6178
+ const thinkEl = document.createElement('div');
6179
+ thinkEl.className = 'thinking-block';
6180
+ thinkEl.textContent = msg.reasoning || msg.text || '';
6181
+ if (!state.showTools) thinkEl.style.display = 'none';
6182
+ msgsEl.appendChild(thinkEl);
6183
+ msgsEl.scrollTop = msgsEl.scrollHeight;
6031
6184
  } else if (type === 'session.error') {
6032
6185
  finalizeStreamingMsg();
6033
6186
  setStatus('idle');
@@ -6042,6 +6195,70 @@ function generateAdminHtml(version2) {
6042
6195
  .replace(/>/g, '&gt;');
6043
6196
  }
6044
6197
 
6198
+ // \u2500\u2500 Skill dropdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6199
+ var selectedSkillIndex = -1;
6200
+
6201
+ async function loadSkills() {
6202
+ if (state.skillsLoaded) return;
6203
+ try {
6204
+ const data = await apiFetch('/api/skills');
6205
+ state.skills = Array.isArray(data) ? data : [];
6206
+ state.skillsLoaded = true;
6207
+ } catch (e) {
6208
+ state.skills = [];
6209
+ }
6210
+ }
6211
+
6212
+ function showSkillDropdown(query) {
6213
+ const dropdown = document.getElementById('skill-dropdown');
6214
+ if (!dropdown) return;
6215
+ const q = query.toLowerCase();
6216
+ const filtered = state.skills.filter(function(s) {
6217
+ return s.name.toLowerCase().startsWith(q);
6218
+ });
6219
+ if (!filtered.length) {
6220
+ dropdown.style.display = 'none';
6221
+ selectedSkillIndex = -1;
6222
+ return;
6223
+ }
6224
+ selectedSkillIndex = -1;
6225
+ dropdown.innerHTML = filtered.map(function(s, i) {
6226
+ return '<div class="skill-item" data-name="' + escHtml(s.name) + '">' +
6227
+ '<span class="skill-name">/' + escHtml(s.name) + '</span>' +
6228
+ '<span class="skill-desc">' + escHtml(s.description || '') + '</span>' +
6229
+ '</div>';
6230
+ }).join('');
6231
+ dropdown.querySelectorAll('.skill-item').forEach(function(el) {
6232
+ el.addEventListener('click', function() {
6233
+ var name = el.getAttribute('data-name');
6234
+ var inputEl = document.getElementById('msg-input');
6235
+ inputEl.value = '/' + name + ' ';
6236
+ hideSkillDropdown();
6237
+ inputEl.focus();
6238
+ });
6239
+ });
6240
+ dropdown.style.display = 'block';
6241
+ }
6242
+
6243
+ function hideSkillDropdown() {
6244
+ const dropdown = document.getElementById('skill-dropdown');
6245
+ if (dropdown) dropdown.style.display = 'none';
6246
+ selectedSkillIndex = -1;
6247
+ }
6248
+
6249
+ function updateSkillSelection() {
6250
+ const dropdown = document.getElementById('skill-dropdown');
6251
+ if (!dropdown) return;
6252
+ const items = dropdown.querySelectorAll('.skill-item');
6253
+ items.forEach(function(el, i) {
6254
+ if (i === selectedSkillIndex) {
6255
+ el.classList.add('selected');
6256
+ } else {
6257
+ el.classList.remove('selected');
6258
+ }
6259
+ });
6260
+ }
6261
+
6045
6262
  // \u2500\u2500 Approval modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6046
6263
  function showApprovalModal(msg) {
6047
6264
  const box = document.getElementById('approval-box');
@@ -6075,7 +6292,7 @@ function generateAdminHtml(version2) {
6075
6292
  document.getElementById('deny-btn').addEventListener('click', () => sendApproval(false));
6076
6293
 
6077
6294
  // \u2500\u2500 Session selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6078
- function selectSession(id) {
6295
+ async function selectSession(id) {
6079
6296
  state.activeSessionId = id;
6080
6297
  const session = state.sessions.find(s => s.id === id);
6081
6298
  document.getElementById('chat-title').textContent =
@@ -6084,6 +6301,12 @@ function generateAdminHtml(version2) {
6084
6301
  loadMessages(id);
6085
6302
  renderSidebar();
6086
6303
  setStatus('idle');
6304
+ // \u5148\u5C1D\u8BD5\u6062\u590D\uFF08\u5C06\u5386\u53F2\u4F1A\u8BDD\u52A0\u8F7D\u5230\u5185\u5B58\uFF09\uFF0C\u518D\u8FDE\u63A5 WS
6305
+ try {
6306
+ await apiFetch('/api/chat/sessions/' + id + '/resume', { method: 'POST' });
6307
+ } catch {
6308
+ // \u6062\u590D\u5931\u8D25\u4E5F\u7EE7\u7EED\uFF08\u53EF\u80FD\u5DF2\u5728\u5185\u5B58\u4E2D\uFF0C\u6216\u53EA\u8BFB\u67E5\u770B\uFF09
6309
+ }
6087
6310
  connectWs(id);
6088
6311
  // Close sidebar on mobile
6089
6312
  document.getElementById('sidebar').classList.remove('open');
@@ -6135,17 +6358,57 @@ function generateAdminHtml(version2) {
6135
6358
 
6136
6359
  document.getElementById('send-btn').addEventListener('click', sendMessage);
6137
6360
 
6138
- document.getElementById('msg-input').addEventListener('keydown', (e) => {
6361
+ document.getElementById('msg-input').addEventListener('keydown', function(e) {
6362
+ const dropdown = document.getElementById('skill-dropdown');
6363
+ const dropdownVisible = dropdown && dropdown.style.display !== 'none';
6364
+ if (dropdownVisible) {
6365
+ const items = dropdown.querySelectorAll('.skill-item');
6366
+ if (e.key === 'ArrowDown') {
6367
+ e.preventDefault();
6368
+ selectedSkillIndex = Math.min(selectedSkillIndex + 1, items.length - 1);
6369
+ updateSkillSelection();
6370
+ return;
6371
+ }
6372
+ if (e.key === 'ArrowUp') {
6373
+ e.preventDefault();
6374
+ selectedSkillIndex = Math.max(selectedSkillIndex - 1, -1);
6375
+ updateSkillSelection();
6376
+ return;
6377
+ }
6378
+ if (e.key === 'Enter' && selectedSkillIndex >= 0) {
6379
+ e.preventDefault();
6380
+ const selected = items[selectedSkillIndex];
6381
+ if (selected) {
6382
+ var name = selected.getAttribute('data-name');
6383
+ var inputEl = document.getElementById('msg-input');
6384
+ inputEl.value = '/' + name + ' ';
6385
+ hideSkillDropdown();
6386
+ inputEl.focus();
6387
+ }
6388
+ return;
6389
+ }
6390
+ if (e.key === 'Escape') {
6391
+ e.preventDefault();
6392
+ hideSkillDropdown();
6393
+ return;
6394
+ }
6395
+ }
6139
6396
  if (e.key === 'Enter' && !e.shiftKey) {
6140
6397
  e.preventDefault();
6141
6398
  sendMessage();
6142
6399
  }
6143
6400
  });
6144
6401
 
6145
- // Auto-resize textarea
6402
+ // Auto-resize textarea + slash command dropdown
6146
6403
  document.getElementById('msg-input').addEventListener('input', function() {
6147
6404
  this.style.height = 'auto';
6148
6405
  this.style.height = Math.min(this.scrollHeight, 120) + 'px';
6406
+ const val = this.value;
6407
+ if (val.startsWith('/')) {
6408
+ loadSkills().then(function() { showSkillDropdown(val.slice(1)); });
6409
+ } else {
6410
+ hideSkillDropdown();
6411
+ }
6149
6412
  });
6150
6413
 
6151
6414
  // \u2500\u2500 Hamburger (mobile) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -6248,6 +6511,22 @@ function generateAdminHtml(version2) {
6248
6511
  }
6249
6512
  });
6250
6513
 
6514
+ // \u2500\u2500 \u5DE5\u5177/\u601D\u8003\u53EF\u89C1\u6027\u5207\u6362 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6515
+ document.getElementById('toggle-tools-btn').addEventListener('click', () => {
6516
+ state.showTools = !state.showTools;
6517
+ document.getElementById('toggle-tools-btn').textContent = state.showTools ? '\u5DE5\u5177 \u25BE' : '\u5DE5\u5177 \u25B8';
6518
+ document.querySelectorAll('.tool-box, .thinking-block').forEach(el => {
6519
+ el.style.display = state.showTools ? '' : 'none';
6520
+ });
6521
+ });
6522
+
6523
+ // \u2500\u2500 Skill dropdown: hide on outside click \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6524
+ document.addEventListener('click', function(e) {
6525
+ if (!e.target.closest('#skill-dropdown') && !e.target.closest('#msg-input')) {
6526
+ hideSkillDropdown();
6527
+ }
6528
+ });
6529
+
6251
6530
  // \u2500\u2500 Mobile keyboard adaptation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6252
6531
  document.getElementById('msg-input').addEventListener('focus', () => {
6253
6532
  setTimeout(() => {
@@ -6255,6 +6534,15 @@ function generateAdminHtml(version2) {
6255
6534
  }, 300);
6256
6535
  });
6257
6536
 
6537
+ if (window.visualViewport) {
6538
+ window.visualViewport.addEventListener('resize', function() {
6539
+ const bar = document.getElementById('input-bar');
6540
+ if (!bar) return;
6541
+ const offsetBottom = window.innerHeight - window.visualViewport.height - window.visualViewport.offsetTop;
6542
+ bar.style.bottom = Math.max(0, offsetBottom) + 'px';
6543
+ });
6544
+ }
6545
+
6258
6546
  // \u2500\u2500 Global error boundary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6259
6547
  window.onerror = function(msg, src, line) {
6260
6548
  const wsStatusEl = document.getElementById('ws-status');
@@ -6372,6 +6660,23 @@ async function sessionsRoutes(app, opts) {
6372
6660
  return reply.send({ success: true, command });
6373
6661
  }
6374
6662
  );
6663
+ app.delete(
6664
+ "/api/sessions/:id",
6665
+ async (request, reply) => {
6666
+ const { id } = request.params;
6667
+ if (!opts.config.logDir) {
6668
+ return reply.code(404).send({ success: false, error: "Log directory not configured" });
6669
+ }
6670
+ const deleted = deleteSessionFiles(opts.config.logDir, id);
6671
+ if (!deleted) {
6672
+ return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
6673
+ }
6674
+ if (opts.manager) {
6675
+ opts.manager.deleteSession(id);
6676
+ }
6677
+ return reply.send({ success: true });
6678
+ }
6679
+ );
6375
6680
  app.get(
6376
6681
  "/api/sessions/:id/fork-command",
6377
6682
  async (request, reply) => {
@@ -6503,6 +6808,7 @@ async function automationRoutes(app, opts) {
6503
6808
  }
6504
6809
 
6505
6810
  // src/web/routes/chat.ts
6811
+ import { join as join14 } from "path";
6506
6812
  var BUSY_STATUSES = /* @__PURE__ */ new Set(["thinking", "tool_calling", "awaiting_confirm"]);
6507
6813
  async function chatRoutes(app, opts) {
6508
6814
  const { manager } = opts;
@@ -6512,6 +6818,9 @@ async function chatRoutes(app, opts) {
6512
6818
  if (typeof body.systemPrompt === "string") {
6513
6819
  sessionOpts.systemPrompt = body.systemPrompt;
6514
6820
  }
6821
+ if (opts.autoApprove) {
6822
+ sessionOpts.autoApproveNormal = true;
6823
+ }
6515
6824
  if (!opts.config.apiKey) {
6516
6825
  return reply.code(400).send({
6517
6826
  success: false,
@@ -6556,6 +6865,37 @@ async function chatRoutes(app, opts) {
6556
6865
  return reply.send({ success: true });
6557
6866
  }
6558
6867
  );
6868
+ app.post(
6869
+ "/api/chat/sessions/:id/resume",
6870
+ async (request, reply) => {
6871
+ const { id } = request.params;
6872
+ if (manager.getSession(id)) {
6873
+ return reply.send({ success: true, resumed: false });
6874
+ }
6875
+ if (!opts.config.apiKey) {
6876
+ return reply.code(400).send({
6877
+ success: false,
6878
+ error: "\u672A\u914D\u7F6E API Key\uFF0C\u8BF7\u5148\u70B9\u51FB\u53F3\u4E0A\u89D2\u300C\u914D\u7F6E\u300D\u6309\u94AE\u586B\u5199"
6879
+ });
6880
+ }
6881
+ if (!opts.config.logDir) {
6882
+ return reply.code(404).send({ success: false, error: "Log directory not configured" });
6883
+ }
6884
+ const session = findSession(opts.config.logDir, id);
6885
+ if (!session) {
6886
+ return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
6887
+ }
6888
+ const logFilePath2 = join14(opts.config.logDir, session.logFile);
6889
+ const initialMessages = loadMessagesFromJsonl(logFilePath2);
6890
+ try {
6891
+ await manager.createSession({ sessionId: id, initialMessages });
6892
+ return reply.send({ success: true, resumed: true });
6893
+ } catch (err) {
6894
+ const msg = err instanceof Error ? err.message : String(err);
6895
+ return reply.code(500).send({ success: false, error: msg });
6896
+ }
6897
+ }
6898
+ );
6559
6899
  app.post(
6560
6900
  "/api/chat/sessions/:id/approve",
6561
6901
  async (request, reply) => {
@@ -6645,6 +6985,80 @@ async function systemRoutes(app, opts) {
6645
6985
  });
6646
6986
  }
6647
6987
 
6988
+ // src/web/routes/skills.ts
6989
+ import { readdir as readdir5, readFile as readFile10 } from "fs/promises";
6990
+ import { join as join15 } from "path";
6991
+ import os2 from "os";
6992
+ var BUILTIN_COMMANDS = [
6993
+ { name: "clear", description: "Clear conversation history", type: "builtin" },
6994
+ { name: "interrupt", description: "Interrupt current operation", type: "builtin" },
6995
+ { name: "help", description: "Show help information", type: "builtin" }
6996
+ ];
6997
+ function parseFrontmatter2(content) {
6998
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
6999
+ if (!match) return null;
7000
+ const frontmatter = match[1];
7001
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
7002
+ if (!nameMatch) return null;
7003
+ const descMatch = frontmatter.match(/^description:\s*(?:>-?|[|>+])?\s*(.+)$/m);
7004
+ if (!descMatch) return null;
7005
+ const name = nameMatch[1].trim();
7006
+ const description = descMatch[1].trim();
7007
+ if (!name || !description) return null;
7008
+ return { name, description };
7009
+ }
7010
+ async function loadAgents() {
7011
+ const agentsDir = join15(os2.homedir(), ".claude", "agents");
7012
+ let files;
7013
+ try {
7014
+ const entries = await readdir5(agentsDir);
7015
+ files = entries.filter((f) => f.endsWith(".md"));
7016
+ } catch {
7017
+ return [];
7018
+ }
7019
+ const items = [];
7020
+ for (const file of files) {
7021
+ try {
7022
+ const content = await readFile10(join15(agentsDir, file), "utf-8");
7023
+ const parsed = parseFrontmatter2(content);
7024
+ if (parsed) {
7025
+ items.push({ ...parsed, type: "agent" });
7026
+ }
7027
+ } catch {
7028
+ }
7029
+ }
7030
+ return items;
7031
+ }
7032
+ async function loadSkills() {
7033
+ const skillsDir = join15(process.cwd(), "skills");
7034
+ let subdirs;
7035
+ try {
7036
+ const entries = await readdir5(skillsDir, { withFileTypes: true });
7037
+ subdirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
7038
+ } catch {
7039
+ return [];
7040
+ }
7041
+ const items = [];
7042
+ for (const subdir of subdirs) {
7043
+ try {
7044
+ const skillFile = join15(skillsDir, subdir, "SKILL.md");
7045
+ const content = await readFile10(skillFile, "utf-8");
7046
+ const parsed = parseFrontmatter2(content);
7047
+ if (parsed) {
7048
+ items.push({ ...parsed, type: "skill" });
7049
+ }
7050
+ } catch {
7051
+ }
7052
+ }
7053
+ return items;
7054
+ }
7055
+ async function skillsRoutes(app) {
7056
+ app.get("/api/skills", async (_request, _reply) => {
7057
+ const [agents, skills] = await Promise.all([loadAgents(), loadSkills()]);
7058
+ return [...BUILTIN_COMMANDS, ...agents, ...skills];
7059
+ });
7060
+ }
7061
+
6648
7062
  // src/web/server.ts
6649
7063
  async function buildServer(opts) {
6650
7064
  const app = Fastify({ logger: false });
@@ -6671,8 +7085,9 @@ async function buildServer(opts) {
6671
7085
  await app.register(sessionsRoutes, { config: opts.config, manager: opts.manager });
6672
7086
  await app.register(configRoutes, { config: opts.config, save: saveConfig });
6673
7087
  await app.register(automationRoutes, { config: opts.config });
6674
- await app.register(chatRoutes, { config: opts.config, manager: opts.manager });
7088
+ await app.register(chatRoutes, { config: opts.config, manager: opts.manager, autoApprove: opts.autoApprove });
6675
7089
  await app.register(systemRoutes, { version: opts.version });
7090
+ await app.register(skillsRoutes);
6676
7091
  await app.register(sessionHubRoutes, { manager: opts.manager });
6677
7092
  return app;
6678
7093
  }
@@ -6768,6 +7183,7 @@ var SessionRuntime = class {
6768
7183
  messages;
6769
7184
  config;
6770
7185
  model;
7186
+ autoApproveNormal;
6771
7187
  abortController = null;
6772
7188
  _stopAfterToolRound = false;
6773
7189
  _turnCount = 0;
@@ -6780,6 +7196,7 @@ var SessionRuntime = class {
6780
7196
  this.config = config2;
6781
7197
  this.llm = opts.llm ?? createProvider(resolveActiveProfile(config2));
6782
7198
  this.model = config2.model;
7199
+ this.autoApproveNormal = opts.autoApproveNormal ?? false;
6783
7200
  const systemContent = opts.systemPrompt ?? (config2.systemPrompt ?? DEFAULT_SYSTEM_PROMPT);
6784
7201
  this.messages = systemContent ? [{ role: "system", content: systemContent }] : [];
6785
7202
  if (opts.initialMessages) {
@@ -6918,17 +7335,20 @@ var SessionRuntime = class {
6918
7335
  const parsed = JSON.parse(args);
6919
7336
  const cls = classifyCommand(parsed.command, this.config.dangerousPatterns);
6920
7337
  if (cls === "normal" || cls === "danger") {
6921
- this._status = "awaiting_confirm";
6922
- const kind = cls === "danger" ? "danger" : "normal";
6923
- const prompt = cls === "danger" ? `\u26A0\uFE0F DANGEROUS COMMAND: ${parsed.command}` : `Execute command: ${parsed.command}
7338
+ if (cls === "normal" && this.autoApproveNormal) {
7339
+ } else {
7340
+ this._status = "awaiting_confirm";
7341
+ const kind = cls === "danger" ? "danger" : "normal";
7342
+ const prompt = cls === "danger" ? `\u26A0\uFE0F DANGEROUS COMMAND: ${parsed.command}` : `Execute command: ${parsed.command}
6924
7343
  Proceed?`;
6925
- const { id: reqId, promise } = this.approvals.request(kind, prompt);
6926
- this.bus.emit({ type: "approval.requested", requestId: reqId, kind, prompt });
6927
- const approved = await promise;
6928
- this._status = "tool_calling";
6929
- if (!approved) {
6930
- this._stopAfterToolRound = true;
6931
- return SKIP_MESSAGE;
7344
+ const { id: reqId, promise } = this.approvals.request(kind, prompt);
7345
+ this.bus.emit({ type: "approval.requested", requestId: reqId, kind, prompt });
7346
+ const approved = await promise;
7347
+ this._status = "tool_calling";
7348
+ if (!approved) {
7349
+ this._stopAfterToolRound = true;
7350
+ return SKIP_MESSAGE;
7351
+ }
6932
7352
  }
6933
7353
  }
6934
7354
  if (signal.aborted) return SKIP_MESSAGE;
@@ -7050,6 +7470,10 @@ var SessionManager = class {
7050
7470
  listRunning() {
7051
7471
  return [...this.sessions.values()].map((s) => s.snapshot());
7052
7472
  }
7473
+ /** 从内存中移除指定会话(不影响磁盘文件,由上层路由负责文件清理) */
7474
+ deleteSession(sessionId) {
7475
+ this.sessions.delete(sessionId);
7476
+ }
7053
7477
  };
7054
7478
 
7055
7479
  // src/index.ts
@@ -7202,6 +7626,7 @@ if (rawArgs[0] === "sessions") {
7202
7626
  if (rawArgs[0] === "web") {
7203
7627
  let webPort = 4310;
7204
7628
  let webHost = "127.0.0.1";
7629
+ let webAutoApprove = false;
7205
7630
  for (let i = 1; i < rawArgs.length; i++) {
7206
7631
  const arg = rawArgs[i];
7207
7632
  const next = rawArgs[i + 1];
@@ -7212,6 +7637,8 @@ if (rawArgs[0] === "web") {
7212
7637
  } else if (arg === "--host" && next && !next.startsWith("-")) {
7213
7638
  webHost = next;
7214
7639
  i++;
7640
+ } else if (arg === "--auto") {
7641
+ webAutoApprove = true;
7215
7642
  }
7216
7643
  }
7217
7644
  const token = generateAccessToken();
@@ -7220,7 +7647,8 @@ if (rawArgs[0] === "web") {
7220
7647
  config: finalConfig,
7221
7648
  manager,
7222
7649
  token,
7223
- version: VERSION
7650
+ version: VERSION,
7651
+ autoApprove: webAutoApprove
7224
7652
  });
7225
7653
  await server.listen({ port: webPort, host: webHost });
7226
7654
  const browserHost = webHost === "0.0.0.0" ? "localhost" : webHost;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.5.29",
3
+ "version": "0.5.31",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",