@zhongqian97-code/ecode 0.5.19 → 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 +706 -93
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5255,119 +5255,732 @@ function generateAdminHtml(version2) {
5255
5255
  <style>
5256
5256
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
5257
5257
  body {
5258
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
5259
- background: #0d1117; color: #c9d1d9; min-height: 100vh; padding: 24px;
5260
- }
5261
- header {
5262
- display: flex; align-items: center; gap: 12px;
5263
- padding-bottom: 16px; border-bottom: 1px solid #21262d; margin-bottom: 24px;
5264
- }
5265
- header h1 { font-size: 18px; font-weight: 600; color: #e6edf3; }
5266
- header .version { font-size: 12px; color: #8b949e; background: #21262d;
5267
- padding: 2px 8px; border-radius: 20px; }
5268
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }
5269
- .card {
5270
- background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px;
5271
- }
5272
- .card h2 { font-size: 13px; font-weight: 600; color: #8b949e;
5273
- text-transform: uppercase; letter-spacing: .05em; margin-bottom: 12px; }
5274
- .stat { display: flex; justify-content: space-between; align-items: center;
5275
- padding: 6px 0; border-bottom: 1px solid #21262d; font-size: 13px; }
5276
- .stat:last-child { border-bottom: none; }
5277
- .stat .label { color: #8b949e; }
5278
- .stat .value { color: #e6edf3; font-weight: 500; }
5279
- .badge { display: inline-block; padding: 2px 8px; border-radius: 20px;
5280
- font-size: 11px; font-weight: 600; }
5281
- .badge.ok { background: #1a4731; color: #3fb950; }
5282
- .badge.err { background: #4d1f24; color: #f85149; }
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
+ }
5283
5324
  .session-item {
5284
- padding: 8px 0; border-bottom: 1px solid #21262d; font-size: 13px;
5285
- }
5286
- .session-item:last-child { border-bottom: none; }
5287
- .session-id { color: #79c0ff; font-family: monospace; font-size: 12px; }
5288
- .session-title { color: #e6edf3; margin-top: 2px; }
5289
- .empty { color: #8b949e; font-size: 13px; font-style: italic; }
5290
- .error { color: #f85149; font-size: 13px; }
5291
- .loading { color: #8b949e; font-size: 13px; font-style: italic; }
5292
- .api-row { font-size: 12px; font-family: monospace; padding: 4px 0;
5293
- border-bottom: 1px solid #21262d; display: flex; gap: 8px; }
5294
- .api-row:last-child { border-bottom: none; }
5295
- .method { color: #79c0ff; min-width: 50px; }
5296
- .path { color: #e6edf3; }
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
+ }
5297
5567
  </style>
5298
5568
  </head>
5299
5569
  <body>
5300
- <header>
5570
+ <div id="topbar">
5571
+ <button id="hamburger" aria-label="Toggle sidebar">\u2630</button>
5301
5572
  <h1>\u26A1 ecode web admin</h1>
5302
5573
  <span class="version">v${version2}</span>
5303
- </header>
5574
+ </div>
5304
5575
 
5305
- <div class="grid">
5306
- <div class="card">
5307
- <h2>\u670D\u52A1\u72B6\u6001</h2>
5308
- <div id="status-content"><span class="loading">\u52A0\u8F7D\u4E2D\u2026</span></div>
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>
5309
5585
  </div>
5310
5586
 
5311
- <div class="card">
5312
- <h2>\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD</h2>
5313
- <div id="sessions-content"><span class="loading">\u52A0\u8F7D\u4E2D\u2026</span></div>
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>
5314
5604
  </div>
5605
+ </div>
5315
5606
 
5316
- <div class="card">
5317
- <h2>API \u7AEF\u70B9</h2>
5318
- <div class="api-row"><span class="method">GET</span><span class="path">/api/status</span></div>
5319
- <div class="api-row"><span class="method">GET</span><span class="path">/api/sessions</span></div>
5320
- <div class="api-row"><span class="method">GET</span><span class="path">/api/config</span></div>
5321
- <div class="api-row"><span class="method">GET</span><span class="path">/api/automation/jobs</span></div>
5322
- <div class="api-row"><span class="method">POST</span><span class="path">/api/chat/sessions</span></div>
5323
- <div class="api-row"><span class="method">WS</span><span class="path">/api/ws/sessions/:id</span></div>
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>
5324
5616
  </div>
5325
5617
  </div>
5326
5618
 
5327
5619
  <script>
5328
- const token = new URLSearchParams(location.search).get('token') || '';
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
+ };
5329
5632
 
5330
- async function apiFetch(path) {
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) {
5331
5635
  const sep = path.includes('?') ? '&' : '?';
5332
- const r = await fetch(path + sep + 'token=' + encodeURIComponent(token));
5333
- if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
5334
- return r.json();
5335
- }
5336
-
5337
- function setHtml(id, html) {
5338
- document.getElementById(id).innerHTML = html;
5339
- }
5340
-
5341
- // \u72B6\u6001\u5361
5342
- apiFetch('/api/status').then(d => {
5343
- const rows = [
5344
- ['\u72B6\u6001', '<span class="badge ok">\u8FD0\u884C\u4E2D</span>'],
5345
- ['\u7248\u672C', d.version ?? '-'],
5346
- ['\u6D3B\u8DC3\u4F1A\u8BDD', d.activeSessions ?? 0],
5347
- ];
5348
- setHtml('status-content',
5349
- rows.map(([l,v]) =>
5350
- '<div class="stat"><span class="label">' + l + '</span><span class="value">' + v + '</span></div>'
5351
- ).join('')
5352
- );
5353
- }).catch(e => setHtml('status-content', '<span class="error">' + e.message + '</span>'));
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
+ }
5354
5650
 
5355
- // \u4F1A\u8BDD\u5217\u8868
5356
- apiFetch('/api/sessions').then(d => {
5357
- const sessions = d.sessions ?? d.data ?? d ?? [];
5358
- if (!sessions.length) {
5359
- setHtml('sessions-content', '<span class="empty">\u6682\u65E0\u4F1A\u8BDD</span>');
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>';
5360
5703
  return;
5361
5704
  }
5362
- setHtml('sessions-content',
5363
- sessions.slice(0, 10).map(s =>
5364
- '<div class="session-item">' +
5365
- '<div class="session-id">' + s.id + '</div>' +
5366
- '<div class="session-title">' + (s.title || '(\u65E0\u6807\u9898)') + '</div>' +
5367
- '</div>'
5368
- ).join('')
5369
- );
5370
- }).catch(e => setHtml('sessions-content', '<span class="error">' + e.message + '</span>'));
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();
5371
5984
  </script>
5372
5985
  </body>
5373
5986
  </html>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.5.19",
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",