@zhongqian97-code/ecode 0.5.21 → 0.5.23

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 +139 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5250,7 +5250,7 @@ function generateAdminHtml(version2) {
5250
5250
  <html lang="zh">
5251
5251
  <head>
5252
5252
  <meta charset="UTF-8">
5253
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
5253
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
5254
5254
  <title>ecode web admin</title>
5255
5255
  <style>
5256
5256
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -5567,6 +5567,13 @@ function generateAdminHtml(version2) {
5567
5567
  .modal-footer { display:flex; gap:8px; justify-content:flex-end; margin-top:16px; }
5568
5568
  #upgrade-output { display:none; background:#0d1117; border:1px solid #30363d; border-radius:4px; padding:8px; font-family:monospace; font-size:12px; color:#c9d1d9; white-space:pre-wrap; max-height:200px; overflow-y:auto; margin-top:8px; }
5569
5569
 
5570
+ /* \u2500\u2500 Syntax highlight \u2500\u2500 */
5571
+ .kw { color: #ff7b72; }
5572
+ .str { color: #a5d6ff; }
5573
+ .cmt { color: #8b949e; font-style: italic; }
5574
+ .num { color: #79c0ff; }
5575
+ .fn { color: #d2a8ff; }
5576
+
5570
5577
  /* \u2500\u2500 Mobile \u2500\u2500 */
5571
5578
  @media (max-width: 600px) {
5572
5579
  #hamburger { display: block; }
@@ -5586,6 +5593,7 @@ function generateAdminHtml(version2) {
5586
5593
  <button id="hamburger" aria-label="Toggle sidebar">\u2630</button>
5587
5594
  <h1>\u26A1 ecode web admin</h1>
5588
5595
  <span class="version">v${version2}</span>
5596
+ <span id="topbar-model" class="version" style="display:none"></span>
5589
5597
  <button id="config-btn" class="btn">\u914D\u7F6E</button>
5590
5598
  <button id="upgrade-btn" class="btn">\u5347\u7EA7</button>
5591
5599
  </div>
@@ -5694,6 +5702,21 @@ function generateAdminHtml(version2) {
5694
5702
  return proto + '//' + location.host + '/api/ws/sessions/' + sessionId + '?token=' + encodeURIComponent(state.token);
5695
5703
  }
5696
5704
 
5705
+ // \u2500\u2500 Code highlighting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5706
+ function highlightCode(code) {
5707
+ let s = escHtml(code);
5708
+ // keywords
5709
+ s = s.replace(/\b(function|const|let|var|return|if|else|for|while|class|import|export|from|async|await|new|this|typeof|instanceof|null|undefined|true|false)\b/g, '<span class="kw">$1</span>');
5710
+ // strings (single, double \u2014 after HTML escaping, quotes appear as &#39; or &quot;)
5711
+ s = s.replace(/(&#39;[^&#]*&#39;|&quot;[^&]*&quot;)/g, '<span class="str">$1</span>');
5712
+ // comments
5713
+ s = s.replace(/(//[^
5714
+ ]*)/g, '<span class="cmt">$1</span>');
5715
+ // numbers
5716
+ s = s.replace(/\b(d+.?d*)\b/g, '<span class="num">$1</span>');
5717
+ return s;
5718
+ }
5719
+
5697
5720
  // Basic markdown \u2192 HTML (no libs)
5698
5721
  function renderMarkdown(text) {
5699
5722
  // Escape HTML first
@@ -5701,9 +5724,9 @@ function generateAdminHtml(version2) {
5701
5724
  .replace(/&/g, '&amp;')
5702
5725
  .replace(/</g, '&lt;')
5703
5726
  .replace(/>/g, '&gt;');
5704
- // Code blocks
5727
+ // Code blocks (use highlightCode for syntax coloring)
5705
5728
  s = s.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, (_, code) =>
5706
- '<pre><code>' + code.trim() + '</code></pre>');
5729
+ '<pre><code>' + highlightCode(code.trim()) + '</code></pre>');
5707
5730
  // Inline code
5708
5731
  s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
5709
5732
  // Bold
@@ -5781,6 +5804,31 @@ function generateAdminHtml(version2) {
5781
5804
  state.streamingMsgEl = null;
5782
5805
  }
5783
5806
 
5807
+ async function loadMessages(sessionId) {
5808
+ try {
5809
+ const sep = '/api/sessions/' + sessionId + '/messages?token=' + encodeURIComponent(state.token);
5810
+ const res = await fetch(sep);
5811
+ if (!res.ok) return;
5812
+ const text = await res.text();
5813
+ const lines = text.split('\\n').filter(l => l.trim());
5814
+ const msgsEl = document.getElementById('messages');
5815
+ msgsEl.innerHTML = '';
5816
+ for (const line of lines) {
5817
+ try {
5818
+ const m = JSON.parse(line);
5819
+ if (m.content) {
5820
+ appendMessage(m.role === 'user' ? 'user' : 'assistant', renderMarkdown(m.content));
5821
+ }
5822
+ } catch { /* skip malformed lines */ }
5823
+ }
5824
+ if (!lines.length) {
5825
+ msgsEl.innerHTML = '<div class="empty-chat">\u6682\u65E0\u5386\u53F2\u6D88\u606F\uFF0C\u5F00\u59CB\u5BF9\u8BDD\u5427</div>';
5826
+ }
5827
+ } catch (e) {
5828
+ // silently ignore \u2014 session might have no log file yet
5829
+ }
5830
+ }
5831
+
5784
5832
  function appendMessage(role, htmlContent, opts) {
5785
5833
  const msgsEl = document.getElementById('messages');
5786
5834
  // Remove empty-chat placeholder
@@ -5848,7 +5896,12 @@ function generateAdminHtml(version2) {
5848
5896
  finalizeStreamingMsg();
5849
5897
  if (state.activeSessionId === sessionId && state.wsRetries < 5) {
5850
5898
  state.wsRetries++;
5851
- setTimeout(() => openWs(sessionId), 3000);
5899
+ const wsRetryDelay = Math.min(1000 * Math.pow(2, state.wsRetries), 30000);
5900
+ document.getElementById('ws-status').textContent =
5901
+ '\u91CD\u8FDE\u4E2D\u2026 (' + state.wsRetries + '/5\uFF0C' + (wsRetryDelay/1000).toFixed(0) + 's\u540E)';
5902
+ setTimeout(() => openWs(sessionId), wsRetryDelay);
5903
+ } else if (state.wsRetries >= 5) {
5904
+ document.getElementById('ws-status').textContent = '\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u9875\u9762';
5852
5905
  }
5853
5906
  };
5854
5907
  }
@@ -5957,6 +6010,7 @@ function generateAdminHtml(version2) {
5957
6010
  document.getElementById('chat-title').textContent =
5958
6011
  session ? (session.title || id.slice(0, 12) + '\u2026') : id;
5959
6012
  clearMessages();
6013
+ loadMessages(id);
5960
6014
  renderSidebar();
5961
6015
  setStatus('idle');
5962
6016
  connectWs(id);
@@ -6054,6 +6108,7 @@ function generateAdminHtml(version2) {
6054
6108
  if (apiKeyVal) body.apiKey = apiKeyVal;
6055
6109
  try {
6056
6110
  await apiFetch('/api/config', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
6111
+ updateTopbarModel(body.model);
6057
6112
  } catch (e) {
6058
6113
  // ignore errors silently for now
6059
6114
  }
@@ -6113,8 +6168,43 @@ function generateAdminHtml(version2) {
6113
6168
  }
6114
6169
  });
6115
6170
 
6171
+ // \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
6172
+ document.getElementById('msg-input').addEventListener('focus', () => {
6173
+ setTimeout(() => {
6174
+ document.getElementById('msg-input').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
6175
+ }, 300);
6176
+ });
6177
+
6178
+ // \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
6179
+ window.onerror = function(msg, src, line) {
6180
+ const wsStatusEl = document.getElementById('ws-status');
6181
+ if (wsStatusEl) wsStatusEl.textContent = '\u9519\u8BEF: ' + msg;
6182
+ return false;
6183
+ };
6184
+
6116
6185
  // \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
6186
+ function updateTopbarModel(model) {
6187
+ const el = document.getElementById('topbar-model');
6188
+ if (!el) return;
6189
+ if (model) {
6190
+ el.textContent = model;
6191
+ el.style.display = '';
6192
+ } else {
6193
+ el.style.display = 'none';
6194
+ }
6195
+ }
6196
+
6197
+ async function initModel() {
6198
+ try {
6199
+ const data = await apiFetch('/api/config');
6200
+ updateTopbarModel(data.model);
6201
+ } catch {
6202
+ // non-critical, ignore
6203
+ }
6204
+ }
6205
+
6117
6206
  loadSessions();
6207
+ initModel();
6118
6208
  </script>
6119
6209
  </body>
6120
6210
  </html>`;
@@ -6142,33 +6232,48 @@ async function statusRoutes(app, opts) {
6142
6232
  import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
6143
6233
  import { join as join13 } from "path";
6144
6234
  async function sessionsRoutes(app, opts) {
6145
- app.get("/api/sessions", async (_request, reply) => {
6146
- if (!opts.config.logDir) {
6147
- return reply.send([]);
6148
- }
6149
- return listSessions(opts.config.logDir);
6235
+ app.get("/api/sessions", async (_request, _reply) => {
6236
+ const fileSessions = opts.config.logDir ? listSessions(opts.config.logDir) : [];
6237
+ if (!opts.manager) {
6238
+ return fileSessions;
6239
+ }
6240
+ const runningSnapshots = opts.manager.listRunning();
6241
+ const fileIds = new Set(fileSessions.map((s) => s.id));
6242
+ const runtimeSessions = runningSnapshots.filter((s) => !fileIds.has(s.id)).map((s) => ({
6243
+ id: s.id,
6244
+ title: s.title,
6245
+ model: s.model,
6246
+ status: s.status,
6247
+ turnCount: s.turnCount,
6248
+ totalTokens: s.totalTokens,
6249
+ startTime: s.startedAt,
6250
+ lastActivity: s.lastActivity,
6251
+ cwd: "",
6252
+ logFile: ""
6253
+ }));
6254
+ return [...fileSessions, ...runtimeSessions];
6150
6255
  });
6151
6256
  app.get(
6152
6257
  "/api/sessions/:id/messages",
6153
6258
  async (request, reply) => {
6154
6259
  const { id } = request.params;
6155
- if (!opts.config.logDir) {
6156
- return reply.code(404).send({ success: false, error: "Log directory not configured" });
6157
- }
6158
- const session = findSession(opts.config.logDir, id);
6159
- if (!session) {
6160
- return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
6161
- }
6162
- const logFilePath2 = join13(opts.config.logDir, session.logFile);
6163
- if (!existsSync4(logFilePath2)) {
6164
- return reply.code(404).send({ success: false, error: `Log file not found: ${session.logFile}` });
6260
+ if (opts.config.logDir) {
6261
+ const session = findSession(opts.config.logDir, id);
6262
+ if (session) {
6263
+ const logFilePath2 = join13(opts.config.logDir, session.logFile);
6264
+ if (existsSync4(logFilePath2)) {
6265
+ try {
6266
+ const content = readFileSync5(logFilePath2, "utf-8");
6267
+ return reply.header("Content-Type", "text/plain; charset=utf-8").send(content);
6268
+ } catch {
6269
+ }
6270
+ }
6271
+ }
6165
6272
  }
6166
- try {
6167
- const content = readFileSync5(logFilePath2, "utf-8");
6168
- return reply.header("Content-Type", "text/plain; charset=utf-8").send(content);
6169
- } catch (_err) {
6170
- return reply.code(404).send({ success: false, error: `Log file not found: ${session.logFile}` });
6273
+ if (opts.manager && opts.manager.getSession(id)) {
6274
+ return reply.header("Content-Type", "text/plain; charset=utf-8").send("");
6171
6275
  }
6276
+ return reply.code(404).send({ success: false, error: `Session not found: ${id}` });
6172
6277
  }
6173
6278
  );
6174
6279
  app.get(
@@ -6472,7 +6577,7 @@ async function buildServer(opts) {
6472
6577
  manager: opts.manager,
6473
6578
  version: opts.version
6474
6579
  });
6475
- await app.register(sessionsRoutes, { config: opts.config });
6580
+ await app.register(sessionsRoutes, { config: opts.config, manager: opts.manager });
6476
6581
  await app.register(configRoutes, { config: opts.config });
6477
6582
  await app.register(automationRoutes, { config: opts.config });
6478
6583
  await app.register(chatRoutes, { config: opts.config, manager: opts.manager });
@@ -6573,6 +6678,7 @@ var SessionRuntime = class {
6573
6678
  config;
6574
6679
  model;
6575
6680
  abortController = null;
6681
+ _stopAfterToolRound = false;
6576
6682
  _turnCount = 0;
6577
6683
  _totalTokens = 0;
6578
6684
  startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -6691,6 +6797,10 @@ var SessionRuntime = class {
6691
6797
  this.bus.emit({ type: "tool.completed", callId: tc.id, toolName: tc.name, output: toolResult });
6692
6798
  this.messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
6693
6799
  }
6800
+ if (this._stopAfterToolRound) {
6801
+ this._stopAfterToolRound = false;
6802
+ break;
6803
+ }
6694
6804
  } else {
6695
6805
  if (assistantText) {
6696
6806
  this.messages.push({
@@ -6725,7 +6835,10 @@ Proceed?`;
6725
6835
  this.bus.emit({ type: "approval.requested", requestId: reqId, kind, prompt });
6726
6836
  const approved = await promise;
6727
6837
  this._status = "tool_calling";
6728
- if (!approved) return SKIP_MESSAGE;
6838
+ if (!approved) {
6839
+ this._stopAfterToolRound = true;
6840
+ return SKIP_MESSAGE;
6841
+ }
6729
6842
  }
6730
6843
  if (signal.aborted) return SKIP_MESSAGE;
6731
6844
  const result = await executeBash(parsed.command);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.5.21",
3
+ "version": "0.5.23",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",