@zhongqian97-code/ecode 0.5.48 → 0.5.53

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.
package/README.md CHANGED
@@ -27,6 +27,7 @@ Priority: **env vars > config file > defaults**
27
27
  | `ECODE_BASE_URL` | `https://api.openai.com/v1` | Base URL (any OpenAI-compatible endpoint) |
28
28
  | `ECODE_MODEL` | `gpt-4o` | Model name |
29
29
  | `ECODE_LOG_DIR` | *(disabled)* | Directory for session logs (JSONL) |
30
+ | `ECODE_WEB_TOKEN` | *(random per launch)* | Fixed access token for `ecode web` — set this to keep the same URL across restarts |
30
31
 
31
32
  ### Config file
32
33
 
@@ -38,6 +39,7 @@ Priority: **env vars > config file > defaults**
38
39
  "baseUrl": "https://api.openai.com/v1",
39
40
  "model": "gpt-4o",
40
41
  "logDir": "~/.ecode/logs",
42
+ "webToken": "my-fixed-token",
41
43
  "contextLimit": 128000,
42
44
  "dangerousPatterns": [
43
45
  "rm -rf", "sudo", "chmod", "chown",
@@ -53,6 +55,8 @@ Priority: **env vars > config file > defaults**
53
55
 
54
56
  **`dangerousPatterns`** — list of command prefixes that require double confirmation. Setting this field replaces the entire default list.
55
57
 
58
+ **`webToken`** — fixed access token for the `ecode web` admin server. When unset, a fresh random token is generated on every launch (so the access URL changes each time). Set this (or `ECODE_WEB_TOKEN`) to a stable value to keep the same `http://host:port?token=...` URL across restarts. Priority: `ECODE_WEB_TOKEN` env > config file > random.
59
+
56
60
  ### Multi-provider config
57
61
 
58
62
  Configure multiple providers and switch between them:
@@ -223,6 +227,52 @@ Job state and run logs are stored in `<ECODE_LOG_DIR>/automation/` (or `~/.confi
223
227
 
224
228
  ---
225
229
 
230
+ ## Web admin (`ecode web`)
231
+
232
+ Run ecode as a browser-based admin server instead of the TUI — manage sessions, chat, skills, config, and automation jobs from a web UI.
233
+
234
+ ```bash
235
+ ecode web [options]
236
+ ```
237
+
238
+ | Option | Default | Description |
239
+ |---|---|---|
240
+ | `--host <host>` | `127.0.0.1` | Bind host. Localhost-only by default; pass `0.0.0.0` to expose on your network |
241
+ | `--port <port>` | `4310` | Bind port |
242
+ | `--auto` | *(off)* | Auto-approve tool calls (skip confirmation prompts) |
243
+ | `-h`, `--help` | | Show command help |
244
+
245
+ On start it prints the access URL with the token baked in:
246
+
247
+ ```
248
+ ecode web admin started
249
+ Bind: 127.0.0.1:4310
250
+ URL: http://127.0.0.1:4310?token=...
251
+ Token: fixed (from config)
252
+ ```
253
+
254
+ Just open the URL in a browser — the `?token=` query param authenticates you, no header setup needed.
255
+
256
+ ### Access token
257
+
258
+ The server is token-authenticated. By default a fresh random token is generated on every launch, so the URL changes each time. Set a fixed token to keep a stable URL across restarts:
259
+
260
+ ```bash
261
+ # via env var
262
+ export ECODE_WEB_TOKEN=my-fixed-token
263
+
264
+ # or in ~/.ecode/config.json
265
+ # "webToken": "my-fixed-token"
266
+ ```
267
+
268
+ Priority: `ECODE_WEB_TOKEN` env > config file `webToken` > random. See [Configuration](#configuration) for details.
269
+
270
+ > **Security:** binds to `127.0.0.1` by default. If you pass `--host 0.0.0.0` to expose it, always set a strong fixed token — anyone who can reach the port and guess the token gets full session/shell access.
271
+
272
+ > Requires Node.js 18.7+ (Fastify 5). On Node 16 use the TUI or `--pipe` mode instead.
273
+
274
+ ---
275
+
226
276
  ## Session logging
227
277
 
228
278
  Enable JSONL session logs to replay or analyze conversations:
package/dist/index.js CHANGED
@@ -923,6 +923,6 @@ Node.js 16/18 \u8BF7\u4F7F\u7528 --web \u6216 --pipe \u6A21\u5F0F\u3002
923
923
  );
924
924
  process.exit(1);
925
925
  }
926
- const { App, React, render } = await import("./ui-6WIYQZ7Y.js");
926
+ const { App, React, render } = await import("./ui-A5WEKWOC.js");
927
927
  render(React.createElement(App, { config: finalConfig, version: VERSION, autoMode, registry, trustedSkillDirs, initialMessages }));
928
928
  }
@@ -47,7 +47,7 @@ import { render } from "ink";
47
47
 
48
48
  // src/ui/App.tsx
49
49
  import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3, useMemo } from "react";
50
- import { Box as Box6, useInput as useInput2, useStdout, useStdin } from "ink";
50
+ import { Box as Box6, Text as Text6, useInput as useInput2, useStdout, useStdin } from "ink";
51
51
 
52
52
  // src/skills/executor.ts
53
53
  import { exec } from "child_process";
@@ -283,6 +283,53 @@ function messageToLines(msg, expandTools, terminalWidth) {
283
283
  return [];
284
284
  }
285
285
 
286
+ // src/ui/mouseSelection.ts
287
+ function initialSelectionState() {
288
+ return { anchor: null, focus: null, dragging: false };
289
+ }
290
+ function startSelection(_state, pt) {
291
+ return { anchor: pt, focus: pt, dragging: true };
292
+ }
293
+ function updateSelection(state, pt) {
294
+ return { ...state, focus: pt };
295
+ }
296
+ function finishSelection(state, pt) {
297
+ return { ...state, focus: pt, dragging: false };
298
+ }
299
+ function clearSelection() {
300
+ return { anchor: null, focus: null, dragging: false };
301
+ }
302
+ function normalizeSelection(state) {
303
+ if (!state.anchor || !state.focus) return null;
304
+ const a = state.anchor;
305
+ const f = state.focus;
306
+ const before = a.row < f.row || a.row === f.row && a.col <= f.col;
307
+ return before ? { start: a, end: f } : { start: f, end: a };
308
+ }
309
+ function isCellSelected(row, col, state) {
310
+ const norm = normalizeSelection(state);
311
+ if (!norm) return false;
312
+ const { start, end } = norm;
313
+ if (row < start.row || row > end.row) return false;
314
+ if (start.row === end.row) return col >= start.col && col <= end.col;
315
+ if (row === start.row) return col >= start.col;
316
+ if (row === end.row) return col <= end.col;
317
+ return true;
318
+ }
319
+ function extractSelectedText(lines, state) {
320
+ const norm = normalizeSelection(state);
321
+ if (!norm) return "";
322
+ const { start, end } = norm;
323
+ const parts = [];
324
+ for (let r = start.row; r <= end.row; r++) {
325
+ const line = lines[r] ?? "";
326
+ const colStart = r === start.row ? start.col : 0;
327
+ const colEnd = r === end.row ? Math.min(end.col + 1, line.length) : line.length;
328
+ parts.push(line.slice(colStart, colEnd));
329
+ }
330
+ return parts.join("\n");
331
+ }
332
+
286
333
  // src/ui/ConversationHistory.tsx
287
334
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
288
335
  function ConversationHistory({
@@ -290,7 +337,8 @@ function ConversationHistory({
290
337
  maxHeight = 20,
291
338
  expandTools = false,
292
339
  terminalWidth = 0,
293
- scrollOffset = 0
340
+ scrollOffset = 0,
341
+ selection
294
342
  }) {
295
343
  const visible = messages.filter(
296
344
  (m) => m.role !== "system"
@@ -313,7 +361,30 @@ function ConversationHistory({
313
361
  linesAbove,
314
362
  " \u884C \xB7 PageUp/Down \u6EDA\u52A8"
315
363
  ] }),
316
- visibleLines.map((line, idx) => /* @__PURE__ */ jsx2(Text2, { color: line.color, dimColor: line.dimColor, children: line.text }, idx))
364
+ visibleLines.map((line, idx) => {
365
+ const hasSelection = selection && normalizeSelection(selection) !== null;
366
+ if (!hasSelection) {
367
+ return /* @__PURE__ */ jsx2(Text2, { color: line.color, dimColor: line.dimColor, children: line.text }, idx);
368
+ }
369
+ const runs = [];
370
+ let cur = { text: "", selected: isCellSelected(idx, 0, selection) };
371
+ for (let col = 0; col < line.text.length; col++) {
372
+ const sel = isCellSelected(idx, col, selection);
373
+ if (sel !== cur.selected) {
374
+ if (cur.text) runs.push(cur);
375
+ cur = { text: line.text[col], selected: sel };
376
+ } else {
377
+ cur.text += line.text[col];
378
+ }
379
+ }
380
+ if (cur.text) runs.push(cur);
381
+ if (runs.length === 0) {
382
+ return /* @__PURE__ */ jsx2(Text2, { children: " " }, idx);
383
+ }
384
+ return /* @__PURE__ */ jsx2(Box2, { flexDirection: "row", children: runs.map(
385
+ (run, ri) => run.selected ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "blue", children: run.text }, ri) : /* @__PURE__ */ jsx2(Text2, { color: line.color, dimColor: line.dimColor, children: run.text }, ri)
386
+ ) }, idx);
387
+ })
317
388
  ] });
318
389
  }
319
390
 
@@ -701,34 +772,6 @@ function extractAtQuery(text) {
701
772
  if (spaceIdx !== -1) return null;
702
773
  return after;
703
774
  }
704
- async function expandFileRefs(text, cwd) {
705
- const atPattern = /@([\w./\-]+)/g;
706
- const replacements = [];
707
- let match;
708
- atPattern.lastIndex = 0;
709
- while ((match = atPattern.exec(text)) !== null) {
710
- const filePath = match[1];
711
- const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
712
- let replacement;
713
- try {
714
- const content = await fs.readFile(fullPath, "utf8");
715
- replacement = `\`\`\`
716
- // @${filePath}
717
- ${content}
718
- \`\`\``;
719
- } catch {
720
- replacement = `@${filePath} (not found)`;
721
- }
722
- replacements.push({ start: match.index, end: match.index + match[0].length, replacement });
723
- }
724
- if (replacements.length === 0) return text;
725
- let result = text;
726
- for (let i = replacements.length - 1; i >= 0; i--) {
727
- const { start, end, replacement } = replacements[i];
728
- result = result.slice(0, start) + replacement + result.slice(end);
729
- }
730
- return result;
731
- }
732
775
 
733
776
  // src/ui/fileAutocompleteLogic.ts
734
777
  function getInitialState2() {
@@ -751,25 +794,120 @@ function moveDown2(state, count) {
751
794
  function dismiss2(state) {
752
795
  return { ...state, dismissed: true };
753
796
  }
754
- function confirmSelection(inputText, selectedPath) {
797
+ function stripAtQuery(inputText) {
755
798
  const lastAt = inputText.lastIndexOf("@");
756
799
  if (lastAt === -1) return inputText;
757
- const prefix = inputText.slice(0, lastAt);
758
- return `${prefix}@${selectedPath} `;
800
+ return inputText.slice(0, lastAt).trimEnd();
801
+ }
802
+
803
+ // src/ui/selection.ts
804
+ import * as path2 from "path";
805
+ function createSelectionItem(filePath, cwd) {
806
+ const absPath = path2.isAbsolute(filePath) ? filePath : path2.join(cwd, filePath);
807
+ return {
808
+ id: `${absPath}-${Date.now()}-${Math.random().toString(36).slice(2)}`,
809
+ kind: "file",
810
+ path: filePath,
811
+ absPath,
812
+ label: filePath,
813
+ source: "at-file",
814
+ addedAt: Date.now()
815
+ };
816
+ }
817
+ function addSelection(items, item) {
818
+ if (items.some((i) => i.absPath === item.absPath)) return items;
819
+ return [...items, item];
820
+ }
821
+ function clearSelections() {
822
+ return [];
823
+ }
824
+
825
+ // src/context/selectionAssembler.ts
826
+ import * as fs2 from "fs/promises";
827
+ async function assembleMessageWithSelections(args) {
828
+ const { userText, selections, maxBytesPerFile = 1e5 } = args;
829
+ if (selections.length === 0) return userText;
830
+ const cleanText = userText.replace(/@[\w./\-]+/g, "").replace(/\s+/g, " ").trim();
831
+ const seen = /* @__PURE__ */ new Set();
832
+ const blocks = [];
833
+ for (const item of selections) {
834
+ if (seen.has(item.absPath)) continue;
835
+ seen.add(item.absPath);
836
+ try {
837
+ let content = await fs2.readFile(item.absPath, "utf8");
838
+ if (Buffer.byteLength(content) > maxBytesPerFile) {
839
+ content = content.slice(0, maxBytesPerFile) + "\n...(truncated)";
840
+ }
841
+ blocks.push(`\`\`\`
842
+ // @${item.path}
843
+ ${content}
844
+ \`\`\``);
845
+ } catch {
846
+ blocks.push(`@${item.path} (not found)`);
847
+ }
848
+ }
849
+ const contextSection = `[Selected Context]
850
+ ${blocks.join("\n\n")}`;
851
+ return cleanText ? `${cleanText}
852
+
853
+ ${contextSection}` : contextSection;
854
+ }
855
+
856
+ // src/ui/mouseInput.ts
857
+ var SGR_MOUSE_RE = /^\x1b\[<(\d+);\d+;\d+[Mm]/;
858
+ var SGR_FULL_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
859
+ function mouseEventLength(data) {
860
+ const s = typeof data === "string" ? data : data.toString("binary");
861
+ if (!s) return 0;
862
+ const sgrMatch = SGR_MOUSE_RE.exec(s);
863
+ if (sgrMatch) return sgrMatch[0].length;
864
+ if (s.length >= 6 && s.charCodeAt(0) === 27 && s[1] === "[" && s[2] === "M") return 6;
865
+ return 0;
866
+ }
867
+ function parseMouseScroll(data) {
868
+ const s = typeof data === "string" ? data : data.toString("binary");
869
+ if (!s) return null;
870
+ const sgrMatch = SGR_MOUSE_RE.exec(s);
871
+ if (sgrMatch) {
872
+ const cb = parseInt(sgrMatch[1], 10);
873
+ if (cb === 64) return { direction: "up" };
874
+ if (cb === 65) return { direction: "down" };
875
+ return null;
876
+ }
877
+ if (s.length >= 6 && s.charCodeAt(0) === 27 && s[1] === "[" && s[2] === "M") {
878
+ const buttonByte = s.charCodeAt(3);
879
+ if (buttonByte === 96) return { direction: "up" };
880
+ if (buttonByte === 97) return { direction: "down" };
881
+ return null;
882
+ }
883
+ return null;
884
+ }
885
+ function parseMouseSelectionEvent(data) {
886
+ const s = typeof data === "string" ? data : data.toString("binary");
887
+ const m = SGR_FULL_RE.exec(s);
888
+ if (!m) return null;
889
+ const cb = parseInt(m[1], 10);
890
+ const col = parseInt(m[2], 10) - 1;
891
+ const row = parseInt(m[3], 10) - 1;
892
+ const isRelease = m[4] === "m";
893
+ if (isRelease && cb === 0) return { type: "leftUp", col, row };
894
+ if (!isRelease && cb === 0) return { type: "leftDown", col, row };
895
+ if (!isRelease && cb === 32) return { type: "leftDrag", col, row };
896
+ return null;
759
897
  }
760
898
 
761
899
  // src/ui/App.tsx
762
900
  import { homedir as homedir2 } from "os";
763
- import { join as join4 } from "path";
901
+ import { join as join5 } from "path";
764
902
 
765
903
  // src/automation/runtime.ts
766
904
  import { randomUUID } from "crypto";
767
905
 
768
906
  // src/automation/log.ts
769
- import { readFile as readFile3, appendFile, mkdir } from "fs/promises";
770
- import { join as join2 } from "path";
907
+ import { readFile as readFile4, appendFile, mkdir } from "fs/promises";
908
+ import { join as join3 } from "path";
771
909
  function logFilePath(logDir) {
772
- return join2(logDir, "automation-runs.jsonl");
910
+ return join3(logDir, "automation-runs.jsonl");
773
911
  }
774
912
  async function appendRunLog(logDir, entry) {
775
913
  await mkdir(logDir, { recursive: true });
@@ -1393,8 +1531,8 @@ var AutomationManager = class {
1393
1531
  };
1394
1532
 
1395
1533
  // src/meta_skill/index.ts
1396
- import * as fs3 from "fs";
1397
- import * as path3 from "path";
1534
+ import * as fs4 from "fs";
1535
+ import * as path4 from "path";
1398
1536
  import { fileURLToPath } from "url";
1399
1537
 
1400
1538
  // src/meta_skill/task-classifier.ts
@@ -2110,24 +2248,24 @@ function materialize(policy, profile, input) {
2110
2248
  }
2111
2249
 
2112
2250
  // src/meta_skill/learning-store.ts
2113
- import * as fs2 from "fs";
2114
- import * as path2 from "path";
2251
+ import * as fs3 from "fs";
2252
+ import * as path3 from "path";
2115
2253
  import * as os from "os";
2116
- var STORE_PATH = path2.resolve(os.homedir(), ".ecode", "learning-records.jsonl");
2254
+ var STORE_PATH = path3.resolve(os.homedir(), ".ecode", "learning-records.jsonl");
2117
2255
 
2118
2256
  // src/meta_skill/index.ts
2119
2257
  function resolveBuiltinSkillsDir() {
2120
2258
  try {
2121
- let dir = path3.dirname(fileURLToPath(import.meta.url));
2259
+ let dir = path4.dirname(fileURLToPath(import.meta.url));
2122
2260
  for (let i = 0; i < 4; i++) {
2123
- const candidate = path3.join(dir, "skills");
2124
- if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) {
2125
- const hasSkillContent = fs3.readdirSync(candidate).some(
2126
- (entry) => fs3.existsSync(path3.join(candidate, entry, "SKILL.md"))
2261
+ const candidate = path4.join(dir, "skills");
2262
+ if (fs4.existsSync(candidate) && fs4.statSync(candidate).isDirectory()) {
2263
+ const hasSkillContent = fs4.readdirSync(candidate).some(
2264
+ (entry) => fs4.existsSync(path4.join(candidate, entry, "SKILL.md"))
2127
2265
  );
2128
2266
  if (hasSkillContent) return candidate;
2129
2267
  }
2130
- const parent = path3.dirname(dir);
2268
+ const parent = path4.dirname(dir);
2131
2269
  if (parent === dir) break;
2132
2270
  dir = parent;
2133
2271
  }
@@ -2143,15 +2281,15 @@ function runMetaAlign(input) {
2143
2281
  for (const skillName of inheritedSkills) {
2144
2282
  const candidates = [];
2145
2283
  if (builtinSkillsDir) {
2146
- candidates.push(path3.join(builtinSkillsDir, skillName, "SKILL.md"));
2147
- candidates.push(path3.join(builtinSkillsDir, skillName + ".md"));
2284
+ candidates.push(path4.join(builtinSkillsDir, skillName, "SKILL.md"));
2285
+ candidates.push(path4.join(builtinSkillsDir, skillName + ".md"));
2148
2286
  }
2149
- candidates.push(path3.join(process.cwd(), "skills", skillName, "SKILL.md"));
2150
- candidates.push(path3.join(process.cwd(), "skills", skillName + ".md"));
2287
+ candidates.push(path4.join(process.cwd(), "skills", skillName, "SKILL.md"));
2288
+ candidates.push(path4.join(process.cwd(), "skills", skillName + ".md"));
2151
2289
  let body = "";
2152
2290
  for (const p of candidates) {
2153
- if (fs3.existsSync(p)) {
2154
- body = fs3.readFileSync(p, { encoding: "utf-8" });
2291
+ if (fs4.existsSync(p)) {
2292
+ body = fs4.readFileSync(p, { encoding: "utf-8" });
2155
2293
  break;
2156
2294
  }
2157
2295
  }
@@ -2324,6 +2462,7 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2324
2462
  const [confirmPrompt, setConfirmPrompt] = useState3(void 0);
2325
2463
  const [expandTools, setExpandTools] = useState3(false);
2326
2464
  const [scrollOffset, setScrollOffset] = useState3(0);
2465
+ const [selectionState, setSelectionState] = useState3(initialSelectionState());
2327
2466
  const [inputHistory, setInputHistory] = useState3([]);
2328
2467
  const inputHistoryRef = useRef2([]);
2329
2468
  inputHistoryRef.current = inputHistory;
@@ -2338,10 +2477,16 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2338
2477
  }, [messages, expandTools, stdout == null ? void 0 : stdout.columns]);
2339
2478
  const totalLinesRef = useRef2(totalLines);
2340
2479
  totalLinesRef.current = totalLines;
2480
+ const scrollOffsetRef = useRef2(scrollOffset);
2481
+ scrollOffsetRef.current = scrollOffset;
2482
+ const historyMaxHeightRef = useRef2(historyMaxHeight);
2483
+ historyMaxHeightRef.current = historyMaxHeight;
2484
+ const selectionStateRef = useRef2(selectionState);
2485
+ selectionStateRef.current = selectionState;
2341
2486
  const pendingConfirmRef = useRef2(null);
2342
2487
  const abortControllerRef = useRef2(null);
2343
2488
  const llmRef = useRef2(llmClient ?? createProvider(resolveActiveProfile(config)));
2344
- const automationDataDir = config.logDir ? join4(config.logDir, "automation") : join4(homedir2(), ".config", "ecode", "automation");
2489
+ const automationDataDir = config.logDir ? join5(config.logDir, "automation") : join5(homedir2(), ".config", "ecode", "automation");
2345
2490
  const automationManagerRef = useRef2(
2346
2491
  new AutomationManager({
2347
2492
  dataDir: automationDataDir,
@@ -2359,8 +2504,10 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2359
2504
  const [acState, setAcState] = useState3(getInitialState());
2360
2505
  const [fileAcState, setFileAcState] = useState3(getInitialState2());
2361
2506
  const [fileSuggestions, setFileSuggestions] = useState3([]);
2507
+ const [selectedItems, setSelectedItems] = useState3([]);
2362
2508
  const loggerRef = useRef2(null);
2363
2509
  const loggedCountRef = useRef2(0);
2510
+ const pendingSelectionRef = useRef2(void 0);
2364
2511
  const sessionMetaRef = useRef2(null);
2365
2512
  const finalTokensRef = useRef2(0);
2366
2513
  useEffect3(() => {
@@ -2376,6 +2523,8 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2376
2523
  if (!loggerRef.current) return;
2377
2524
  for (let i = loggedCountRef.current; i < messages.length; i++) {
2378
2525
  const msg = messages[i];
2526
+ const sel = msg.role === "user" ? pendingSelectionRef.current : void 0;
2527
+ if (msg.role === "user") pendingSelectionRef.current = void 0;
2379
2528
  loggerRef.current.append({
2380
2529
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2381
2530
  role: msg.role,
@@ -2384,7 +2533,8 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2384
2533
  // tool_call_id 只在 tool 角色消息中存在,用于关联工具调用与结果
2385
2534
  tool_call_id: "tool_call_id" in msg ? msg.tool_call_id : void 0,
2386
2535
  // tool_calls 只在 assistant 角色消息中存在(当 LLM 决定调用工具时)
2387
- tool_calls: "tool_calls" in msg ? msg.tool_calls : void 0
2536
+ tool_calls: "tool_calls" in msg ? msg.tool_calls : void 0,
2537
+ selection: sel
2388
2538
  });
2389
2539
  }
2390
2540
  loggedCountRef.current = messages.length;
@@ -2395,6 +2545,78 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2395
2545
  automationManagerRef.current.stop();
2396
2546
  };
2397
2547
  }, []);
2548
+ useEffect3(() => {
2549
+ if (!stdout || !stdin) return;
2550
+ stdout.write("\x1B[?1002h\x1B[?1006h");
2551
+ const origEmit = stdin.emit;
2552
+ stdin.emit = function(event, ...args) {
2553
+ if (event === "readable") {
2554
+ const chunk = stdin.read();
2555
+ if (chunk !== null) {
2556
+ const s = chunk.toString("binary");
2557
+ const mouseLen = mouseEventLength(s);
2558
+ if (mouseLen > 0) {
2559
+ const mouseStr = s.slice(0, mouseLen);
2560
+ const scroll = parseMouseScroll(mouseStr);
2561
+ if (scroll) {
2562
+ const step = Math.max(1, Math.floor(historyMaxHeightRef.current / 2));
2563
+ if (scroll.direction === "up") {
2564
+ setScrollOffset((prev) => Math.min(prev + step, Math.max(0, totalLinesRef.current - 1)));
2565
+ } else {
2566
+ setScrollOffset((prev) => Math.max(0, prev - step));
2567
+ }
2568
+ }
2569
+ const sel = parseMouseSelectionEvent(mouseStr);
2570
+ if (sel) {
2571
+ const totalL = totalLinesRef.current;
2572
+ const scrollOff = scrollOffsetRef.current;
2573
+ const histH = historyMaxHeightRef.current;
2574
+ const visibleCount = Math.min(Math.max(0, totalL - scrollOff), histH);
2575
+ const contentStartRow = histH - visibleCount;
2576
+ const lineIdx = sel.row - contentStartRow;
2577
+ if (lineIdx >= 0 && lineIdx < visibleCount) {
2578
+ const pt = { row: lineIdx, col: sel.col };
2579
+ if (sel.type === "leftDown") {
2580
+ setSelectionState(startSelection(selectionStateRef.current, pt));
2581
+ } else if (sel.type === "leftDrag") {
2582
+ setSelectionState(updateSelection(selectionStateRef.current, pt));
2583
+ } else if (sel.type === "leftUp") {
2584
+ setSelectionState(finishSelection(selectionStateRef.current, pt));
2585
+ }
2586
+ }
2587
+ }
2588
+ const remainder = s.slice(mouseLen);
2589
+ if (remainder.length > 0) {
2590
+ stdin.unshift(Buffer.from(remainder, "binary"));
2591
+ return origEmit.call(this, event, ...args);
2592
+ }
2593
+ return true;
2594
+ }
2595
+ stdin.unshift(chunk);
2596
+ }
2597
+ }
2598
+ return origEmit.call(this, event, ...args);
2599
+ };
2600
+ return () => {
2601
+ stdin.emit = origEmit;
2602
+ stdout.write("\x1B[?1002l\x1B[?1006l");
2603
+ };
2604
+ }, [stdout, stdin]);
2605
+ useEffect3(() => {
2606
+ if (selectionState.dragging || !selectionState.anchor || !selectionState.focus) return;
2607
+ const visible = messages.filter((m) => m.role !== "system");
2608
+ const allLines = visible.flatMap((msg) => messageToLines(msg, expandTools, (stdout == null ? void 0 : stdout.columns) ?? 0));
2609
+ const totalL = allLines.length;
2610
+ const end = Math.max(0, Math.min(totalL, totalL - scrollOffset));
2611
+ const histH = historyMaxHeight;
2612
+ const start = Math.max(0, end - histH);
2613
+ const lineTexts = allLines.slice(start, end).map((l) => l.text);
2614
+ const text = extractSelectedText(lineTexts, selectionState);
2615
+ if (text && stdout) {
2616
+ const b64 = Buffer.from(text, "utf8").toString("base64");
2617
+ stdout.write(`\x1B]52;c;${b64}\x07`);
2618
+ }
2619
+ }, [selectionState]);
2398
2620
  useEffect3(() => {
2399
2621
  const atFragment = extractAtQuery(fileAcState.query);
2400
2622
  if (atFragment === null) {
@@ -2433,8 +2655,9 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2433
2655
  if (key.tab) {
2434
2656
  const selected = fileSuggestions[fileAcState.selectedIndex];
2435
2657
  if (selected) {
2436
- const newText = confirmSelection(fileAcState.query, selected.path);
2437
- (_a = inputRef.current) == null ? void 0 : _a.fill(newText);
2658
+ (_a = inputRef.current) == null ? void 0 : _a.fill(stripAtQuery(fileAcState.query));
2659
+ const item = createSelectionItem(selected.path, process.cwd());
2660
+ setSelectedItems((prev) => addSelection(prev, item));
2438
2661
  }
2439
2662
  return;
2440
2663
  }
@@ -2803,9 +3026,18 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2803
3026
  }
2804
3027
  let content = trimmed;
2805
3028
  try {
2806
- content = await expandFileRefs(trimmed, process.cwd());
3029
+ content = await assembleMessageWithSelections({
3030
+ userText: trimmed,
3031
+ selections: selectedItems,
3032
+ cwd: process.cwd()
3033
+ });
2807
3034
  } catch {
2808
3035
  }
3036
+ if (selectedItems.length > 0) {
3037
+ pendingSelectionRef.current = selectedItems.map((i) => ({ kind: i.kind, path: i.path }));
3038
+ }
3039
+ setSelectedItems(clearSelections());
3040
+ setSelectionState(clearSelection());
2809
3041
  const userMsg = { role: "user", content };
2810
3042
  const nextMessages = [...messages, userMsg];
2811
3043
  setMessages(nextMessages);
@@ -2823,7 +3055,7 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2823
3055
  ]);
2824
3056
  });
2825
3057
  },
2826
- [status, messages, runLlmLoop]
3058
+ [status, messages, selectedItems, runLlmLoop]
2827
3059
  );
2828
3060
  const isInputActive = status === "idle" || status === "awaiting_confirm";
2829
3061
  const handleInputTextChange = useCallback((text) => {
@@ -2847,7 +3079,8 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2847
3079
  expandTools,
2848
3080
  maxHeight: historyMaxHeight,
2849
3081
  terminalWidth: stdout == null ? void 0 : stdout.columns,
2850
- scrollOffset
3082
+ scrollOffset,
3083
+ selection: selectionState
2851
3084
  }
2852
3085
  ) }),
2853
3086
  /* @__PURE__ */ jsx6(
@@ -2876,6 +3109,10 @@ function App({ config, version, autoMode = false, registry, trustedSkillDirs = [
2876
3109
  isOpen: isOpen2(fileAcState, fileSuggestions)
2877
3110
  }
2878
3111
  ),
3112
+ selectedItems.length > 0 && /* @__PURE__ */ jsx6(Box6, { paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: "cyan", dimColor: true, children: [
3113
+ "Selected: ",
3114
+ selectedItems.map((i) => i.label).join(", ")
3115
+ ] }) }),
2879
3116
  /* @__PURE__ */ jsx6(
2880
3117
  Input_default,
2881
3118
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.5.48",
3
+ "version": "0.5.53",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",