@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 +50 -0
- package/dist/index.js +1 -1
- package/dist/{ui-6WIYQZ7Y.js → ui-A5WEKWOC.js} +299 -62
- package/package.json +1 -1
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-
|
|
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) =>
|
|
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
|
|
797
|
+
function stripAtQuery(inputText) {
|
|
755
798
|
const lastAt = inputText.lastIndexOf("@");
|
|
756
799
|
if (lastAt === -1) return inputText;
|
|
757
|
-
|
|
758
|
-
|
|
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
|
|
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
|
|
770
|
-
import { join as
|
|
907
|
+
import { readFile as readFile4, appendFile, mkdir } from "fs/promises";
|
|
908
|
+
import { join as join3 } from "path";
|
|
771
909
|
function logFilePath(logDir) {
|
|
772
|
-
return
|
|
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
|
|
1397
|
-
import * as
|
|
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
|
|
2114
|
-
import * as
|
|
2251
|
+
import * as fs3 from "fs";
|
|
2252
|
+
import * as path3 from "path";
|
|
2115
2253
|
import * as os from "os";
|
|
2116
|
-
var STORE_PATH =
|
|
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 =
|
|
2259
|
+
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
2122
2260
|
for (let i = 0; i < 4; i++) {
|
|
2123
|
-
const candidate =
|
|
2124
|
-
if (
|
|
2125
|
-
const hasSkillContent =
|
|
2126
|
-
(entry) =>
|
|
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 =
|
|
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(
|
|
2147
|
-
candidates.push(
|
|
2284
|
+
candidates.push(path4.join(builtinSkillsDir, skillName, "SKILL.md"));
|
|
2285
|
+
candidates.push(path4.join(builtinSkillsDir, skillName + ".md"));
|
|
2148
2286
|
}
|
|
2149
|
-
candidates.push(
|
|
2150
|
-
candidates.push(
|
|
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 (
|
|
2154
|
-
body =
|
|
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 ?
|
|
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
|
-
|
|
2437
|
-
|
|
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
|
|
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
|
{
|