@zhongqian97-code/ecode 0.5.30 → 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 +255 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5700,6 +5700,21 @@ function generateAdminHtml(version2) {
5700
5700
  to { opacity: 1; transform: translateX(0); }
5701
5701
  }
5702
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
+
5703
5718
  /* \u2500\u2500 Mobile \u2500\u2500 */
5704
5719
  @media (max-width: 600px) {
5705
5720
  #hamburger { display: block; }
@@ -5711,6 +5726,22 @@ function generateAdminHtml(version2) {
5711
5726
  transition: transform .2s ease;
5712
5727
  }
5713
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
+ }
5714
5745
  }
5715
5746
  </style>
5716
5747
  </head>
@@ -5749,6 +5780,7 @@ function generateAdminHtml(version2) {
5749
5780
  <div class="empty-chat">\u2190 \u4ECE\u5DE6\u4FA7\u9009\u62E9\u4F1A\u8BDD\uFF0C\u6216\u70B9\u51FB"\u65B0\u5EFA\u4F1A\u8BDD"\u5F00\u59CB</div>
5750
5781
  </div>
5751
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>
5752
5784
  <div id="input-bar">
5753
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>
5754
5786
  <button id="send-btn" disabled>\u53D1\u9001</button>
@@ -5813,6 +5845,8 @@ function generateAdminHtml(version2) {
5813
5845
  wsRetries: 0,
5814
5846
  streamingMsgEl: null, // DOM element currently receiving delta tokens
5815
5847
  showTools: true,
5848
+ skills: [], // cached skills from /api/skills
5849
+ skillsLoaded: false,
5816
5850
  };
5817
5851
 
5818
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
@@ -6161,6 +6195,70 @@ function generateAdminHtml(version2) {
6161
6195
  .replace(/>/g, '&gt;');
6162
6196
  }
6163
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
+
6164
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
6165
6263
  function showApprovalModal(msg) {
6166
6264
  const box = document.getElementById('approval-box');
@@ -6260,17 +6358,57 @@ function generateAdminHtml(version2) {
6260
6358
 
6261
6359
  document.getElementById('send-btn').addEventListener('click', sendMessage);
6262
6360
 
6263
- 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
+ }
6264
6396
  if (e.key === 'Enter' && !e.shiftKey) {
6265
6397
  e.preventDefault();
6266
6398
  sendMessage();
6267
6399
  }
6268
6400
  });
6269
6401
 
6270
- // Auto-resize textarea
6402
+ // Auto-resize textarea + slash command dropdown
6271
6403
  document.getElementById('msg-input').addEventListener('input', function() {
6272
6404
  this.style.height = 'auto';
6273
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
+ }
6274
6412
  });
6275
6413
 
6276
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
@@ -6382,6 +6520,13 @@ function generateAdminHtml(version2) {
6382
6520
  });
6383
6521
  });
6384
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
+
6385
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
6386
6531
  document.getElementById('msg-input').addEventListener('focus', () => {
6387
6532
  setTimeout(() => {
@@ -6389,6 +6534,15 @@ function generateAdminHtml(version2) {
6389
6534
  }, 300);
6390
6535
  });
6391
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
+
6392
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
6393
6547
  window.onerror = function(msg, src, line) {
6394
6548
  const wsStatusEl = document.getElementById('ws-status');
@@ -6664,6 +6818,9 @@ async function chatRoutes(app, opts) {
6664
6818
  if (typeof body.systemPrompt === "string") {
6665
6819
  sessionOpts.systemPrompt = body.systemPrompt;
6666
6820
  }
6821
+ if (opts.autoApprove) {
6822
+ sessionOpts.autoApproveNormal = true;
6823
+ }
6667
6824
  if (!opts.config.apiKey) {
6668
6825
  return reply.code(400).send({
6669
6826
  success: false,
@@ -6828,6 +6985,80 @@ async function systemRoutes(app, opts) {
6828
6985
  });
6829
6986
  }
6830
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
+
6831
7062
  // src/web/server.ts
6832
7063
  async function buildServer(opts) {
6833
7064
  const app = Fastify({ logger: false });
@@ -6854,8 +7085,9 @@ async function buildServer(opts) {
6854
7085
  await app.register(sessionsRoutes, { config: opts.config, manager: opts.manager });
6855
7086
  await app.register(configRoutes, { config: opts.config, save: saveConfig });
6856
7087
  await app.register(automationRoutes, { config: opts.config });
6857
- await app.register(chatRoutes, { config: opts.config, manager: opts.manager });
7088
+ await app.register(chatRoutes, { config: opts.config, manager: opts.manager, autoApprove: opts.autoApprove });
6858
7089
  await app.register(systemRoutes, { version: opts.version });
7090
+ await app.register(skillsRoutes);
6859
7091
  await app.register(sessionHubRoutes, { manager: opts.manager });
6860
7092
  return app;
6861
7093
  }
@@ -6951,6 +7183,7 @@ var SessionRuntime = class {
6951
7183
  messages;
6952
7184
  config;
6953
7185
  model;
7186
+ autoApproveNormal;
6954
7187
  abortController = null;
6955
7188
  _stopAfterToolRound = false;
6956
7189
  _turnCount = 0;
@@ -6963,6 +7196,7 @@ var SessionRuntime = class {
6963
7196
  this.config = config2;
6964
7197
  this.llm = opts.llm ?? createProvider(resolveActiveProfile(config2));
6965
7198
  this.model = config2.model;
7199
+ this.autoApproveNormal = opts.autoApproveNormal ?? false;
6966
7200
  const systemContent = opts.systemPrompt ?? (config2.systemPrompt ?? DEFAULT_SYSTEM_PROMPT);
6967
7201
  this.messages = systemContent ? [{ role: "system", content: systemContent }] : [];
6968
7202
  if (opts.initialMessages) {
@@ -7101,17 +7335,20 @@ var SessionRuntime = class {
7101
7335
  const parsed = JSON.parse(args);
7102
7336
  const cls = classifyCommand(parsed.command, this.config.dangerousPatterns);
7103
7337
  if (cls === "normal" || cls === "danger") {
7104
- this._status = "awaiting_confirm";
7105
- const kind = cls === "danger" ? "danger" : "normal";
7106
- 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}
7107
7343
  Proceed?`;
7108
- const { id: reqId, promise } = this.approvals.request(kind, prompt);
7109
- this.bus.emit({ type: "approval.requested", requestId: reqId, kind, prompt });
7110
- const approved = await promise;
7111
- this._status = "tool_calling";
7112
- if (!approved) {
7113
- this._stopAfterToolRound = true;
7114
- 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
+ }
7115
7352
  }
7116
7353
  }
7117
7354
  if (signal.aborted) return SKIP_MESSAGE;
@@ -7389,6 +7626,7 @@ if (rawArgs[0] === "sessions") {
7389
7626
  if (rawArgs[0] === "web") {
7390
7627
  let webPort = 4310;
7391
7628
  let webHost = "127.0.0.1";
7629
+ let webAutoApprove = false;
7392
7630
  for (let i = 1; i < rawArgs.length; i++) {
7393
7631
  const arg = rawArgs[i];
7394
7632
  const next = rawArgs[i + 1];
@@ -7399,6 +7637,8 @@ if (rawArgs[0] === "web") {
7399
7637
  } else if (arg === "--host" && next && !next.startsWith("-")) {
7400
7638
  webHost = next;
7401
7639
  i++;
7640
+ } else if (arg === "--auto") {
7641
+ webAutoApprove = true;
7402
7642
  }
7403
7643
  }
7404
7644
  const token = generateAccessToken();
@@ -7407,7 +7647,8 @@ if (rawArgs[0] === "web") {
7407
7647
  config: finalConfig,
7408
7648
  manager,
7409
7649
  token,
7410
- version: VERSION
7650
+ version: VERSION,
7651
+ autoApprove: webAutoApprove
7411
7652
  });
7412
7653
  await server.listen({ port: webPort, host: webHost });
7413
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.30",
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",