deel-local-cli 0.9.0 → 1.0.1
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.en.md +470 -27
- package/README.md +481 -31
- package/bin/deel.js +234 -186
- package/package.json +3 -2
- package/src/agent/compact.js +131 -138
- package/src/agent/effort.js +44 -11
- package/src/agent/loop.js +574 -251
- package/src/agent/memory.js +152 -0
- package/src/agent/mention.js +164 -0
- package/src/agent/modes.js +11 -3
- package/src/agent/recall.js +209 -0
- package/src/agent/salvage.js +182 -0
- package/src/agent/session.js +140 -10
- package/src/agent/store.js +36 -11
- package/src/backend/adapter.js +266 -183
- package/src/backend/ctxsize.js +133 -61
- package/src/backend/detect.js +10 -1
- package/src/backend/learn.js +102 -0
- package/src/backend/mcp.js +304 -0
- package/src/commands.js +493 -53
- package/src/oneshot.js +327 -0
- package/src/repl.js +787 -479
- package/src/report.js +19 -5
- package/src/safety/guard.js +123 -7
- package/src/safety/undo.js +96 -11
- package/src/tools/encoding.js +24 -2
- package/src/tools/fsutil.js +70 -0
- package/src/tools/index.js +471 -27
- package/src/tools/webfetch.js +37 -1
- package/src/ui/ansi.js +13 -1
- package/src/ui/diff.js +255 -0
- package/src/ui/inputbox.js +184 -0
- package/src/ui/level.js +3 -1
- package/src/ui/prompt.js +13 -1
- package/src/ui/screen.js +214 -0
- package/src/ui/status.js +81 -17
- package/src/ui/wrap.js +68 -0
- package/src/version.js +28 -0
package/src/ui/screen.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 화면 한 장.
|
|
3
|
+
*
|
|
4
|
+
* 왜 이 파일이 생겼나:
|
|
5
|
+
* repl.js 안에 say()·process.stdout.write 가 79군데 흩어져 있었다. 그 상태로는
|
|
6
|
+
* 화면을 하나 더 얹을 수가 없다 — 어디로 무엇이 나가는지 한 군데서 못 잡으니
|
|
7
|
+
* 두 화면이 같은 코드를 나눠 쓰려다 결국 둘 다 어중간해진다.
|
|
8
|
+
*
|
|
9
|
+
* 그래서 '무엇을 그린다' 와 '어떻게 그린다' 를 가른다. repl.js 는 이제
|
|
10
|
+
* `화면.줄(...)`·`화면.답조각(...)` 처럼 **뜻**만 말하고, 그것을 줄로 흘릴지
|
|
11
|
+
* 칸에 담아 다시 그릴지는 여기서 정한다.
|
|
12
|
+
*
|
|
13
|
+
* 두 가지 구현이 있다:
|
|
14
|
+
* 줄화면(LineScreen) 지금까지의 그 화면. 위에서 아래로 흘러간다.
|
|
15
|
+
* 파이프·기록·CI·`deel run`·검사가 전부 이것을 읽는다.
|
|
16
|
+
* 상자화면(BoxScreen) ui/inputbox.js. 사람이 터미널 앞에 앉아 있을 때만.
|
|
17
|
+
* **대화는 똑같이 흘려보내고** 맨 아래 입력 상자만
|
|
18
|
+
* 우리가 지우고 다시 그린다.
|
|
19
|
+
*
|
|
20
|
+
* ── 한 번 틀렸던 길 ─────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* 처음에는 터미널을 통째로 빌려(대체 화면) 대화 칸·파일 칸·할 일 칸을 나눠
|
|
23
|
+
* 그렸다. 보기에는 그럴듯했는데 **슬래시 명령이 전부 먹통**이 됐다.
|
|
24
|
+
* commands.js 를 비롯한 여섯 모듈이 화면 객체를 안 거치고 stdout 에 바로 쓰는데,
|
|
25
|
+
* 전체화면은 매번 자기 버퍼에서 화면을 다시 그리므로 그 글이 찍히자마자
|
|
26
|
+
* 덮여 사라졌다. 명령이 안 도는 게 아니라 결과가 안 보이는 것이라 더 나빴다.
|
|
27
|
+
*
|
|
28
|
+
* 고치려면 stdout 에 쓰는 자리를 전부 화면 객체로 꿰야 하는데, 지금 여섯 곳이고
|
|
29
|
+
* 앞으로 늘 것이며, 하나라도 빠뜨리면 같은 증상이 조용히 돌아온다. 그래서
|
|
30
|
+
* 대화는 그냥 흘려보내기로 했다. 그러면 stdout 에 쓰는 모든 코드가 손 안 대고
|
|
31
|
+
* 그대로 맞다. 스크롤·복사·`| tee` 도 그대로다.
|
|
32
|
+
*/
|
|
33
|
+
import { c, say, cursor, box, cols } from './ansi.js';
|
|
34
|
+
import { statusLine, contextWarning } from './status.js';
|
|
35
|
+
import { spin } from './spinner.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 지금 이 자리에서 입력 상자를 써도 되는가.
|
|
39
|
+
*
|
|
40
|
+
* 하나라도 아니면 줄화면이다 — 애매하면 줄화면이 맞다. 잘못 켜면 사용자
|
|
41
|
+
* 화면이 깨지지만, 잘못 안 켜면 그냥 지금까지의 화면일 뿐이다.
|
|
42
|
+
*
|
|
43
|
+
* 상자는 커서를 위로 올려 가며 자기가 그린 줄을 지운다. 그 앞자리가
|
|
44
|
+
* 터미널이 아니면(파이프·기록) 제어문자가 그대로 글에 섞인다.
|
|
45
|
+
*/
|
|
46
|
+
export function 상자쓸까({ tui = null } = {}) {
|
|
47
|
+
if (tui === false) return false; // deel --no-tui
|
|
48
|
+
if (!process.stdout.isTTY) return false; // 파이프·기록·CI
|
|
49
|
+
if (!process.stdin.isTTY) return false; // 입력이 파이프로 들어옴 (검사·데모)
|
|
50
|
+
if (process.env.TERM === 'dumb') return false;
|
|
51
|
+
if (process.env.CI) return false;
|
|
52
|
+
// 너무 좁으면 상자 안에 남는 자리가 없다. 테두리만 남고 글이 안 보인다.
|
|
53
|
+
if ((process.stdout.columns ?? 0) < 40) return false;
|
|
54
|
+
if (tui === true) return true;
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 줄로 흘려보내는 화면 — 지금까지의 그 화면.
|
|
60
|
+
*
|
|
61
|
+
* 여기 있는 메서드 하나하나가 전에 repl.js 에 흩어져 있던 출력 한 줄과
|
|
62
|
+
* **글자 하나까지 같아야 한다.** 검사가 전부 이 화면을 글로 읽고 있어서,
|
|
63
|
+
* 다르면 그 자리에서 잡힌다. 그게 이 갈라내기가 맞는지 재는 자다.
|
|
64
|
+
*/
|
|
65
|
+
export class LineScreen {
|
|
66
|
+
constructor() {
|
|
67
|
+
this.kind = 'line';
|
|
68
|
+
// 곧 지워질 줄이 화면에 있나. \r 로 커서만 앞으로 보내 놓고 안 지우면
|
|
69
|
+
// 다음에 오는 짧은 글이 그 줄 위에 겹쳐 찍힌다.
|
|
70
|
+
this.임시중 = false;
|
|
71
|
+
this.돌림 = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── 흘러가는 글 ────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/** 한 줄. 이미 색이 입혀진 글을 받는다. */
|
|
77
|
+
줄(s = '') { this.임시지움(); say(s); }
|
|
78
|
+
|
|
79
|
+
/** 줄바꿈 없이 이어 붙인다 — 스트리밍으로 오는 답. */
|
|
80
|
+
붙임(s) { this.임시지움(); process.stdout.write(s); }
|
|
81
|
+
|
|
82
|
+
// ── 곧 지워질 표시 ─────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 기다리는 중. \r 로 커서만 앞으로 보낸다.
|
|
86
|
+
*
|
|
87
|
+
* 파이프일 때도 찍는다 — 데모·기록에서 '무엇을 기다리는 중이었나' 가
|
|
88
|
+
* 남아야 한다. 다만 지워야 할 줄로 세는 것은 터미널일 때만이다.
|
|
89
|
+
*/
|
|
90
|
+
기다림(s) {
|
|
91
|
+
process.stdout.write(` ${s}\r`);
|
|
92
|
+
if (process.stdout.isTTY) this.임시중 = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 생각하는 중. 글자 수가 계속 바뀌므로 터미널일 때만 그린다. */
|
|
96
|
+
생각(s) {
|
|
97
|
+
if (!process.stdout.isTTY) return;
|
|
98
|
+
cursor.clearLine();
|
|
99
|
+
process.stdout.write(` ${s}`);
|
|
100
|
+
this.임시중 = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
임시지움() {
|
|
104
|
+
if (this.임시중) { cursor.clearLine(); this.임시중 = false; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 사람이 치고 있던 입력 줄을 지운다.
|
|
109
|
+
*
|
|
110
|
+
* Shift+Tab 으로 작업 모드를 돌릴 때처럼, 입력을 기다리는 도중에 화면에
|
|
111
|
+
* 한 줄을 끼워 넣어야 하는 자리가 있다. 안 지우면 `❯ ` 뒤에 새 글이 붙는다.
|
|
112
|
+
* 상자화면에서는 상자를 통째로 걷어낸다(BoxScreen 이 갈아 끼운다).
|
|
113
|
+
*/
|
|
114
|
+
입력지움() { cursor.clearLine(); }
|
|
115
|
+
|
|
116
|
+
/** 오래 걸리는 일에 돌아가는 표시를 세운다. */
|
|
117
|
+
돌리기(label) { this.돌림 = spin(label); return this.돌림; }
|
|
118
|
+
|
|
119
|
+
// spinner 가 멈추면서 이미 줄을 지우고 커서를 되살린다. 여기서 또 지우지 않는다 —
|
|
120
|
+
// 화면에 나가는 제어문자가 한 벌이라도 달라지면 그게 갈라내기의 흔적이 된다.
|
|
121
|
+
돌림멈춤(finalLine) {
|
|
122
|
+
if (!this.돌림) return;
|
|
123
|
+
this.돌림.stop(finalLine);
|
|
124
|
+
this.돌림 = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── 자리를 차지하는 것 ─────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/** 켤 때 한 번 그리는 머리말 상자. */
|
|
130
|
+
머리말(lines) {
|
|
131
|
+
say('');
|
|
132
|
+
for (const l of box(lines, { tone: c.gray })) say(' ' + l);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 입력 자리. 위에 상태줄을 한 줄 깔고 그 아래에 커서를 둔다.
|
|
137
|
+
*
|
|
138
|
+
* 줄화면에서는 이게 매번 새로 찍힌다 — 대화가 길어지면 상태줄이 화면에
|
|
139
|
+
* 여러 번 남는다. 그게 흘러가는 화면의 성질이고, 기록으로 읽을 때는
|
|
140
|
+
* 오히려 그때그때 상태를 알 수 있어 낫다.
|
|
141
|
+
*/
|
|
142
|
+
입력자리(session) {
|
|
143
|
+
say('');
|
|
144
|
+
say(statusLine(session));
|
|
145
|
+
const w = contextWarning(session);
|
|
146
|
+
if (w) say(` ${w}`);
|
|
147
|
+
process.stdout.write(` ${c.hcyan('❯')} `);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 사람이 치는 중인 글이 바뀌었다. 줄화면은 readline 이 알아서 되비춘다. */
|
|
151
|
+
입력갱신() { }
|
|
152
|
+
|
|
153
|
+
close() {
|
|
154
|
+
this.돌림멈춤();
|
|
155
|
+
this.임시지움();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 대화는 그대로 흘려보내고, 맨 아래 입력 상자만 우리가 관리하는 화면.
|
|
161
|
+
*
|
|
162
|
+
* LineScreen 을 그대로 물려받는다 — 대화가 나가는 길이 **한 글자도 다르면
|
|
163
|
+
* 안 되기** 때문이다. 다르면 검사가 읽는 화면과 사람이 보는 화면이 갈라진다.
|
|
164
|
+
* 여기서 갈아 끼우는 것은 입력 자리 세 가지뿐이다.
|
|
165
|
+
*/
|
|
166
|
+
export class BoxScreen extends LineScreen {
|
|
167
|
+
constructor(상자) {
|
|
168
|
+
super();
|
|
169
|
+
this.kind = 'box';
|
|
170
|
+
this.상자 = 상자;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 대화 한 줄이 나가기 전에 상자를 걷어낸다. 안 걷으면 새 글이 상자 위에
|
|
174
|
+
// 겹쳐 찍히고, 지울 때 대화까지 같이 지워진다.
|
|
175
|
+
줄(s = '') { this.상자.지우기(); super.줄(s); }
|
|
176
|
+
붙임(s) { this.상자.지우기(); super.붙임(s); }
|
|
177
|
+
|
|
178
|
+
/** 입력 자리 = 상태줄 + 상자. 커서는 상자 안에 놓인다. */
|
|
179
|
+
입력자리(session) {
|
|
180
|
+
this.임시지움();
|
|
181
|
+
say('');
|
|
182
|
+
this.상자.그리기(session, '', 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 사람이 치는 중인 글이 바뀌었다 (repl.js 의 keypress 가 부른다). */
|
|
186
|
+
입력갱신(session, 글, 커서) { this.상자.그리기(session, 글, 커서); }
|
|
187
|
+
|
|
188
|
+
입력지움() { this.상자.지우기(); }
|
|
189
|
+
|
|
190
|
+
close() {
|
|
191
|
+
this.상자.지우기();
|
|
192
|
+
super.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 상황에 맞는 화면을 하나 고른다.
|
|
198
|
+
*
|
|
199
|
+
* 상자 쪽은 필요할 때만 읽어 들인다. 줄화면으로 돌 때 inputbox.js 를
|
|
200
|
+
* 파싱조차 안 하게 하려는 것이다 — `deel run` 이 조금이라도 빨리 시작해야 한다.
|
|
201
|
+
*/
|
|
202
|
+
export async function 화면고르기(opts = {}) {
|
|
203
|
+
if (!상자쓸까(opts)) return new LineScreen();
|
|
204
|
+
try {
|
|
205
|
+
const { InputBox } = await import('./inputbox.js');
|
|
206
|
+
return new BoxScreen(new InputBox());
|
|
207
|
+
} catch (e) {
|
|
208
|
+
// 상자를 못 세우면 조용히 줄화면으로 간다. 화면 하나 때문에
|
|
209
|
+
// 프로그램이 안 뜨는 일은 없어야 한다.
|
|
210
|
+
const s = new LineScreen();
|
|
211
|
+
s.줄(` ${c.gray(`입력 상자를 못 켰습니다 — 줄 화면으로 갑니다. (${e.message})`)}`);
|
|
212
|
+
return s;
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/ui/status.js
CHANGED
|
@@ -10,20 +10,23 @@ import { isLocalHost, isOffline } from '../safety/network.js';
|
|
|
10
10
|
// 한 조각씩 따로 만든다. 좁은 화면에서는 뒤에서부터 떨군다.
|
|
11
11
|
// 순서 = 중요도 순. 앞쪽이 끝까지 살아남는다.
|
|
12
12
|
export const SEGMENTS = {
|
|
13
|
-
|
|
13
|
+
// 폴더 이름은 사람이 짓는다 — 한글로 길게 짓기도 한다. 안 자르면
|
|
14
|
+
// 이 조각 하나가 상태줄을 통째로 넘겨 줄이 접힌다(모델 이름은 이미 자르고 있었다).
|
|
15
|
+
dir: { desc: '작업 폴더', make: (s) => c.gray(clip(base(s.root), 20)) },
|
|
14
16
|
|
|
15
17
|
model: { desc: '모델 이름', make: (s) => c.hcyan(clip(s.conn.model, 24)) },
|
|
16
18
|
|
|
17
19
|
ctx: {
|
|
18
20
|
desc: '컨텍스트 사용량',
|
|
19
21
|
make: (s) => {
|
|
20
|
-
const b = s
|
|
21
|
-
// 넘칠 수는 있지만 화면에 219% 라고 적으면 고장난 것처럼 보인다. 100 에서 멈춘다.
|
|
22
|
-
const r = Math.min(1, b.total > 0 ? b.used / b.total : 0);
|
|
23
|
-
const pct = Math.round(r * 100);
|
|
24
|
-
const tone = r > 0.85 ? c.hred : r > 0.6 ? c.hyellow : c.gray;
|
|
22
|
+
const { r, pct, tone, b } = 참값(s);
|
|
25
23
|
return `${gauge(r, 10)} ${tone(pct + '%')} ${c.gray(short(b.used) + '/' + short(b.total))}`;
|
|
26
24
|
},
|
|
25
|
+
// 자리가 모자라면 정확한 숫자를 접는다. 게이지와 %가 이미 같은 말을 하고 있다.
|
|
26
|
+
short: (s) => {
|
|
27
|
+
const { r, pct, tone } = 참값(s);
|
|
28
|
+
return `${gauge(r, 10)} ${tone(pct + '%')}`;
|
|
29
|
+
},
|
|
27
30
|
},
|
|
28
31
|
|
|
29
32
|
// 지금 무슨 일을 하는 중인가. 파일을 못 바꾸는 모드면 자물쇠를 같이 그린다 —
|
|
@@ -47,6 +50,8 @@ export const SEGMENTS = {
|
|
|
47
50
|
const p = PROFILES[s.effort] ?? PROFILES.save;
|
|
48
51
|
return `${c.gray('◇')} ${c.white(s.think)}${c.gray('·')}${c.magenta(p.name)}`;
|
|
49
52
|
},
|
|
53
|
+
// 배분은 강도보다 덜 급하다. 좁으면 강도만 남긴다.
|
|
54
|
+
short: (s) => `${c.gray('◇')} ${c.white(s.think)}`,
|
|
50
55
|
},
|
|
51
56
|
|
|
52
57
|
mode: {
|
|
@@ -80,7 +85,36 @@ export const SEGMENTS = {
|
|
|
80
85
|
},
|
|
81
86
|
};
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
/**
|
|
89
|
+
* 상태줄에 무엇을, 어떤 순서로.
|
|
90
|
+
*
|
|
91
|
+
* 전에는 칸 여섯 개가 같은 굵기의 막대로 나란히 서 있었다.
|
|
92
|
+
* ▏폴더 ▏모델 ▏게이지 ▏◎ 종합 ▏◇ medium·절약 ▏auto
|
|
93
|
+
* 뒤의 셋은 셋 다 '모드' 처럼 생겨서, 무엇이 무엇인지 읽으려면 멈춰서 세어야 했다.
|
|
94
|
+
*
|
|
95
|
+
* 세 덩이로 묶는다 — **어디서(폴더·모델) · 얼마나 찼나(게이지) · 어떻게 도나(모드)**.
|
|
96
|
+
* 덩이 사이만 굵은 칸막이(▏)로 가르고, 덩이 안은 가운뎃점으로 잇는다.
|
|
97
|
+
* 그러면 눈이 세 번만 멈춘다.
|
|
98
|
+
*/
|
|
99
|
+
export const SEGMENT_GROUPS = [
|
|
100
|
+
['dir', 'model'],
|
|
101
|
+
['ctx'],
|
|
102
|
+
['work', 'think', 'mode'],
|
|
103
|
+
['tok'],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// 옛 이름. 조각 이름을 직접 넘기던 자리(검사·설정)가 그대로 돌게 남긴다.
|
|
107
|
+
export const DEFAULT_SEGMENTS = SEGMENT_GROUPS.flat();
|
|
108
|
+
|
|
109
|
+
// 컨텍스트 게이지가 쓰는 숫자. 자세한 꼴과 짧은 꼴이 같은 값을 봐야 한다.
|
|
110
|
+
function 참값(s) {
|
|
111
|
+
const b = s.breakdown();
|
|
112
|
+
// 넘칠 수는 있지만 화면에 219% 라고 적으면 고장난 것처럼 보인다. 100 에서 멈춘다.
|
|
113
|
+
const r = Math.min(1, b.total > 0 ? b.used / b.total : 0);
|
|
114
|
+
const pct = Math.round(r * 100);
|
|
115
|
+
const tone = r > 0.85 ? c.hred : r > 0.6 ? c.hyellow : c.gray;
|
|
116
|
+
return { b, r, pct, tone };
|
|
117
|
+
}
|
|
84
118
|
|
|
85
119
|
function base(p) {
|
|
86
120
|
const parts = String(p ?? '').split(/[\\/]/).filter(Boolean);
|
|
@@ -105,16 +139,46 @@ function short(n) {
|
|
|
105
139
|
* 상태줄 한 줄을 만든다. 폭이 모자라면 덜 중요한 조각부터 뺀다.
|
|
106
140
|
* @param {import('../agent/session.js').Session} session
|
|
107
141
|
*/
|
|
108
|
-
export function statusLine(session, { segments =
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
142
|
+
export function statusLine(session, { segments = null, max = cols() - 2 } = {}) {
|
|
143
|
+
const 칸막이 = ` ${c.gray('▏')} `;
|
|
144
|
+
const 안쪽 = c.gray(' · ');
|
|
145
|
+
|
|
146
|
+
// 설정으로 조각 이름을 직접 준 경우에는 그대로 한 줄로 잇는다(옛 방식).
|
|
147
|
+
const 덩이들 = segments
|
|
148
|
+
? segments.map((k) => [k])
|
|
149
|
+
: SEGMENT_GROUPS;
|
|
150
|
+
|
|
151
|
+
const 그리기 = (짧게) => 덩이들
|
|
152
|
+
.map((그룹) => 그룹
|
|
153
|
+
.map((k) => {
|
|
154
|
+
const seg = SEGMENTS[k];
|
|
155
|
+
if (!seg) return null;
|
|
156
|
+
return 짧게 && seg.short ? seg.short(session) : seg.make(session);
|
|
157
|
+
})
|
|
158
|
+
.filter((x) => x != null && x !== '')
|
|
159
|
+
.join(안쪽))
|
|
160
|
+
.filter((x) => x !== '');
|
|
161
|
+
|
|
162
|
+
const 맞나 = (ps) => width(ps.join(칸막이)) + 2 <= max;
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
* 떨구기 전에 **먼저 줄여 본다.**
|
|
166
|
+
*
|
|
167
|
+
* 처음엔 곧바로 뒤에서부터 떨궜다. 그러면 80칸 터미널에서 모델 이름이 조금만
|
|
168
|
+
* 길어도 '어떻게 도나'(모드·강도·승인) 덩이가 통째로 사라졌다 — 세 덩이로
|
|
169
|
+
* 묶어 놓고 정작 세 번째를 못 보는 화면이 된 것이다.
|
|
170
|
+
*
|
|
171
|
+
* 컨텍스트의 '20k/32k' 는 게이지·%와 같은 말을 하고, 배분 이름은 강도보다
|
|
172
|
+
* 덜 급하다. 그 둘을 접으면 대개 다시 들어간다. 정보를 통째로 잃는 것보다
|
|
173
|
+
* 덜 급한 자리를 접는 편이 낫다.
|
|
174
|
+
*/
|
|
175
|
+
let parts = 그리기(false);
|
|
176
|
+
if (!맞나(parts)) parts = 그리기(true);
|
|
177
|
+
|
|
178
|
+
// 그래도 모자라면 뒤에서부터 떨군다. 앞쪽(폴더·모델·컨텍스트)이 마지막까지 남는다.
|
|
179
|
+
while (parts.length > 1 && !맞나(parts)) parts.pop();
|
|
180
|
+
|
|
181
|
+
return ` ${c.gray('▏')}${parts.join(칸막이)}`;
|
|
118
182
|
}
|
|
119
183
|
|
|
120
184
|
// 컨텍스트가 위험하면 한마디 붙인다. 상태줄만 보고 넘기지 않도록.
|
package/src/ui/wrap.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 색을 유지하면서 폭에 맞춰 접기.
|
|
2
|
+
//
|
|
3
|
+
// 전에는 전체화면(tui.js)에만 쓰던 것이라 거기 있었다. 전체화면을 걷어내면서
|
|
4
|
+
// 여기로 옮긴다 — 입력 상자가 긴 입력을 접을 때 같은 것이 필요하다.
|
|
5
|
+
import { width, clip } from './ansi.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 색을 유지하면서 폭에 맞춰 접는다.
|
|
9
|
+
*
|
|
10
|
+
* clip 은 잘라 버리지만 사람이 친 글은 잘리면 안 된다 — 뒷부분이 통째로
|
|
11
|
+
* 사라진다. 그래서 접는다. 색 코드는 폭이 0 이므로 세지 않고, 줄을 넘길 때
|
|
12
|
+
* 마지막으로 켜져 있던 색을 다음 줄 앞에 다시 켜 준다. 안 그러면 접힌 줄부터
|
|
13
|
+
* 색이 풀려 화면이 얼룩덜룩해진다.
|
|
14
|
+
*/
|
|
15
|
+
export function 접어쓰기(글, 폭) {
|
|
16
|
+
const s = String(글 ?? '');
|
|
17
|
+
if (폭 < 2) return [s];
|
|
18
|
+
if (width(s) <= 폭) return [s];
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* 접힌 줄에 앞 들여쓰기를 물려 준다.
|
|
22
|
+
*
|
|
23
|
+
* 안 물려 주면 ` ▌ 모델의 답…` 이 접히는 순간 다음 줄이 왼쪽 끝에서
|
|
24
|
+
* 시작한다. 화면에서 세로줄이 끊겨 보이고, 도구 결과의 ` └ …` 도
|
|
25
|
+
* 두 번째 줄부터 갑자기 튀어나온다 — 무엇에 딸린 글인지 알 수 없게 된다.
|
|
26
|
+
* 들여쓴 만큼은 글자 자리에서 빼야 하므로 접는 폭도 같이 줄인다.
|
|
27
|
+
*/
|
|
28
|
+
const 들여 = (/^ */.exec(s)[0] ?? '').slice(0, Math.max(0, 폭 - 8));
|
|
29
|
+
const 이어폭 = Math.max(2, 폭 - 들여.length);
|
|
30
|
+
|
|
31
|
+
const 줄들 = [];
|
|
32
|
+
let 지금 = '';
|
|
33
|
+
let w = 0;
|
|
34
|
+
let 색 = ''; // 지금 켜져 있는 SGR
|
|
35
|
+
for (let i = 0; i < s.length;) {
|
|
36
|
+
// 색 코드는 통째로 옮긴다. 폭에는 안 센다.
|
|
37
|
+
if (s[i] === '\x1b') {
|
|
38
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
|
|
39
|
+
if (m) {
|
|
40
|
+
지금 += m[0];
|
|
41
|
+
색 = /\x1b\[0?m/.test(m[0]) ? '' : 색 + m[0];
|
|
42
|
+
i += m[0].length;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const ch = s[i];
|
|
47
|
+
const cw = width(ch);
|
|
48
|
+
// 첫 줄은 원래 폭, 접힌 줄부터는 들여쓴 만큼 좁아진다.
|
|
49
|
+
const 한도 = 줄들.length === 0 ? 폭 : 이어폭 + 들여.length;
|
|
50
|
+
if (w + cw > 한도) {
|
|
51
|
+
줄들.push(지금 + (색 ? '\x1b[0m' : ''));
|
|
52
|
+
지금 = 들여 + 색;
|
|
53
|
+
w = 들여.length;
|
|
54
|
+
}
|
|
55
|
+
지금 += ch;
|
|
56
|
+
w += cw;
|
|
57
|
+
i += 1;
|
|
58
|
+
}
|
|
59
|
+
if (지금.trim() !== '' || 줄들.length === 0) 줄들.push(지금 + (색 ? '\x1b[0m' : ''));
|
|
60
|
+
return 줄들;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 폭에 맞춰 오른쪽을 공백으로 채운다. 안 채우면 앞 그림이 남는다. */
|
|
64
|
+
export function 채움(글, 폭) {
|
|
65
|
+
const w = width(글);
|
|
66
|
+
if (w > 폭) return clip(글, 폭);
|
|
67
|
+
return 글 + ' '.repeat(폭 - w);
|
|
68
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// 판 번호를 한 곳에서만 읽는다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 파일 하나를 따로 두나: 전에는 backend/mcp.js 가 '0.9.0' 을 직접 적어
|
|
4
|
+
// 들고 있었다. package.json 을 올릴 때 그건 안 올라간다 — 아무 데서도 안
|
|
5
|
+
// 터지고, 검사도 안 걸리고, MCP 서버 쪽 기록에만 옛 번호가 남는다.
|
|
6
|
+
// 이런 것은 반드시 어긋나므로 애초에 두 벌을 안 만든다.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* 못 읽어도 죽지 않는다.
|
|
13
|
+
*
|
|
14
|
+
* package.json 은 npm 이 언제나 같이 깔아 주지만, 소스만 풀어 쓰는 길도
|
|
15
|
+
* 열어 둔 도구다(README 의 `git clone` 뒤 바로 실행). 판 번호 하나 때문에
|
|
16
|
+
* 프로그램이 안 뜨는 것은 말이 안 된다.
|
|
17
|
+
*/
|
|
18
|
+
function 읽기() {
|
|
19
|
+
try {
|
|
20
|
+
const 여기 = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(여기, '..', 'package.json'), 'utf8'));
|
|
22
|
+
return String(pkg.version ?? '0.0.0');
|
|
23
|
+
} catch {
|
|
24
|
+
return '0.0.0';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const VERSION = 읽기();
|