@zhongqian97-code/ecode 0.5.18 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +746 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5244,6 +5244,748 @@ function createAuthHook(token) {
5244
5244
  };
5245
5245
  }
5246
5246
 
5247
+ // src/web/admin-html.ts
5248
+ function generateAdminHtml(version2) {
5249
+ return `<!DOCTYPE html>
5250
+ <html lang="zh">
5251
+ <head>
5252
+ <meta charset="UTF-8">
5253
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5254
+ <title>ecode web admin</title>
5255
+ <style>
5256
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
5257
+ body {
5258
+ font-family: "Courier New", Courier, monospace;
5259
+ background: #0d1117;
5260
+ color: #c9d1d9;
5261
+ height: 100vh;
5262
+ display: flex;
5263
+ flex-direction: column;
5264
+ overflow: hidden;
5265
+ }
5266
+ /* \u2500\u2500 Top bar \u2500\u2500 */
5267
+ #topbar {
5268
+ display: flex;
5269
+ align-items: center;
5270
+ gap: 10px;
5271
+ padding: 8px 14px;
5272
+ background: #161b22;
5273
+ border-bottom: 1px solid #30363d;
5274
+ flex-shrink: 0;
5275
+ }
5276
+ #topbar h1 { font-size: 14px; color: #e6edf3; font-weight: 600; }
5277
+ #topbar .version { font-size: 11px; color: #8b949e; background: #21262d;
5278
+ padding: 1px 7px; border-radius: 20px; }
5279
+ #hamburger {
5280
+ display: none; background: none; border: none; color: #c9d1d9;
5281
+ font-size: 18px; cursor: pointer; padding: 2px 6px;
5282
+ }
5283
+
5284
+ /* \u2500\u2500 Main layout \u2500\u2500 */
5285
+ #app {
5286
+ display: flex;
5287
+ flex: 1;
5288
+ overflow: hidden;
5289
+ }
5290
+
5291
+ /* \u2500\u2500 Sidebar \u2500\u2500 */
5292
+ #sidebar {
5293
+ width: 220px;
5294
+ flex-shrink: 0;
5295
+ background: #161b22;
5296
+ border-right: 1px solid #30363d;
5297
+ display: flex;
5298
+ flex-direction: column;
5299
+ overflow: hidden;
5300
+ }
5301
+ #sidebar-header {
5302
+ padding: 10px 12px;
5303
+ border-bottom: 1px solid #30363d;
5304
+ flex-shrink: 0;
5305
+ }
5306
+ #new-session-btn {
5307
+ width: 100%;
5308
+ padding: 6px 10px;
5309
+ background: #21262d;
5310
+ color: #79c0ff;
5311
+ border: 1px solid #30363d;
5312
+ border-radius: 4px;
5313
+ cursor: pointer;
5314
+ font-family: inherit;
5315
+ font-size: 12px;
5316
+ text-align: left;
5317
+ }
5318
+ #new-session-btn:hover { background: #30363d; }
5319
+ #session-list {
5320
+ flex: 1;
5321
+ overflow-y: auto;
5322
+ padding: 6px 0;
5323
+ }
5324
+ .session-item {
5325
+ padding: 8px 12px;
5326
+ cursor: pointer;
5327
+ border-left: 2px solid transparent;
5328
+ font-size: 12px;
5329
+ line-height: 1.4;
5330
+ }
5331
+ .session-item:hover { background: #21262d; }
5332
+ .session-item.active {
5333
+ background: #21262d;
5334
+ border-left-color: #79c0ff;
5335
+ }
5336
+ .session-item .s-title { color: #c9d1d9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
5337
+ .session-item .s-id { color: #8b949e; font-size: 10px; }
5338
+ .sidebar-empty { color: #8b949e; font-size: 12px; font-style: italic; padding: 12px; }
5339
+
5340
+ /* \u2500\u2500 Chat area \u2500\u2500 */
5341
+ #chat-area {
5342
+ flex: 1;
5343
+ display: flex;
5344
+ flex-direction: column;
5345
+ overflow: hidden;
5346
+ }
5347
+ #chat-header {
5348
+ padding: 10px 14px;
5349
+ border-bottom: 1px solid #30363d;
5350
+ background: #161b22;
5351
+ flex-shrink: 0;
5352
+ display: flex;
5353
+ align-items: center;
5354
+ gap: 10px;
5355
+ }
5356
+ #chat-title { font-size: 13px; color: #8b949e; }
5357
+ #status-indicator {
5358
+ margin-left: auto;
5359
+ font-size: 11px;
5360
+ color: #8b949e;
5361
+ display: flex;
5362
+ align-items: center;
5363
+ gap: 5px;
5364
+ }
5365
+ #status-dot {
5366
+ width: 7px; height: 7px; border-radius: 50%;
5367
+ background: #3fb950;
5368
+ }
5369
+ #status-dot.thinking { background: #d29922; animation: pulse 1s infinite; }
5370
+ #status-dot.error { background: #f85149; }
5371
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
5372
+
5373
+ /* \u2500\u2500 Messages \u2500\u2500 */
5374
+ #messages {
5375
+ flex: 1;
5376
+ overflow-y: auto;
5377
+ padding: 14px;
5378
+ display: flex;
5379
+ flex-direction: column;
5380
+ gap: 10px;
5381
+ }
5382
+ .msg {
5383
+ font-size: 13px;
5384
+ line-height: 1.6;
5385
+ max-width: 100%;
5386
+ }
5387
+ .msg-header {
5388
+ font-size: 11px;
5389
+ margin-bottom: 3px;
5390
+ font-weight: 600;
5391
+ }
5392
+ .msg-header.user { color: #3fb950; }
5393
+ .msg-header.assistant { color: #c9d1d9; }
5394
+ .msg-header.tool { color: #8b949e; }
5395
+ .msg-body { color: #c9d1d9; white-space: pre-wrap; word-break: break-word; }
5396
+ .msg-body code {
5397
+ background: #21262d;
5398
+ padding: 1px 5px;
5399
+ border-radius: 3px;
5400
+ font-family: inherit;
5401
+ font-size: 12px;
5402
+ }
5403
+ .msg-body pre {
5404
+ background: #161b22;
5405
+ border: 1px solid #30363d;
5406
+ border-radius: 4px;
5407
+ padding: 10px;
5408
+ overflow-x: auto;
5409
+ margin: 6px 0;
5410
+ }
5411
+ .msg-body pre code { background: none; padding: 0; }
5412
+ .msg-body strong { color: #e6edf3; }
5413
+ .tool-box {
5414
+ border: 1px solid #30363d;
5415
+ border-radius: 4px;
5416
+ font-size: 12px;
5417
+ overflow: hidden;
5418
+ margin: 4px 0;
5419
+ }
5420
+ .tool-box-header {
5421
+ background: #21262d;
5422
+ padding: 4px 10px;
5423
+ color: #8b949e;
5424
+ font-size: 11px;
5425
+ }
5426
+ .tool-box-body {
5427
+ padding: 8px 10px;
5428
+ color: #c9d1d9;
5429
+ white-space: pre-wrap;
5430
+ word-break: break-word;
5431
+ }
5432
+ .cursor-blink::after {
5433
+ content: "\u258B";
5434
+ animation: blink 1s step-start infinite;
5435
+ }
5436
+ @keyframes blink { 50% { opacity: 0; } }
5437
+ .empty-chat {
5438
+ color: #8b949e;
5439
+ font-size: 13px;
5440
+ font-style: italic;
5441
+ text-align: center;
5442
+ padding: 40px 20px;
5443
+ }
5444
+ .ws-status {
5445
+ font-size: 11px;
5446
+ color: #8b949e;
5447
+ text-align: center;
5448
+ padding: 4px;
5449
+ flex-shrink: 0;
5450
+ }
5451
+
5452
+ /* \u2500\u2500 Input bar \u2500\u2500 */
5453
+ #input-bar {
5454
+ border-top: 1px solid #30363d;
5455
+ background: #161b22;
5456
+ padding: 10px 14px;
5457
+ display: flex;
5458
+ gap: 8px;
5459
+ align-items: flex-end;
5460
+ flex-shrink: 0;
5461
+ }
5462
+ #msg-input {
5463
+ flex: 1;
5464
+ background: #0d1117;
5465
+ border: 1px solid #30363d;
5466
+ border-radius: 4px;
5467
+ color: #c9d1d9;
5468
+ font-family: inherit;
5469
+ font-size: 13px;
5470
+ padding: 8px 10px;
5471
+ resize: none;
5472
+ outline: none;
5473
+ min-height: 38px;
5474
+ max-height: 120px;
5475
+ }
5476
+ #msg-input:focus { border-color: #58a6ff; }
5477
+ #msg-input:disabled { opacity: 0.5; cursor: not-allowed; }
5478
+ #send-btn {
5479
+ padding: 8px 14px;
5480
+ background: #21262d;
5481
+ color: #79c0ff;
5482
+ border: 1px solid #30363d;
5483
+ border-radius: 4px;
5484
+ cursor: pointer;
5485
+ font-family: inherit;
5486
+ font-size: 13px;
5487
+ white-space: nowrap;
5488
+ flex-shrink: 0;
5489
+ }
5490
+ #send-btn:hover:not(:disabled) { background: #30363d; }
5491
+ #send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
5492
+
5493
+ /* \u2500\u2500 Approval modal \u2500\u2500 */
5494
+ #approval-modal {
5495
+ display: none;
5496
+ position: fixed;
5497
+ inset: 0;
5498
+ background: rgba(0,0,0,.65);
5499
+ z-index: 100;
5500
+ align-items: center;
5501
+ justify-content: center;
5502
+ }
5503
+ #approval-modal.visible { display: flex; }
5504
+ #approval-box {
5505
+ background: #161b22;
5506
+ border: 1px solid #30363d;
5507
+ border-radius: 6px;
5508
+ width: min(520px, 90vw);
5509
+ padding: 20px;
5510
+ }
5511
+ #approval-box.danger { border-color: #f85149; }
5512
+ #approval-title {
5513
+ font-size: 13px;
5514
+ font-weight: 600;
5515
+ color: #e6edf3;
5516
+ margin-bottom: 10px;
5517
+ }
5518
+ #approval-prompt {
5519
+ background: #0d1117;
5520
+ border: 1px solid #30363d;
5521
+ border-radius: 4px;
5522
+ padding: 10px;
5523
+ font-size: 12px;
5524
+ color: #c9d1d9;
5525
+ white-space: pre-wrap;
5526
+ word-break: break-word;
5527
+ max-height: 200px;
5528
+ overflow-y: auto;
5529
+ margin-bottom: 14px;
5530
+ }
5531
+ #approval-actions { display: flex; gap: 10px; justify-content: flex-end; }
5532
+ #deny-btn {
5533
+ padding: 6px 14px;
5534
+ background: none;
5535
+ border: 1px solid #30363d;
5536
+ color: #c9d1d9;
5537
+ border-radius: 4px;
5538
+ cursor: pointer;
5539
+ font-family: inherit;
5540
+ font-size: 13px;
5541
+ }
5542
+ #deny-btn:hover { background: #21262d; }
5543
+ #approve-btn {
5544
+ padding: 6px 14px;
5545
+ background: #1a4731;
5546
+ border: 1px solid #3fb950;
5547
+ color: #3fb950;
5548
+ border-radius: 4px;
5549
+ cursor: pointer;
5550
+ font-family: inherit;
5551
+ font-size: 13px;
5552
+ }
5553
+ #approve-btn:hover { background: #2ea043; color: #fff; }
5554
+
5555
+ /* \u2500\u2500 Mobile \u2500\u2500 */
5556
+ @media (max-width: 600px) {
5557
+ #hamburger { display: block; }
5558
+ #sidebar {
5559
+ position: fixed;
5560
+ top: 0; left: 0; bottom: 0;
5561
+ z-index: 50;
5562
+ transform: translateX(-100%);
5563
+ transition: transform .2s ease;
5564
+ }
5565
+ #sidebar.open { transform: translateX(0); }
5566
+ }
5567
+ </style>
5568
+ </head>
5569
+ <body>
5570
+ <div id="topbar">
5571
+ <button id="hamburger" aria-label="Toggle sidebar">\u2630</button>
5572
+ <h1>\u26A1 ecode web admin</h1>
5573
+ <span class="version">v${version2}</span>
5574
+ </div>
5575
+
5576
+ <div id="app">
5577
+ <!-- Left: session sidebar -->
5578
+ <div id="sidebar">
5579
+ <div id="sidebar-header">
5580
+ <button id="new-session-btn">\uFF0B \u65B0\u5EFA\u4F1A\u8BDD</button>
5581
+ </div>
5582
+ <div id="session-list">
5583
+ <div class="sidebar-empty">\u52A0\u8F7D\u4E2D\u2026</div>
5584
+ </div>
5585
+ </div>
5586
+
5587
+ <!-- Right: chat area -->
5588
+ <div id="chat-area">
5589
+ <div id="chat-header">
5590
+ <span id="chat-title">\u9009\u62E9\u6216\u65B0\u5EFA\u4F1A\u8BDD</span>
5591
+ <div id="status-indicator">
5592
+ <div id="status-dot"></div>
5593
+ <span id="status-text">idle</span>
5594
+ </div>
5595
+ </div>
5596
+ <div id="messages">
5597
+ <div class="empty-chat">\u2190 \u4ECE\u5DE6\u4FA7\u9009\u62E9\u4F1A\u8BDD\uFF0C\u6216\u70B9\u51FB"\u65B0\u5EFA\u4F1A\u8BDD"\u5F00\u59CB</div>
5598
+ </div>
5599
+ <div id="ws-status" class="ws-status"></div>
5600
+ <div id="input-bar">
5601
+ <textarea id="msg-input" rows="1" placeholder="\u8F93\u5165\u6D88\u606F\uFF0C\u6309 Enter \u53D1\u9001\uFF08Shift+Enter \u6362\u884C\uFF09\u2026" disabled></textarea>
5602
+ <button id="send-btn" disabled>\u53D1\u9001</button>
5603
+ </div>
5604
+ </div>
5605
+ </div>
5606
+
5607
+ <!-- Bash approval modal -->
5608
+ <div id="approval-modal" role="dialog" aria-modal="true">
5609
+ <div id="approval-box">
5610
+ <div id="approval-title">\u26A0 \u8BF7\u6C42 Bash \u6267\u884C\u6743\u9650</div>
5611
+ <pre id="approval-prompt"></pre>
5612
+ <div id="approval-actions">
5613
+ <button id="deny-btn">\u62D2\u7EDD</button>
5614
+ <button id="approve-btn">\u5141\u8BB8</button>
5615
+ </div>
5616
+ </div>
5617
+ </div>
5618
+
5619
+ <script>
5620
+ // \u2500\u2500 State \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
5621
+ const state = {
5622
+ token: new URLSearchParams(location.search).get('token') || '',
5623
+ sessions: [],
5624
+ activeSessionId: null,
5625
+ messages: [],
5626
+ ws: null,
5627
+ pendingApproval: null,
5628
+ status: 'idle', // idle | thinking | tool_calling | awaiting_confirm
5629
+ wsRetries: 0,
5630
+ streamingMsgEl: null, // DOM element currently receiving delta tokens
5631
+ };
5632
+
5633
+ // \u2500\u2500 Helpers \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
5634
+ function apiUrl(path) {
5635
+ const sep = path.includes('?') ? '&' : '?';
5636
+ return path + sep + 'token=' + encodeURIComponent(state.token);
5637
+ }
5638
+
5639
+ async function apiFetch(path, opts) {
5640
+ const url = apiUrl(path);
5641
+ const res = await fetch(url, opts);
5642
+ if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
5643
+ return res.json();
5644
+ }
5645
+
5646
+ function wsUrl(sessionId) {
5647
+ const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
5648
+ return proto + '//' + location.host + '/api/ws/sessions/' + sessionId + '?token=' + encodeURIComponent(state.token);
5649
+ }
5650
+
5651
+ // Basic markdown \u2192 HTML (no libs)
5652
+ function renderMarkdown(text) {
5653
+ // Escape HTML first
5654
+ let s = text
5655
+ .replace(/&/g, '&amp;')
5656
+ .replace(/</g, '&lt;')
5657
+ .replace(/>/g, '&gt;');
5658
+ // Code blocks
5659
+ s = s.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, (_, code) =>
5660
+ '<pre><code>' + code.trim() + '</code></pre>');
5661
+ // Inline code
5662
+ s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
5663
+ // Bold
5664
+ s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
5665
+ // Paragraph breaks
5666
+ s = s.replace(/\\n\\n+/g, '</p><p>');
5667
+ return '<p>' + s + '</p>';
5668
+ }
5669
+
5670
+ // \u2500\u2500 Status \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
5671
+ function setStatus(status) {
5672
+ state.status = status;
5673
+ const dot = document.getElementById('status-dot');
5674
+ const txt = document.getElementById('status-text');
5675
+ dot.className = '';
5676
+ if (status === 'idle') {
5677
+ dot.style.background = '#3fb950';
5678
+ txt.textContent = 'idle';
5679
+ } else if (status === 'thinking') {
5680
+ dot.className = 'thinking';
5681
+ dot.style.background = '';
5682
+ txt.textContent = 'thinking\u2026';
5683
+ } else if (status === 'tool_calling') {
5684
+ dot.className = 'thinking';
5685
+ dot.style.background = '';
5686
+ txt.textContent = 'tool\u2026';
5687
+ } else if (status === 'awaiting_confirm') {
5688
+ dot.style.background = '#d29922';
5689
+ txt.textContent = 'awaiting approval';
5690
+ }
5691
+ const inputEl = document.getElementById('msg-input');
5692
+ const sendEl = document.getElementById('send-btn');
5693
+ const canType = status === 'idle' && state.activeSessionId;
5694
+ inputEl.disabled = !canType;
5695
+ sendEl.disabled = !canType;
5696
+ }
5697
+
5698
+ // \u2500\u2500 Sidebar \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
5699
+ function renderSidebar() {
5700
+ const listEl = document.getElementById('session-list');
5701
+ if (!state.sessions.length) {
5702
+ listEl.innerHTML = '<div class="sidebar-empty">\u6682\u65E0\u4F1A\u8BDD</div>';
5703
+ return;
5704
+ }
5705
+ listEl.innerHTML = state.sessions.map(s => {
5706
+ const active = s.id === state.activeSessionId ? ' active' : '';
5707
+ const title = (s.title || '(\u65E0\u6807\u9898)').replace(/</g, '&lt;');
5708
+ const id = s.id.slice(0, 8);
5709
+ return '<div class="session-item' + active + '" data-id="' + s.id + '">' +
5710
+ '<div class="s-title">' + title + '</div>' +
5711
+ '<div class="s-id">' + id + '\u2026</div>' +
5712
+ '</div>';
5713
+ }).join('');
5714
+ listEl.querySelectorAll('.session-item').forEach(el => {
5715
+ el.addEventListener('click', () => selectSession(el.dataset.id));
5716
+ });
5717
+ }
5718
+
5719
+ async function loadSessions() {
5720
+ try {
5721
+ const data = await apiFetch('/api/sessions');
5722
+ state.sessions = data.sessions || data.data || data || [];
5723
+ renderSidebar();
5724
+ } catch (e) {
5725
+ document.getElementById('session-list').innerHTML =
5726
+ '<div class="sidebar-empty" style="color:#f85149">\u52A0\u8F7D\u5931\u8D25</div>';
5727
+ }
5728
+ }
5729
+
5730
+ // \u2500\u2500 Messages \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
5731
+ function clearMessages() {
5732
+ document.getElementById('messages').innerHTML =
5733
+ '<div class="empty-chat">\u2190 \u52A0\u8F7D\u4F1A\u8BDD\u4E2D\u2026</div>';
5734
+ state.messages = [];
5735
+ state.streamingMsgEl = null;
5736
+ }
5737
+
5738
+ function appendMessage(role, htmlContent, opts) {
5739
+ const msgsEl = document.getElementById('messages');
5740
+ // Remove empty-chat placeholder
5741
+ const placeholder = msgsEl.querySelector('.empty-chat');
5742
+ if (placeholder) placeholder.remove();
5743
+
5744
+ const msgEl = document.createElement('div');
5745
+ msgEl.className = 'msg';
5746
+ const prefix = role === 'user' ? '[user]' : role === 'tool' ? '[tool]' : '[assistant]';
5747
+ msgEl.innerHTML =
5748
+ '<div class="msg-header ' + role + '">' + prefix + '</div>' +
5749
+ '<div class="msg-body">' + htmlContent + '</div>';
5750
+ if (opts && opts.streaming) {
5751
+ msgEl.querySelector('.msg-body').classList.add('cursor-blink');
5752
+ }
5753
+ msgsEl.appendChild(msgEl);
5754
+ msgsEl.scrollTop = msgsEl.scrollHeight;
5755
+ return msgEl;
5756
+ }
5757
+
5758
+ function finalizeStreamingMsg() {
5759
+ if (state.streamingMsgEl) {
5760
+ const body = state.streamingMsgEl.querySelector('.msg-body');
5761
+ if (body) body.classList.remove('cursor-blink');
5762
+ state.streamingMsgEl = null;
5763
+ }
5764
+ }
5765
+
5766
+ // \u2500\u2500 WebSocket \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
5767
+ function connectWs(sessionId) {
5768
+ if (state.ws) {
5769
+ state.ws.onclose = null;
5770
+ state.ws.close();
5771
+ state.ws = null;
5772
+ }
5773
+ state.wsRetries = 0;
5774
+ openWs(sessionId);
5775
+ }
5776
+
5777
+ function openWs(sessionId) {
5778
+ const url = wsUrl(sessionId);
5779
+ const ws = new WebSocket(url);
5780
+ state.ws = ws;
5781
+
5782
+ document.getElementById('ws-status').textContent = '\u8FDE\u63A5\u4E2D\u2026';
5783
+
5784
+ ws.onopen = () => {
5785
+ document.getElementById('ws-status').textContent = '';
5786
+ state.wsRetries = 0;
5787
+ setStatus('idle');
5788
+ };
5789
+
5790
+ ws.onmessage = (evt) => {
5791
+ let msg;
5792
+ try { msg = JSON.parse(evt.data); } catch { return; }
5793
+ handleWsEvent(msg);
5794
+ };
5795
+
5796
+ ws.onerror = () => {
5797
+ document.getElementById('ws-status').textContent = 'WebSocket \u9519\u8BEF';
5798
+ };
5799
+
5800
+ ws.onclose = () => {
5801
+ document.getElementById('ws-status').textContent = 'WebSocket \u5DF2\u65AD\u5F00';
5802
+ finalizeStreamingMsg();
5803
+ if (state.activeSessionId === sessionId && state.wsRetries < 5) {
5804
+ state.wsRetries++;
5805
+ setTimeout(() => openWs(sessionId), 3000);
5806
+ }
5807
+ };
5808
+ }
5809
+
5810
+ function handleWsEvent(msg) {
5811
+ const type = msg.type || msg.event;
5812
+ if (type === 'message.delta') {
5813
+ const token = msg.delta || msg.content || '';
5814
+ if (!state.streamingMsgEl) {
5815
+ state.streamingMsgEl = appendMessage('assistant', escHtml(token), { streaming: true });
5816
+ } else {
5817
+ const body = state.streamingMsgEl.querySelector('.msg-body');
5818
+ body.textContent += token;
5819
+ document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
5820
+ }
5821
+ setStatus('thinking');
5822
+ } else if (type === 'message.completed') {
5823
+ finalizeStreamingMsg();
5824
+ setStatus('idle');
5825
+ } else if (type === 'tool.started') {
5826
+ const name = msg.tool || msg.name || 'tool';
5827
+ const msgsEl = document.getElementById('messages');
5828
+ const placeholder = msgsEl.querySelector('.empty-chat');
5829
+ if (placeholder) placeholder.remove();
5830
+ const boxEl = document.createElement('div');
5831
+ boxEl.className = 'tool-box';
5832
+ boxEl.setAttribute('data-tool-id', msg.id || name);
5833
+ boxEl.innerHTML =
5834
+ '<div class="tool-box-header">\u250C\u2500 ' + escHtml(name) + ' \u2500</div>' +
5835
+ '<div class="tool-box-body">\u2026</div>';
5836
+ msgsEl.appendChild(boxEl);
5837
+ msgsEl.scrollTop = msgsEl.scrollHeight;
5838
+ setStatus('tool_calling');
5839
+ } else if (type === 'tool.completed') {
5840
+ const toolId = msg.id || msg.tool || msg.name || '';
5841
+ const boxEl = document.querySelector('[data-tool-id="' + toolId + '"]');
5842
+ if (boxEl) {
5843
+ const body = boxEl.querySelector('.tool-box-body');
5844
+ body.textContent = msg.output || msg.result || '(\uC644\uB8CC)';
5845
+ boxEl.querySelector('.tool-box-header').textContent =
5846
+ '\u2514\u2500 ' + (msg.tool || msg.name || toolId) + ' \u2500';
5847
+ }
5848
+ setStatus('idle');
5849
+ } else if (type === 'approval.requested') {
5850
+ state.pendingApproval = msg;
5851
+ showApprovalModal(msg);
5852
+ setStatus('awaiting_confirm');
5853
+ } else if (type === 'approval.resolved') {
5854
+ hideApprovalModal();
5855
+ state.pendingApproval = null;
5856
+ setStatus('thinking');
5857
+ } else if (type === 'session.idle') {
5858
+ finalizeStreamingMsg();
5859
+ setStatus('idle');
5860
+ } else if (type === 'session.error') {
5861
+ finalizeStreamingMsg();
5862
+ setStatus('idle');
5863
+ document.getElementById('ws-status').textContent =
5864
+ '\u9519\u8BEF: ' + escHtml(msg.error || 'unknown error');
5865
+ }
5866
+ }
5867
+
5868
+ function escHtml(s) {
5869
+ return String(s)
5870
+ .replace(/&/g, '&amp;')
5871
+ .replace(/</g, '&lt;')
5872
+ .replace(/>/g, '&gt;');
5873
+ }
5874
+
5875
+ // \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
5876
+ function showApprovalModal(msg) {
5877
+ const box = document.getElementById('approval-box');
5878
+ box.className = msg.kind === 'danger' ? 'danger' : '';
5879
+ document.getElementById('approval-prompt').textContent =
5880
+ msg.prompt || msg.command || msg.text || JSON.stringify(msg);
5881
+ document.getElementById('approval-modal').classList.add('visible');
5882
+ }
5883
+
5884
+ function hideApprovalModal() {
5885
+ document.getElementById('approval-modal').classList.remove('visible');
5886
+ }
5887
+
5888
+ async function sendApproval(approved) {
5889
+ const pending = state.pendingApproval;
5890
+ if (!pending || !state.activeSessionId) return;
5891
+ hideApprovalModal();
5892
+ state.pendingApproval = null;
5893
+ try {
5894
+ await apiFetch('/api/chat/sessions/' + state.activeSessionId + '/approve', {
5895
+ method: 'POST',
5896
+ headers: { 'Content-Type': 'application/json' },
5897
+ body: JSON.stringify({ requestId: pending.requestId || pending.id, approved }),
5898
+ });
5899
+ } catch (e) {
5900
+ document.getElementById('ws-status').textContent = '\u5BA1\u6279\u8BF7\u6C42\u5931\u8D25: ' + e.message;
5901
+ }
5902
+ }
5903
+
5904
+ document.getElementById('approve-btn').addEventListener('click', () => sendApproval(true));
5905
+ document.getElementById('deny-btn').addEventListener('click', () => sendApproval(false));
5906
+
5907
+ // \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
5908
+ function selectSession(id) {
5909
+ state.activeSessionId = id;
5910
+ const session = state.sessions.find(s => s.id === id);
5911
+ document.getElementById('chat-title').textContent =
5912
+ session ? (session.title || id.slice(0, 12) + '\u2026') : id;
5913
+ clearMessages();
5914
+ renderSidebar();
5915
+ setStatus('idle');
5916
+ connectWs(id);
5917
+ // Close sidebar on mobile
5918
+ document.getElementById('sidebar').classList.remove('open');
5919
+ }
5920
+
5921
+ // \u2500\u2500 New session \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
5922
+ document.getElementById('new-session-btn').addEventListener('click', async () => {
5923
+ try {
5924
+ const data = await apiFetch('/api/chat/sessions', {
5925
+ method: 'POST',
5926
+ headers: { 'Content-Type': 'application/json' },
5927
+ body: JSON.stringify({}),
5928
+ });
5929
+ const session = data.session || data;
5930
+ if (session && session.id) {
5931
+ state.sessions.unshift(session);
5932
+ renderSidebar();
5933
+ selectSession(session.id);
5934
+ }
5935
+ } catch (e) {
5936
+ document.getElementById('ws-status').textContent = '\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25: ' + e.message;
5937
+ }
5938
+ });
5939
+
5940
+ // \u2500\u2500 Send message \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
5941
+ async function sendMessage() {
5942
+ if (state.status !== 'idle' || !state.activeSessionId) return;
5943
+ const inputEl = document.getElementById('msg-input');
5944
+ const text = inputEl.value.trim();
5945
+ if (!text) return;
5946
+ inputEl.value = '';
5947
+ inputEl.style.height = 'auto';
5948
+ appendMessage('user', escHtml(text));
5949
+ setStatus('thinking');
5950
+ try {
5951
+ await apiFetch('/api/chat/sessions/' + state.activeSessionId + '/submit', {
5952
+ method: 'POST',
5953
+ headers: { 'Content-Type': 'application/json' },
5954
+ body: JSON.stringify({ message: text }),
5955
+ });
5956
+ } catch (e) {
5957
+ document.getElementById('ws-status').textContent = '\u53D1\u9001\u5931\u8D25: ' + e.message;
5958
+ setStatus('idle');
5959
+ }
5960
+ }
5961
+
5962
+ document.getElementById('send-btn').addEventListener('click', sendMessage);
5963
+
5964
+ document.getElementById('msg-input').addEventListener('keydown', (e) => {
5965
+ if (e.key === 'Enter' && !e.shiftKey) {
5966
+ e.preventDefault();
5967
+ sendMessage();
5968
+ }
5969
+ });
5970
+
5971
+ // Auto-resize textarea
5972
+ document.getElementById('msg-input').addEventListener('input', function() {
5973
+ this.style.height = 'auto';
5974
+ this.style.height = Math.min(this.scrollHeight, 120) + 'px';
5975
+ });
5976
+
5977
+ // \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
5978
+ document.getElementById('hamburger').addEventListener('click', () => {
5979
+ document.getElementById('sidebar').classList.toggle('open');
5980
+ });
5981
+
5982
+ // \u2500\u2500 Init \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\u2500
5983
+ loadSessions();
5984
+ </script>
5985
+ </body>
5986
+ </html>`;
5987
+ }
5988
+
5247
5989
  // src/web/routes/status.ts
5248
5990
  async function statusRoutes(app, opts) {
5249
5991
  app.get(
@@ -5551,6 +6293,10 @@ async function buildServer(opts) {
5551
6293
  authHook(request, reply, done);
5552
6294
  });
5553
6295
  app.get("/health", async () => ({ ok: true }));
6296
+ const adminHtml = generateAdminHtml(opts.version);
6297
+ app.get("/", async (_req, reply) => {
6298
+ return reply.type("text/html").send(adminHtml);
6299
+ });
5554
6300
  await app.register(statusRoutes, {
5555
6301
  config: opts.config,
5556
6302
  manager: opts.manager,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.5.18",
3
+ "version": "0.5.20",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",