agentlas 1.0.36 → 1.0.38
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/CHANGELOG.md +40 -0
- package/engine/agentlas-banner.cjs +1 -1
- package/engine/agentlas-onboard.cjs +2 -2
- package/engine/agentlas-ui.cjs +22 -7
- package/engine/agentlas-workforce.cjs +9 -3
- package/engine/architecture.data.json +13 -1
- package/engine/commands/cloud.cjs +4 -1
- package/engine/commands/doctor.cjs +37 -8
- package/engine/commands/list.cjs +1 -1
- package/engine/commands/roles.cjs +2 -1
- package/engine/commands/upload.cjs +4 -1
- package/engine/commands/workforce.cjs +1 -1
- package/engine/graph/interview.cjs +55 -11
- package/engine/hephaestus/runtime.cjs +7 -1
- package/engine/memory-cli/curate.cjs +12 -1
- package/engine/sessions/apply-fences.cjs +75 -1
- package/engine/sessions/prompt.cjs +27 -0
- package/engine/storm/deps.cjs +2 -0
- package/engine/storm/storm.cjs +8 -2
- package/engine/storm/swarm.cjs +11 -5
- package/engine/ui/pitui-shell.cjs +295 -0
- package/engine/ui/repl.cjs +30 -7
- package/engine/ui/width.cjs +172 -0
- package/engine/workforce/deps.cjs +21 -0
- package/package.json +6 -3
- package/engine/agentlas-composer.cjs +0 -532
|
@@ -314,11 +314,30 @@ async function workforceAccountContext() {
|
|
|
314
314
|
params: { name: "agentlas.account_context", arguments: {} },
|
|
315
315
|
}),
|
|
316
316
|
});
|
|
317
|
+
/*
|
|
318
|
+
* 서버의 로그인 안내를 먼저 중계한다 (2026-08-11 존폐 판단 결함 4).
|
|
319
|
+
* 실사고: 세션 만료 시 서버는 auth_required + 로그인 방법을 친절히 돌려줬는데,
|
|
320
|
+
* 클라이언트가 그 필드를 안 보고 스키마 검사부터 실패시켜 "유효하지 않은 연속성
|
|
321
|
+
* 영수증"으로 오진했고, 그 오진마저 표시 경계가 지웠다 — 안내가 세 번 소실됐다.
|
|
322
|
+
* 기계 code를 달아 표시 경계(usage/honestStop 통과 조건)를 지나게 한다.
|
|
323
|
+
*/
|
|
324
|
+
const authRequiredError = (detail) => Object.assign(
|
|
325
|
+
new Error(detail || "Agentlas sign-in required — run `agentlas login`, then retry."),
|
|
326
|
+
{ code: "auth_required", honestStop: true },
|
|
327
|
+
);
|
|
328
|
+
if (response.status === 401 || response.status === 403) throw authRequiredError();
|
|
317
329
|
if (!response.ok) throw new Error(`Agentlas account context failed with HTTP ${response.status}.`);
|
|
318
330
|
const rpc = hubClient.parseHubJson(response, "Agentlas account context");
|
|
319
331
|
const text = rpc?.result?.content?.[0]?.text;
|
|
320
332
|
let payload;
|
|
321
333
|
try { payload = JSON.parse(String(text || "")); } catch { payload = null; }
|
|
334
|
+
const authMarker = [payload?.error, payload?.code, rpc?.error?.code, rpc?.error?.message]
|
|
335
|
+
.map((v) => String(v || ""))
|
|
336
|
+
.find((v) => /auth_required|unauthorized|not signed in/i.test(v));
|
|
337
|
+
if (authMarker) {
|
|
338
|
+
const hint = String(payload?.message || payload?.hint || rpc?.error?.message || "").slice(0, 400);
|
|
339
|
+
throw authRequiredError(hint ? `${hint} — run \`agentlas login\`, then retry.` : null);
|
|
340
|
+
}
|
|
322
341
|
// 연속성 영수증은 스키마·계정 다이제스트·과금 권한을 정확히 검증한다 — 위조/구버전
|
|
323
342
|
// 응답으로 goal 바인딩을 진행하면 안 된다.
|
|
324
343
|
if (
|
|
@@ -524,6 +543,8 @@ function buildWorkforceDeps(ctx = {}) {
|
|
|
524
543
|
return {
|
|
525
544
|
now: () => new Date(),
|
|
526
545
|
out: typeof ctx.out === "function" ? ctx.out : (s) => process.stdout.write(`${s}\n`),
|
|
546
|
+
// Phase 1-2: 엔진 ctx의 Ui를 관통시킨다 — 워크포스가 자체 Ui를 만들지 않게.
|
|
547
|
+
uiInstance: ctx.uiInstance || null,
|
|
527
548
|
prefsLang: () => ctx.lang || "en",
|
|
528
549
|
userDataDir,
|
|
529
550
|
projectCwd: capture.projectCwd,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"test:tool-access-notice-parity": "node test/tool-access-notice-parity.cjs"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=20"
|
|
16
|
+
"node": ">=20.19"
|
|
17
17
|
},
|
|
18
18
|
"optionalDependencies": {
|
|
19
19
|
"better-sqlite3": "^11.3.0",
|
|
@@ -48,5 +48,8 @@
|
|
|
48
48
|
"url": "https://github.com/agentlas-ai/agentlas-terminal/issues"
|
|
49
49
|
},
|
|
50
50
|
"author": "Agentlas (https://agentlas.cloud)",
|
|
51
|
-
"license": "Apache-2.0"
|
|
51
|
+
"license": "Apache-2.0",
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@earendil-works/pi-tui": "0.84.1"
|
|
54
|
+
}
|
|
52
55
|
}
|
|
@@ -1,532 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/*
|
|
3
|
-
* agentlas-composer: a raw-mode bottom input box (Claude Code / Hermes style).
|
|
4
|
-
*
|
|
5
|
-
* ───────────────────────────────────────────────
|
|
6
|
-
* › your message
|
|
7
|
-
* ───────────────────────────────────────────────
|
|
8
|
-
* ◆ read + write · codex · Agent · / for commands
|
|
9
|
-
* (slash suggestions render here while typing /…)
|
|
10
|
-
*
|
|
11
|
-
* Single-line field with horizontal scroll (fixed 3-line box → flicker-free clear/redraw).
|
|
12
|
-
* Full line editing, persisted history, Tab/path/slash completion, slash palette.
|
|
13
|
-
* Zero external deps. Caller falls back to readline when stdin/stdout is not a TTY.
|
|
14
|
-
*/
|
|
15
|
-
const readline = require("node:readline");
|
|
16
|
-
const i18n = require("./agentlas-i18n.cjs");
|
|
17
|
-
const permissions = require("./agentlas-permissions.cjs");
|
|
18
|
-
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
19
|
-
const MARK_RE = /\p{Mark}/u;
|
|
20
|
-
const EXTENDED_PICTOGRAPHIC_RE = /\p{Extended_Pictographic}/u;
|
|
21
|
-
|
|
22
|
-
// East-Asian width: CJK / Hangul / Kana / fullwidth glyphs occupy 2 terminal cells.
|
|
23
|
-
function isWide(cp) {
|
|
24
|
-
return (
|
|
25
|
-
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
26
|
-
(cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals … symbols
|
|
27
|
-
(cp >= 0x3041 && cp <= 0x33ff) || // Hiragana … CJK compat
|
|
28
|
-
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK ext A
|
|
29
|
-
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK unified
|
|
30
|
-
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
|
31
|
-
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
|
|
32
|
-
(cp >= 0xf900 && cp <= 0xfaff) || // CJK compat ideographs
|
|
33
|
-
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK compat forms
|
|
34
|
-
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth forms
|
|
35
|
-
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
36
|
-
(cp >= 0x1f300 && cp <= 0x1faff) // emoji / pictographs
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
function charWidth(ch) {
|
|
40
|
-
const cp = ch.codePointAt(0);
|
|
41
|
-
if (cp < 0x20) return 0;
|
|
42
|
-
if (
|
|
43
|
-
cp === 0x200d || // zero-width joiner
|
|
44
|
-
(cp >= 0xfe00 && cp <= 0xfe0f) || // variation selectors
|
|
45
|
-
(cp >= 0xe0100 && cp <= 0xe01ef) ||
|
|
46
|
-
(cp >= 0x1f3fb && cp <= 0x1f3ff) || // emoji skin tones
|
|
47
|
-
MARK_RE.test(ch)
|
|
48
|
-
) return 0;
|
|
49
|
-
return isWide(cp) ? 2 : 1;
|
|
50
|
-
}
|
|
51
|
-
function graphemeSegments(value) {
|
|
52
|
-
return [...GRAPHEME_SEGMENTER.segment(String(value || ""))];
|
|
53
|
-
}
|
|
54
|
-
function graphemeWidth(segment) {
|
|
55
|
-
const text = String(segment || "");
|
|
56
|
-
if (
|
|
57
|
-
EXTENDED_PICTOGRAPHIC_RE.test(text) ||
|
|
58
|
-
/[\u{1f1e6}-\u{1f1ff}]/u.test(text) ||
|
|
59
|
-
text.includes("\u20e3")
|
|
60
|
-
) return 2;
|
|
61
|
-
let width = 0;
|
|
62
|
-
for (const ch of text) width += charWidth(ch);
|
|
63
|
-
return width;
|
|
64
|
-
}
|
|
65
|
-
function previousGraphemeIndex(value, index) {
|
|
66
|
-
const text = String(value || "");
|
|
67
|
-
const cursor = Math.max(0, Math.min(Number(index) || 0, text.length));
|
|
68
|
-
let previous = 0;
|
|
69
|
-
for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
|
|
70
|
-
const end = entry.index + entry.segment.length;
|
|
71
|
-
if (cursor <= entry.index) return previous;
|
|
72
|
-
if (cursor <= end) return entry.index;
|
|
73
|
-
previous = entry.index;
|
|
74
|
-
}
|
|
75
|
-
return previous;
|
|
76
|
-
}
|
|
77
|
-
function nextGraphemeIndex(value, index) {
|
|
78
|
-
const text = String(value || "");
|
|
79
|
-
const cursor = Math.max(0, Math.min(Number(index) || 0, text.length));
|
|
80
|
-
for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
|
|
81
|
-
const end = entry.index + entry.segment.length;
|
|
82
|
-
if (cursor < end) return end;
|
|
83
|
-
}
|
|
84
|
-
return text.length;
|
|
85
|
-
}
|
|
86
|
-
function visWidth(s) {
|
|
87
|
-
const clean = String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
88
|
-
let n = 0;
|
|
89
|
-
for (const entry of GRAPHEME_SEGMENTER.segment(clean)) n += graphemeWidth(entry.segment);
|
|
90
|
-
return n;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function truncateWidth(value, max) {
|
|
94
|
-
const text = String(value || "");
|
|
95
|
-
if (visWidth(text) <= max) return text;
|
|
96
|
-
let out = "";
|
|
97
|
-
let width = 0;
|
|
98
|
-
const room = Math.max(0, max - 1);
|
|
99
|
-
for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
|
|
100
|
-
const cells = graphemeWidth(entry.segment);
|
|
101
|
-
if (width + cells > room) break;
|
|
102
|
-
out += entry.segment;
|
|
103
|
-
width += cells;
|
|
104
|
-
}
|
|
105
|
-
return out + "…";
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function splitWidth(value, max) {
|
|
109
|
-
const text = String(value || "");
|
|
110
|
-
const limit = Math.max(1, Math.floor(Number(max) || 1));
|
|
111
|
-
const lines = [];
|
|
112
|
-
let line = "";
|
|
113
|
-
let width = 0;
|
|
114
|
-
for (const entry of GRAPHEME_SEGMENTER.segment(text)) {
|
|
115
|
-
const cells = graphemeWidth(entry.segment);
|
|
116
|
-
if (line && width + cells > limit) {
|
|
117
|
-
lines.push(line);
|
|
118
|
-
line = "";
|
|
119
|
-
width = 0;
|
|
120
|
-
}
|
|
121
|
-
line += entry.segment;
|
|
122
|
-
width += cells;
|
|
123
|
-
}
|
|
124
|
-
if (line || !lines.length) lines.push(line);
|
|
125
|
-
return lines;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function wrapWidth(value, max) {
|
|
129
|
-
const limit = Math.max(2, Math.floor(Number(max) || 2));
|
|
130
|
-
const lines = [];
|
|
131
|
-
const pushWord = (word, state) => {
|
|
132
|
-
if (!word) return;
|
|
133
|
-
const cells = visWidth(word);
|
|
134
|
-
if (state.line && state.width + 1 + cells <= limit) {
|
|
135
|
-
state.line += " " + word;
|
|
136
|
-
state.width += 1 + cells;
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (state.line) {
|
|
140
|
-
lines.push(state.line);
|
|
141
|
-
state.line = "";
|
|
142
|
-
state.width = 0;
|
|
143
|
-
}
|
|
144
|
-
if (cells <= limit) {
|
|
145
|
-
state.line = word;
|
|
146
|
-
state.width = cells;
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
let chunk = "";
|
|
150
|
-
let chunkWidth = 0;
|
|
151
|
-
for (const entry of GRAPHEME_SEGMENTER.segment(word)) {
|
|
152
|
-
const width = graphemeWidth(entry.segment);
|
|
153
|
-
if (chunk && chunkWidth + width > limit) {
|
|
154
|
-
lines.push(chunk);
|
|
155
|
-
chunk = "";
|
|
156
|
-
chunkWidth = 0;
|
|
157
|
-
}
|
|
158
|
-
chunk += entry.segment;
|
|
159
|
-
chunkWidth += width;
|
|
160
|
-
}
|
|
161
|
-
state.line = chunk;
|
|
162
|
-
state.width = chunkWidth;
|
|
163
|
-
};
|
|
164
|
-
const paragraphs = String(value || "").split(/\r?\n/);
|
|
165
|
-
paragraphs.forEach((paragraph) => {
|
|
166
|
-
const state = { line: "", width: 0 };
|
|
167
|
-
for (const word of paragraph.trim().split(/\s+/u).filter(Boolean)) pushWord(word, state);
|
|
168
|
-
if (state.line) lines.push(state.line);
|
|
169
|
-
else if (!paragraph.trim()) lines.push("");
|
|
170
|
-
});
|
|
171
|
-
return lines.length ? lines : [""];
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function identityPalette() {
|
|
175
|
-
const id = (value) => String(value);
|
|
176
|
-
return { faint: id, emerald: id, text: id, paw: id, amber: id, blue: id, dim: id, inverse: id };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function permissionPresentation(ctx, c) {
|
|
180
|
-
const permission = permissions.normalize(ctx.permission, "write");
|
|
181
|
-
const fallback = permissions.copy(permission, ctx.lang || "en").label;
|
|
182
|
-
const label = ctx.permissionLabel || fallback;
|
|
183
|
-
if (permission === "full") return c.paw("▶▶ " + label);
|
|
184
|
-
if (permission === "read") return c.blue("◇ " + label);
|
|
185
|
-
return c.amber("◆ " + label);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Pure frame builder so layout can be regression-tested without taking over a real TTY.
|
|
189
|
-
function buildComposerFrame(state, ctx = {}, palette, width = 80) {
|
|
190
|
-
const c = palette || identityPalette();
|
|
191
|
-
const w = Math.max(29, width);
|
|
192
|
-
const fieldW = Math.max(8, w - 2);
|
|
193
|
-
|
|
194
|
-
// horizontal scroll by visual width — keep the cursor visible (CJK-safe)
|
|
195
|
-
let start = Math.min(state.scroll, state.cur);
|
|
196
|
-
while (start < state.cur && visWidth(state.buf.slice(start, state.cur)) > fieldW - 2) {
|
|
197
|
-
start = nextGraphemeIndex(state.buf, start);
|
|
198
|
-
}
|
|
199
|
-
state.scroll = start;
|
|
200
|
-
|
|
201
|
-
let shown = "";
|
|
202
|
-
let shownWidth = 0;
|
|
203
|
-
for (const entry of GRAPHEME_SEGMENTER.segment(state.buf.slice(start))) {
|
|
204
|
-
const cw = graphemeWidth(entry.segment);
|
|
205
|
-
if (shownWidth + cw > fieldW - 2) break;
|
|
206
|
-
shown += entry.segment;
|
|
207
|
-
shownWidth += cw;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const prefix = (ctx.glyph || "›") + " ";
|
|
211
|
-
const top = c.faint("─".repeat(w));
|
|
212
|
-
const mid = c.text(prefix) + c.text(shown);
|
|
213
|
-
const bot = c.faint("─".repeat(w));
|
|
214
|
-
const lines = [top, mid, bot];
|
|
215
|
-
if (ctx.status || ctx.permission) {
|
|
216
|
-
const permission = permissions.normalize(ctx.permission, "write");
|
|
217
|
-
const fallback = permissions.copy(permission, ctx.lang || "en").label;
|
|
218
|
-
const permissionLabel = ctx.permissionLabel || fallback;
|
|
219
|
-
const permissionText = (permission === "full" ? "▶▶ " : permission === "read" ? "◇ " : "◆ ") + permissionLabel;
|
|
220
|
-
const available = Math.max(0, w - visWidth(permissionText) - 5);
|
|
221
|
-
const rest = ctx.status && available > 0 ? c.faint(" · " + truncateWidth(ctx.status, available)) : "";
|
|
222
|
-
lines.push(permissionPresentation(ctx, c) + rest);
|
|
223
|
-
}
|
|
224
|
-
// 연결 LLM 세션 사용량 상시 표시줄 — 입력박스 바로 아래에 항상 유지된다.
|
|
225
|
-
const usageText = typeof ctx.usage === "function" ? ctx.usage() : ctx.usage;
|
|
226
|
-
if (usageText) lines.push(c.faint(truncateWidth(String(usageText), w)));
|
|
227
|
-
if (state.notice) lines.push(c.faint(truncateWidth(String(state.notice), w)));
|
|
228
|
-
if (ctx.confirmation) {
|
|
229
|
-
const prefix = ctx.confirmationTone === "danger" ? "! " : "✓ ";
|
|
230
|
-
const paint = ctx.confirmationTone === "danger" && c.paw ? c.paw : c.green || c.emerald || ((value) => value);
|
|
231
|
-
lines.push(paint(truncateWidth(prefix + ctx.confirmation, w)));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const rows = state.suggest || [];
|
|
235
|
-
const terminalRows = Math.max(5, Number(ctx.rows) || 24);
|
|
236
|
-
const maxSuggestions = Math.min(8, Math.max(0, terminalRows - lines.length - 1));
|
|
237
|
-
const selected = Math.max(0, Math.min(Number(state.suggestSel) || 0, Math.max(0, rows.length - 1)));
|
|
238
|
-
const startRow = Math.min(
|
|
239
|
-
Math.max(0, selected - maxSuggestions + 1),
|
|
240
|
-
Math.max(0, rows.length - maxSuggestions),
|
|
241
|
-
);
|
|
242
|
-
rows.slice(startRow, startRow + maxSuggestions).forEach((row, offset) => {
|
|
243
|
-
const index = startRow + offset;
|
|
244
|
-
const cmd = String(row.command || "").padEnd(16);
|
|
245
|
-
const desc = String(row.description || "");
|
|
246
|
-
const descRoom = Math.max(0, w - visWidth(" " + cmd + " "));
|
|
247
|
-
const clippedDesc = truncateWidth(desc, descRoom);
|
|
248
|
-
const label = " " + cmd + " " + clippedDesc;
|
|
249
|
-
lines.push(index === state.suggestSel ? c.inverse(label) : " " + c.blue(cmd) + " " + c.dim(clippedDesc));
|
|
250
|
-
});
|
|
251
|
-
if (rows.length && lines.length < terminalRows) {
|
|
252
|
-
lines.push(c.faint(" " + i18n.t(ctx.lang || "en", "palette.controls")));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return { lines, curCol: visWidth(prefix) + visWidth(state.buf.slice(start, state.cur)) };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function createComposer(opts) {
|
|
259
|
-
const out = opts.stream || process.stdout;
|
|
260
|
-
const inp = opts.input || process.stdin;
|
|
261
|
-
const ui = opts.ui;
|
|
262
|
-
const c = ui.c;
|
|
263
|
-
const loadHistory = opts.loadHistory || (() => []);
|
|
264
|
-
const saveHistory = opts.saveHistory || (() => {});
|
|
265
|
-
const getHistoryScope = opts.getHistoryScope || (() => "");
|
|
266
|
-
let loadedHistoryScope = String(getHistoryScope() || "");
|
|
267
|
-
let history = (loadHistory() || []).filter((x) => typeof x === "string"); // index 0 = most recent
|
|
268
|
-
let idleExitArmedUntil = 0;
|
|
269
|
-
|
|
270
|
-
function cols() {
|
|
271
|
-
return Math.max(30, out.columns || process.stdout.columns || 80);
|
|
272
|
-
}
|
|
273
|
-
function boxWidth() {
|
|
274
|
-
return Math.min(cols() - 1, 120);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Build the rendered block (array of lines) + the cursor target column on the input line.
|
|
278
|
-
function frame(state, ctx) {
|
|
279
|
-
return buildComposerFrame(
|
|
280
|
-
state,
|
|
281
|
-
{ ...ctx, rows: out.rows || process.stdout.rows || 24 },
|
|
282
|
-
c,
|
|
283
|
-
boxWidth(),
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function render(state, ctx) {
|
|
288
|
-
const f = frame(state, ctx);
|
|
289
|
-
let seq = "";
|
|
290
|
-
if (state.drawn > 0) seq += "\r\x1b[1A\x1b[0J"; // from input line: col0, up to top border, clear down
|
|
291
|
-
seq += f.lines.join("\r\n");
|
|
292
|
-
const up = f.lines.length - 1 - 1; // from last line up to the input line (index 1)
|
|
293
|
-
if (up > 0) seq += "\x1b[" + up + "A";
|
|
294
|
-
seq += "\r";
|
|
295
|
-
if (f.curCol > 0) seq += "\x1b[" + f.curCol + "C";
|
|
296
|
-
out.write(seq);
|
|
297
|
-
state.drawn = f.lines.length;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function clearBox(state) {
|
|
301
|
-
if (state.drawn > 0) {
|
|
302
|
-
out.write("\r\x1b[1A\x1b[0J");
|
|
303
|
-
state.drawn = 0;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function read(ctx) {
|
|
308
|
-
ctx = ctx || {};
|
|
309
|
-
const nextHistoryScope = String(getHistoryScope() || "");
|
|
310
|
-
if (nextHistoryScope !== loadedHistoryScope) {
|
|
311
|
-
history = (loadHistory() || []).filter((x) => typeof x === "string");
|
|
312
|
-
loadedHistoryScope = nextHistoryScope;
|
|
313
|
-
}
|
|
314
|
-
return new Promise((resolve) => {
|
|
315
|
-
const state = { buf: "", cur: 0, scroll: 0, drawn: 0, suggest: [], suggestSel: 0, hist: -1, stash: "", dismissed: null, notice: null };
|
|
316
|
-
let scheduledDraw = null;
|
|
317
|
-
|
|
318
|
-
function refreshSuggest() {
|
|
319
|
-
if (ctx.suggest && state.buf !== state.dismissed) {
|
|
320
|
-
state.suggest = ctx.suggest(state.buf) || [];
|
|
321
|
-
} else {
|
|
322
|
-
state.suggest = [];
|
|
323
|
-
}
|
|
324
|
-
if (state.suggestSel >= state.suggest.length) state.suggestSel = 0;
|
|
325
|
-
}
|
|
326
|
-
function draw() {
|
|
327
|
-
if (scheduledDraw) {
|
|
328
|
-
clearImmediate(scheduledDraw);
|
|
329
|
-
scheduledDraw = null;
|
|
330
|
-
}
|
|
331
|
-
refreshSuggest();
|
|
332
|
-
render(state, ctx);
|
|
333
|
-
}
|
|
334
|
-
function drawSoon() {
|
|
335
|
-
if (scheduledDraw) return;
|
|
336
|
-
scheduledDraw = setImmediate(() => {
|
|
337
|
-
scheduledDraw = null;
|
|
338
|
-
refreshSuggest();
|
|
339
|
-
render(state, ctx);
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const wasRaw = !!inp.isRaw;
|
|
344
|
-
try { if (inp.setRawMode) inp.setRawMode(true); } catch { /* ignore */ }
|
|
345
|
-
readline.emitKeypressEvents(inp);
|
|
346
|
-
inp.resume();
|
|
347
|
-
|
|
348
|
-
function done(result) {
|
|
349
|
-
if (scheduledDraw) {
|
|
350
|
-
clearImmediate(scheduledDraw);
|
|
351
|
-
scheduledDraw = null;
|
|
352
|
-
}
|
|
353
|
-
if (typeof out.removeListener === "function") out.removeListener("resize", drawSoon);
|
|
354
|
-
inp.removeListener("keypress", onKey);
|
|
355
|
-
try { if (inp.setRawMode) inp.setRawMode(wasRaw); } catch { /* ignore */ }
|
|
356
|
-
resolve(result);
|
|
357
|
-
}
|
|
358
|
-
function setBuf(s, cur, deferDraw = false) {
|
|
359
|
-
state.buf = s;
|
|
360
|
-
state.cur = cur == null ? s.length : Math.max(0, Math.min(cur, s.length));
|
|
361
|
-
state.dismissed = null;
|
|
362
|
-
if (deferDraw) drawSoon();
|
|
363
|
-
else draw();
|
|
364
|
-
}
|
|
365
|
-
function submit() {
|
|
366
|
-
const value = state.buf;
|
|
367
|
-
clearBox(state);
|
|
368
|
-
out.write(c.paw("▌") + c.emerald(" › ") + c.text(value) + "\r\n");
|
|
369
|
-
if (value.trim()) {
|
|
370
|
-
history = history.filter((h) => h !== value);
|
|
371
|
-
history.unshift(value);
|
|
372
|
-
saveHistory(history);
|
|
373
|
-
}
|
|
374
|
-
done({ value });
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function onKey(str, key) {
|
|
378
|
-
key = key || {};
|
|
379
|
-
const name = key.name;
|
|
380
|
-
const shiftTab = name === "tab" && key.shift;
|
|
381
|
-
// Node's keypress decoder keeps a lone Escape open briefly in case it
|
|
382
|
-
// starts a longer sequence. If Ctrl-C arrives in that window, the
|
|
383
|
-
// event can be reported as meta-C with sequence ESC+ETX and
|
|
384
|
-
// `key.ctrl === false`. ETX still means cancel; never leave the prior
|
|
385
|
-
// buffer armed for the next submitted command.
|
|
386
|
-
const keySequence = String(key.sequence ?? str ?? "");
|
|
387
|
-
const ctrlC = (key.ctrl && name === "c") || keySequence.includes("\x03");
|
|
388
|
-
if (!ctrlC) {
|
|
389
|
-
state.notice = null;
|
|
390
|
-
idleExitArmedUntil = 0;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (!shiftTab && ctx.onPermissionCycleCancel) {
|
|
394
|
-
const hadConfirmation = Boolean(ctx.confirmation);
|
|
395
|
-
const next = ctx.onPermissionCycleCancel();
|
|
396
|
-
if (next && typeof next === "object") Object.assign(ctx, next);
|
|
397
|
-
if (hadConfirmation && !ctx.confirmation) draw();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (ctrlC) {
|
|
401
|
-
if (ctx.continuation) {
|
|
402
|
-
clearBox(state);
|
|
403
|
-
return done({ cancel: true });
|
|
404
|
-
}
|
|
405
|
-
if (state.buf.length) { idleExitArmedUntil = 0; return setBuf("", 0); }
|
|
406
|
-
const now = Date.now();
|
|
407
|
-
if (now < idleExitArmedUntil) {
|
|
408
|
-
idleExitArmedUntil = 0;
|
|
409
|
-
clearBox(state);
|
|
410
|
-
return done({ exit: true });
|
|
411
|
-
}
|
|
412
|
-
idleExitArmedUntil = now + 3000;
|
|
413
|
-
state.notice = i18n.t(ctx.lang || "en", "ctrlcAgain");
|
|
414
|
-
return draw();
|
|
415
|
-
}
|
|
416
|
-
if (key.ctrl && name === "d") {
|
|
417
|
-
if (!state.buf.length) { clearBox(state); return done({ eof: true }); }
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
if (name === "return" || name === "enter") return submit();
|
|
421
|
-
if (name === "escape") {
|
|
422
|
-
if (state.suggest.length) { state.dismissed = state.buf; state.suggest = []; return render(state, ctx); }
|
|
423
|
-
return setBuf("", 0);
|
|
424
|
-
}
|
|
425
|
-
if (name === "backspace" || (key.ctrl && name === "h")) {
|
|
426
|
-
if (state.cur > 0) {
|
|
427
|
-
const previous = previousGraphemeIndex(state.buf, state.cur);
|
|
428
|
-
setBuf(state.buf.slice(0, previous) + state.buf.slice(state.cur), previous, true);
|
|
429
|
-
}
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (name === "delete") {
|
|
433
|
-
const next = nextGraphemeIndex(state.buf, state.cur);
|
|
434
|
-
return setBuf(state.buf.slice(0, state.cur) + state.buf.slice(next), state.cur, true);
|
|
435
|
-
}
|
|
436
|
-
if (name === "left") {
|
|
437
|
-
if (state.cur > 0) {
|
|
438
|
-
state.cur = previousGraphemeIndex(state.buf, state.cur);
|
|
439
|
-
draw();
|
|
440
|
-
}
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
if (name === "right") {
|
|
444
|
-
if (state.cur < state.buf.length) {
|
|
445
|
-
state.cur = nextGraphemeIndex(state.buf, state.cur);
|
|
446
|
-
draw();
|
|
447
|
-
}
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
if (name === "home" || (key.ctrl && name === "a")) { state.cur = 0; draw(); return; }
|
|
451
|
-
if (name === "end" || (key.ctrl && name === "e")) { state.cur = state.buf.length; draw(); return; }
|
|
452
|
-
if (key.ctrl && name === "u") return setBuf(state.buf.slice(state.cur), 0, true);
|
|
453
|
-
if (key.ctrl && name === "k") return setBuf(state.buf.slice(0, state.cur), state.cur, true);
|
|
454
|
-
if (key.ctrl && name === "w") {
|
|
455
|
-
const left = state.buf.slice(0, state.cur).replace(/\s*\S+\s*$/, "");
|
|
456
|
-
return setBuf(left + state.buf.slice(state.cur), left.length, true);
|
|
457
|
-
}
|
|
458
|
-
if (name === "up") {
|
|
459
|
-
if (state.suggest.length) { state.suggestSel = (state.suggestSel - 1 + state.suggest.length) % state.suggest.length; state.buf = state.suggest[state.suggestSel].command; state.cur = state.buf.length; return render(state, ctx); }
|
|
460
|
-
return histNav(1);
|
|
461
|
-
}
|
|
462
|
-
if (name === "down") {
|
|
463
|
-
if (state.suggest.length) { state.suggestSel = (state.suggestSel + 1) % state.suggest.length; state.buf = state.suggest[state.suggestSel].command; state.cur = state.buf.length; return render(state, ctx); }
|
|
464
|
-
return histNav(-1);
|
|
465
|
-
}
|
|
466
|
-
if (shiftTab) {
|
|
467
|
-
if (ctx.onCyclePermission) {
|
|
468
|
-
const next = ctx.onCyclePermission(ctx.permission);
|
|
469
|
-
if (next && typeof next === "object") Object.assign(ctx, next);
|
|
470
|
-
draw();
|
|
471
|
-
}
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
if (name === "tab") {
|
|
475
|
-
if (state.suggest.length) { const cmd = state.suggest[state.suggestSel].command; state.dismissed = cmd; return setBuf(cmd, cmd.length); }
|
|
476
|
-
if (ctx.complete) {
|
|
477
|
-
const res = ctx.complete(state.buf) || [];
|
|
478
|
-
const hits = res[0] || [];
|
|
479
|
-
const token = res[1] || "";
|
|
480
|
-
if (hits.length === 1) {
|
|
481
|
-
const head = token ? state.buf.slice(0, state.buf.length - token.length) : state.buf;
|
|
482
|
-
return setBuf(head + hits[0]);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
// printable insert (single char or paste). Strip control/newlines → single-line field.
|
|
488
|
-
if (str && !key.ctrl && !key.meta) {
|
|
489
|
-
const text = String(str).replace(/[\r\n\t]+/g, " ").replace(/[\x00-\x1f]/g, "");
|
|
490
|
-
if (text) return setBuf(state.buf.slice(0, state.cur) + text + state.buf.slice(state.cur), state.cur + text.length, true);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function histNav(dir) {
|
|
495
|
-
if (!history.length) return;
|
|
496
|
-
if (state.hist === -1 && dir === 1) state.stash = state.buf;
|
|
497
|
-
let i = state.hist + dir;
|
|
498
|
-
if (i < -1) i = -1;
|
|
499
|
-
if (i >= history.length) i = history.length - 1;
|
|
500
|
-
state.hist = i;
|
|
501
|
-
const v = i === -1 ? state.stash : history[i];
|
|
502
|
-
state.buf = v;
|
|
503
|
-
state.cur = v.length;
|
|
504
|
-
// A recalled slash command is history, not a newly opened palette.
|
|
505
|
-
// Keep Up/Down on history until the user edits the recalled value.
|
|
506
|
-
state.dismissed = v;
|
|
507
|
-
draw();
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
inp.on("keypress", onKey);
|
|
511
|
-
// stdout emits `resize` when the real terminal changes dimensions. The
|
|
512
|
-
// composer owns the visible frame while read() is pending, so repaint it
|
|
513
|
-
// immediately instead of leaving a stale soft-wrapped footer until the
|
|
514
|
-
// next keypress.
|
|
515
|
-
if (typeof out.on === "function") out.on("resize", drawSoon);
|
|
516
|
-
draw();
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
return { read, setHistory: (h) => { history = (h || []).filter((x) => typeof x === "string"); } };
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
module.exports = {
|
|
524
|
-
createComposer,
|
|
525
|
-
visWidth,
|
|
526
|
-
buildComposerFrame,
|
|
527
|
-
truncateWidth,
|
|
528
|
-
splitWidth,
|
|
529
|
-
wrapWidth,
|
|
530
|
-
previousGraphemeIndex,
|
|
531
|
-
nextGraphemeIndex,
|
|
532
|
-
};
|