deel-local-cli 1.8.0 → 1.12.0
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.ko.md +10 -25
- package/README.md +43 -51
- package/bin/deel.js +56 -13
- package/package.json +60 -60
- package/src/acp/serve.js +810 -732
- package/src/agent/budget.js +156 -153
- package/src/agent/compact.js +420 -296
- package/src/agent/effort.js +308 -177
- package/src/agent/evidence.js +2 -0
- package/src/agent/evolve.js +263 -213
- package/src/agent/filemem.js +155 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +1707 -1349
- package/src/agent/memory.js +156 -152
- package/src/agent/mention.js +210 -210
- package/src/agent/session.js +1049 -832
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +1159 -615
- package/src/backend/cachemark.js +150 -0
- package/src/backend/detect.js +327 -288
- package/src/backend/learn.js +24 -0
- package/src/backend/mcp.js +96 -5
- package/src/backend/probe.js +131 -61
- package/src/backend/quota.js +250 -133
- package/src/backend/retry.js +30 -6
- package/src/backend/tokens.js +37 -0
- package/src/backend/toolfit.js +352 -0
- package/src/backend/wire.js +600 -0
- package/src/commands.js +3135 -2918
- package/src/config.js +47 -2
- package/src/i18n/en.js +544 -466
- package/src/i18n/index.js +18 -0
- package/src/i18n/ja.js +497 -417
- package/src/i18n/ko.js +591 -466
- package/src/i18n/zh.js +497 -417
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +620 -471
- package/src/pack/sbom.js +30 -4
- package/src/pack/selfpack.js +25 -9
- package/src/pack/sheet.en.js +288 -0
- package/src/pack/tar.js +65 -2
- package/src/plugins/manage.js +46 -9
- package/src/providers/bedrock.js +17 -0
- package/src/repl.js +2602 -2286
- package/src/safety/audit.js +92 -8
- package/src/safety/authcmd.js +14 -3
- package/src/safety/guard.js +143 -0
- package/src/safety/keystore.js +62 -39
- package/src/safety/undo.js +24 -6
- package/src/tools/fsutil.js +265 -250
- package/src/tools/index.js +239 -34
- package/src/tools/jobs.js +158 -29
- package/src/tools/verify.js +358 -328
- package/src/tools/webfetch.js +92 -9
- package/src/ui/md.js +201 -5
- package/src/ui/motion.js +0 -1
- package/src/ui/pastechip.js +50 -3
- package/src/ui/pick.js +115 -0
- package/src/ui/screen.js +23 -3
- package/src/ui/status.js +613 -610
package/src/ui/status.js
CHANGED
|
@@ -1,610 +1,613 @@
|
|
|
1
|
-
// 상태줄. 지금 무엇으로, 어디까지 차서 돌고 있는지 한 줄로 보여 준다.
|
|
2
|
-
//
|
|
3
|
-
// 남의 패키지를 붙이지 않는다. 필요한 숫자는 전부 session 이 이미 갖고 있고,
|
|
4
|
-
// 화면 그리기는 ansi.js 만 쓴다. 반입 심사에 새로 설명할 것이 늘지 않게 하려는 뜻이다.
|
|
5
|
-
import { c, 눈금게이지, width, clip, cols, mark } from './ansi.js';
|
|
6
|
-
import { 마지막할당량, 할당량말, 아슬아슬한가 } from '../backend/quota.js';
|
|
7
|
-
import { 세션요금, 돈셈, 돈말 } from '../backend/price.js';
|
|
8
|
-
import { PROFILES } from '../agent/effort.js';
|
|
9
|
-
import { get as workMode, canWrite, 보일이름 } from '../agent/modes.js';
|
|
10
|
-
import { isLocalHost, isOffline } from '../safety/network.js';
|
|
11
|
-
import { 프록시고르기, 프록시설정 } from '../backend/proxy.js';
|
|
12
|
-
import { COMPACT_AT, FOLD_AT } from '../agent/compact.js';
|
|
13
|
-
import { 말, 언어 } from '../i18n/index.js';
|
|
14
|
-
import { 표시 as 승인표시, 고르기 as 승인고르기 } from './approve.js';
|
|
15
|
-
import { VERSION } from '../version.js';
|
|
16
|
-
|
|
17
|
-
// 저장소 주소. package.json 의 repository 와 같아야 한다 — 검사가 지킨다.
|
|
18
|
-
export const GITHUB = 'https://github.com/jysvai/deel-local-cli';
|
|
19
|
-
|
|
20
|
-
// 한 조각씩 따로 만든다. 좁은 화면에서는 뒤에서부터 떨군다.
|
|
21
|
-
// 순서 = 중요도 순. 앞쪽이 끝까지 살아남는다.
|
|
22
|
-
export const SEGMENTS = {
|
|
23
|
-
/*
|
|
24
|
-
* 경계선 — 소스가 어디로 나가는가.
|
|
25
|
-
*
|
|
26
|
-
* 이 프로그램이 하는 말이 딱 하나다. **내 소스가 이 컴퓨터 밖으로 안 나간다.**
|
|
27
|
-
* 그런데 그 말이 켤 때 머리말에 한 번 뜨고는 스크롤에 밀려 사라졌다. 한 시간
|
|
28
|
-
* 뒤에 /model 로 사내 게이트웨이로 갈아탄 사람은, 화면 어디를 봐도 지금
|
|
29
|
-
* 소스가 밖으로 나가고 있다는 것을 알 수가 없었다.
|
|
30
|
-
*
|
|
31
|
-
* 그래서 한 글자를 상태줄 맨 앞에 박아 둔다. 한 칸이면 좁은 터미널에서도
|
|
32
|
-
* 안 밀린다 — 이건 밀리면 안 되는 것이다.
|
|
33
|
-
* ⌂ 이 컴퓨터 안
|
|
34
|
-
* ↗ 바깥으로 나간다
|
|
35
|
-
* 무슨 뜻인지는 켤 때 머리말에서 한 번 읽고 나면 그 다음부터는 글자만 봐도 안다.
|
|
36
|
-
*/
|
|
37
|
-
// 폴더 이름은 사람이 짓는다 — 한글로 길게 짓기도 한다. 안 자르면
|
|
38
|
-
// 이 조각 하나가 상태줄을 통째로 넘겨 줄이 접힌다(모델 이름은 이미 자르고 있었다).
|
|
39
|
-
//
|
|
40
|
-
// 경계 글자를 **폴더 앞에 붙여서** 낸다. 따로 조각을 두면 가운뎃점까지
|
|
41
|
-
// 네 칸을 먹는데, 100칸 터미널에서 그 네 칸 때문에 승인 방식이 짧은 이름
|
|
42
|
-
// ('위험만')으로 접혔다. 안전 표시를 흐리게 만드는 안전 표시는 손해다.
|
|
43
|
-
// 붙여 쓰면 두 칸이고, '어디 폴더에서 · 어디로 나가나' 가 한 덩이로 읽힌다.
|
|
44
|
-
dir: {
|
|
45
|
-
get desc() { return 말('seg.dir'); },
|
|
46
|
-
make: (s) => `${경계표(s)} ${c.gray(clip(base(s.root), 20))}`,
|
|
47
|
-
short: (s) => `${경계표(s)} ${c.gray(clip(base(s.root), 12))}`,
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
/*
|
|
51
|
-
* 판 번호.
|
|
52
|
-
*
|
|
53
|
-
* 「무슨 판을 쓰고 있나」 는 화면 어디에도 안 떴다 — `deel --version` 을 따로
|
|
54
|
-
* 쳐야 알았다. 그런데 그 질문이 나오는 자리는 대개 뭔가 이상할 때고, 그때
|
|
55
|
-
* 사람은 화면을 캡처해서 보낸다. 캡처에 판 번호가 없으면 무슨 판 이야기인지
|
|
56
|
-
* 부터 다시 물어야 한다. 켤 때 머리말에도 적지만 그건 스크롤에 밀린다.
|
|
57
|
-
*
|
|
58
|
-
* **조각을 따로 둔다.** 처음에는 폴더 이름(dir) 안에 글자로 붙였는데, 그러면
|
|
59
|
-
* 여덟 칸이 늘어난 채로 「자리에 맞나」 를 재게 되어 114칸 아래 터미널에서
|
|
60
|
-
* 온 줄이 짧은 꼴로 바뀌었다 — 승인 방식이 `위험만 확인` 에서 `위험만` 이
|
|
61
|
-
* 됐다. 이 파일이 이미 정해 둔 원칙에 걸린다: **안전 표시를 흐리게 만드는
|
|
62
|
-
* 표시는 손해다.**
|
|
63
|
-
*
|
|
64
|
-
* 조각으로 떼어 두면 자리가 모자랄 때 이것부터 뺄 수 있다(아래 사다리).
|
|
65
|
-
* 판 번호는 밀려도 되는 것이고, 승인 방식은 아니다.
|
|
66
|
-
*/
|
|
67
|
-
ver: {
|
|
68
|
-
get desc() { return 말('seg.ver'); },
|
|
69
|
-
make: () => c.gray(VERSION),
|
|
70
|
-
short: () => null,
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
/*
|
|
74
|
-
* 지금 어느 대화 갈래인가.
|
|
75
|
-
*
|
|
76
|
-
* 갈래를 안 쓰면 **아무것도 안 그린다.** 상태줄은 이미 빽빽해서, 안 쓰는
|
|
77
|
-
* 사람에게까지 한 칸을 더 내주면 좁은 터미널에서 뒤엣것이 떨어져 나간다.
|
|
78
|
-
* 갈래가 둘 이상일 때만 repl 이 session.갈래표 를 채운다(agent/threads.js).
|
|
79
|
-
*/
|
|
80
|
-
thread: {
|
|
81
|
-
get desc() { return 말('seg.thread'); },
|
|
82
|
-
make: (s) => (s.갈래표 ? c.hcyan(`⑂ ${clip(s.갈래표, 12)}`) : null),
|
|
83
|
-
short: (s) => (s.갈래표 ? c.hcyan(`⑂ ${clip(s.갈래표, 6)}`) : null),
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
/*
|
|
87
|
-
* 모델 이름.
|
|
88
|
-
*
|
|
89
|
-
* 좁을 때 더 줄인다. 사내 게이트웨이 이름은 `databricks-gpt-5-6-luna` 처럼
|
|
90
|
-
* 스물세 칸을 그냥 먹는데, 자리가 없을 때 이것 하나 때문에 **셋째 덩이가
|
|
91
|
-
* 통째로 떨어져 나간다** — 승인 방식이 화면에서 사라진다는 뜻이다.
|
|
92
|
-
* 무엇으로 돌고 있는지는 이미 알고 있다. 내 파일이 안 물어보고 바뀌는지는
|
|
93
|
-
* 지금 봐야 안다. 둘 중 하나를 접어야 하면 이쪽이다.
|
|
94
|
-
*/
|
|
95
|
-
model: {
|
|
96
|
-
get desc() { return 말('seg.model'); },
|
|
97
|
-
make: (s) => c.hcyan(clip(s.conn.model, 24)),
|
|
98
|
-
short: (s) => c.hcyan(clip(s.conn.model, 14)),
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
/*
|
|
102
|
-
* 모델 급 — 얼마나 알아서 하나 (agent/grade.js).
|
|
103
|
-
*
|
|
104
|
-
* 컨텍스트 게이지 바로 옆에 둔다. 그 둘이 다른 축이라는 것이 화면에서
|
|
105
|
-
* 나란히 보여야 한다 — 창이 128k 인데 급이 '작음' 인 경우가 실제로 있고,
|
|
106
|
-
* 그때 "왜 이렇게 조심스럽게 하지" 의 답이 이 한 글자다.
|
|
107
|
-
*
|
|
108
|
-
* 짐작일 때는 흐리게 그린다. 정한 것과 짐작한 것을 같은 색으로 보여주면,
|
|
109
|
-
* 사람은 프로그램이 확인한 사실이라고 읽는다.
|
|
110
|
-
*/
|
|
111
|
-
grade: {
|
|
112
|
-
get desc() { return 말('seg.grade'); },
|
|
113
|
-
make: (s) => {
|
|
114
|
-
const g = 볼만한급(s);
|
|
115
|
-
if (!g) return '';
|
|
116
|
-
const 이름 = 급이름(g.급);
|
|
117
|
-
return g.짐작 ? c.gray(`◈ ${이름}?`) : c.white(`◈ ${이름}`);
|
|
118
|
-
},
|
|
119
|
-
short: (s) => {
|
|
120
|
-
const g = 볼만한급(s);
|
|
121
|
-
if (!g) return '';
|
|
122
|
-
// 첫 글자만. 한글이든 영어든 한 글자면 뜻이 남는다 — 작/보/큼, s/m/l.
|
|
123
|
-
const 첫 = [...급이름(g.급)][0] ?? '';
|
|
124
|
-
return g.짐작 ? c.gray(`◈${첫}?`) : c.white(`◈${첫}`);
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
/*
|
|
129
|
-
* 컨텍스트 사용량.
|
|
130
|
-
*
|
|
131
|
-
* 게이지에 **눈금 두 개**를 그어 둔다. 55% 를 넘으면 오래된 도구 결과를 접기
|
|
132
|
-
* 시작하고, 80% 를 넘으면 대화를 요약한다. 둘 다 사람 눈에는 갑자기 일어나는
|
|
133
|
-
* 일이라 — 어느 날 갑자기 "앞선 대화를 줄였습니다" 가 뜨고, 모델이 방금 읽은
|
|
134
|
-
* 파일을 잊는다. 막대가 눈금에 다가가는 것이 보이면 먼저 손을 쓸 수 있다:
|
|
135
|
-
* 못 박아 두거나(/pin), 갈래를 새로 파거나, 지금 하던 것을 먼저 끝내거나.
|
|
136
|
-
*
|
|
137
|
-
* 자리를 코드에 두 번 적지 않는다. compact.js 의 값을 그대로 가져온다 —
|
|
138
|
-
* 문턱을 옮겼는데 화면 눈금만 옛 자리에 남으면, 그 눈금은 거짓말이 된다.
|
|
139
|
-
*/
|
|
140
|
-
ctx: {
|
|
141
|
-
get desc() { return 말('seg.ctx'); },
|
|
142
|
-
make: (s) => {
|
|
143
|
-
const { r, pct, tone, b } = 참값(s);
|
|
144
|
-
return `${눈금게이지(r, 10, [FOLD_AT, COMPACT_AT])} ${tone(pct + '%')} ${c.gray(short(b.used) + '/' + short(b.total))}`;
|
|
145
|
-
},
|
|
146
|
-
// 자리가 모자라면 정확한 숫자를 접는다. 게이지와 %가 이미 같은 말을 하고 있다.
|
|
147
|
-
short: (s) => {
|
|
148
|
-
const { r, pct, tone } = 참값(s);
|
|
149
|
-
return `${눈금게이지(r, 10, [FOLD_AT, COMPACT_AT])} ${tone(pct + '%')}`;
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
// 지금 무슨 일을 하는 중인가. 파일을 못 바꾸는 모드면 자물쇠를 같이 그린다 —
|
|
154
|
-
// 계획만 세우는 중인지 실제로 고치는 중인지 한눈에 보여야 한다.
|
|
155
|
-
work: {
|
|
156
|
-
get desc() { return 말('seg.work'); },
|
|
157
|
-
make: (s) => {
|
|
158
|
-
const 지금 = s.effectiveWork ? s.effectiveWork() : s.work;
|
|
159
|
-
const w = workMode(지금);
|
|
160
|
-
const 잠김 = !canWrite(지금);
|
|
161
|
-
// 저절로 옮겨 간 것인지 사람이 고른 것인지 구분해서 보여준다.
|
|
162
|
-
// 이게 없으면 "왜 갑자기 읽기만 되지" 를 알 길이 없다.
|
|
163
|
-
const 저절로 = s.routed ? c.gray('~') : '';
|
|
164
|
-
// 모드 이름은 표에 영어가 이미 있다(modes.js 의 en). 화면 말이 영어면 그걸 쓴다 —
|
|
165
|
-
// 굳이 i18n 에 같은 말을 또 적어 두면 언젠가 둘이 갈라진다.
|
|
166
|
-
const 이름 = 보일이름(지금);
|
|
167
|
-
const 잠김말 = 언어() === 'en' ? ' read-only' : ' 읽기만';
|
|
168
|
-
return `${c.hcyan(w.glyph)} ${저절로}${잠김 ? c.green(이름) : c.white(이름)}${잠김 ? c.green(잠김말) : ''}`;
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
think: {
|
|
173
|
-
get desc() { return 말('seg.think'); },
|
|
174
|
-
make: (s) => {
|
|
175
|
-
const p = PROFILES[s.effort] ?? PROFILES.save;
|
|
176
|
-
// 배분 이름도 화면 말을 따라간다. 강도(medium)는 어느 말에서나 같은
|
|
177
|
-
// 낱말이라, 여기만 한국어로 남으면 `medium·절약` 처럼 반씩 섞인다.
|
|
178
|
-
const 이름 = 언어() === 'en' ? (p.en ?? p.name) : p.name;
|
|
179
|
-
return `${c.gray('◇')} ${c.white(s.think)}${c.gray('·')}${c.magenta(이름)}`;
|
|
180
|
-
},
|
|
181
|
-
// 배분은 강도보다 덜 급하다. 좁으면 강도만 남긴다.
|
|
182
|
-
short: (s) => `${c.gray('◇')} ${c.white(s.think)}`,
|
|
183
|
-
},
|
|
184
|
-
|
|
185
|
-
/*
|
|
186
|
-
* 승인 방식 — 내 파일이 물어보고 바뀌는가, 안 물어보고 바뀌는가.
|
|
187
|
-
*
|
|
188
|
-
* 전에는 `auto` 라는 영문 한 낱말이었다. 옆에 `종합`·`medium·절약` 이 나란히
|
|
189
|
-
* 있으니 셋 다 그냥 '모드' 로 보였고, 그중 하나가 **묻지 않고 파일을 고친다**
|
|
190
|
-
* 는 뜻이라는 것은 화면 어디에도 없었다. 이건 꾸미기가 아니라 안전 표시다.
|
|
191
|
-
*/
|
|
192
|
-
mode: {
|
|
193
|
-
get desc() { return 말('seg.mode'); },
|
|
194
|
-
make: (s) => 승인표시(s.mode),
|
|
195
|
-
short: (s) => 승인표시(s.mode, { 짧게: true }),
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
tok: {
|
|
199
|
-
get desc() { return 말('seg.tok'); },
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
*
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
['
|
|
299
|
-
['
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
*
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
`${c.gray(자리(말('head.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
`${c.gray(자리(말('head.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
lines.push(`${
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
1
|
+
// 상태줄. 지금 무엇으로, 어디까지 차서 돌고 있는지 한 줄로 보여 준다.
|
|
2
|
+
//
|
|
3
|
+
// 남의 패키지를 붙이지 않는다. 필요한 숫자는 전부 session 이 이미 갖고 있고,
|
|
4
|
+
// 화면 그리기는 ansi.js 만 쓴다. 반입 심사에 새로 설명할 것이 늘지 않게 하려는 뜻이다.
|
|
5
|
+
import { c, 눈금게이지, width, clip, cols, mark } from './ansi.js';
|
|
6
|
+
import { 마지막할당량, 할당량말, 아슬아슬한가 } from '../backend/quota.js';
|
|
7
|
+
import { 세션요금, 돈셈, 돈말 } from '../backend/price.js';
|
|
8
|
+
import { PROFILES } from '../agent/effort.js';
|
|
9
|
+
import { get as workMode, canWrite, 보일이름 } from '../agent/modes.js';
|
|
10
|
+
import { isLocalHost, isOffline } from '../safety/network.js';
|
|
11
|
+
import { 프록시고르기, 프록시설정 } from '../backend/proxy.js';
|
|
12
|
+
import { COMPACT_AT, FOLD_AT } from '../agent/compact.js';
|
|
13
|
+
import { 말, 언어 } from '../i18n/index.js';
|
|
14
|
+
import { 표시 as 승인표시, 고르기 as 승인고르기 } from './approve.js';
|
|
15
|
+
import { VERSION } from '../version.js';
|
|
16
|
+
|
|
17
|
+
// 저장소 주소. package.json 의 repository 와 같아야 한다 — 검사가 지킨다.
|
|
18
|
+
export const GITHUB = 'https://github.com/jysvai/deel-local-cli';
|
|
19
|
+
|
|
20
|
+
// 한 조각씩 따로 만든다. 좁은 화면에서는 뒤에서부터 떨군다.
|
|
21
|
+
// 순서 = 중요도 순. 앞쪽이 끝까지 살아남는다.
|
|
22
|
+
export const SEGMENTS = {
|
|
23
|
+
/*
|
|
24
|
+
* 경계선 — 소스가 어디로 나가는가.
|
|
25
|
+
*
|
|
26
|
+
* 이 프로그램이 하는 말이 딱 하나다. **내 소스가 이 컴퓨터 밖으로 안 나간다.**
|
|
27
|
+
* 그런데 그 말이 켤 때 머리말에 한 번 뜨고는 스크롤에 밀려 사라졌다. 한 시간
|
|
28
|
+
* 뒤에 /model 로 사내 게이트웨이로 갈아탄 사람은, 화면 어디를 봐도 지금
|
|
29
|
+
* 소스가 밖으로 나가고 있다는 것을 알 수가 없었다.
|
|
30
|
+
*
|
|
31
|
+
* 그래서 한 글자를 상태줄 맨 앞에 박아 둔다. 한 칸이면 좁은 터미널에서도
|
|
32
|
+
* 안 밀린다 — 이건 밀리면 안 되는 것이다.
|
|
33
|
+
* ⌂ 이 컴퓨터 안
|
|
34
|
+
* ↗ 바깥으로 나간다
|
|
35
|
+
* 무슨 뜻인지는 켤 때 머리말에서 한 번 읽고 나면 그 다음부터는 글자만 봐도 안다.
|
|
36
|
+
*/
|
|
37
|
+
// 폴더 이름은 사람이 짓는다 — 한글로 길게 짓기도 한다. 안 자르면
|
|
38
|
+
// 이 조각 하나가 상태줄을 통째로 넘겨 줄이 접힌다(모델 이름은 이미 자르고 있었다).
|
|
39
|
+
//
|
|
40
|
+
// 경계 글자를 **폴더 앞에 붙여서** 낸다. 따로 조각을 두면 가운뎃점까지
|
|
41
|
+
// 네 칸을 먹는데, 100칸 터미널에서 그 네 칸 때문에 승인 방식이 짧은 이름
|
|
42
|
+
// ('위험만')으로 접혔다. 안전 표시를 흐리게 만드는 안전 표시는 손해다.
|
|
43
|
+
// 붙여 쓰면 두 칸이고, '어디 폴더에서 · 어디로 나가나' 가 한 덩이로 읽힌다.
|
|
44
|
+
dir: {
|
|
45
|
+
get desc() { return 말('seg.dir'); },
|
|
46
|
+
make: (s) => `${경계표(s)} ${c.gray(clip(base(s.root), 20))}`,
|
|
47
|
+
short: (s) => `${경계표(s)} ${c.gray(clip(base(s.root), 12))}`,
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* 판 번호.
|
|
52
|
+
*
|
|
53
|
+
* 「무슨 판을 쓰고 있나」 는 화면 어디에도 안 떴다 — `deel --version` 을 따로
|
|
54
|
+
* 쳐야 알았다. 그런데 그 질문이 나오는 자리는 대개 뭔가 이상할 때고, 그때
|
|
55
|
+
* 사람은 화면을 캡처해서 보낸다. 캡처에 판 번호가 없으면 무슨 판 이야기인지
|
|
56
|
+
* 부터 다시 물어야 한다. 켤 때 머리말에도 적지만 그건 스크롤에 밀린다.
|
|
57
|
+
*
|
|
58
|
+
* **조각을 따로 둔다.** 처음에는 폴더 이름(dir) 안에 글자로 붙였는데, 그러면
|
|
59
|
+
* 여덟 칸이 늘어난 채로 「자리에 맞나」 를 재게 되어 114칸 아래 터미널에서
|
|
60
|
+
* 온 줄이 짧은 꼴로 바뀌었다 — 승인 방식이 `위험만 확인` 에서 `위험만` 이
|
|
61
|
+
* 됐다. 이 파일이 이미 정해 둔 원칙에 걸린다: **안전 표시를 흐리게 만드는
|
|
62
|
+
* 표시는 손해다.**
|
|
63
|
+
*
|
|
64
|
+
* 조각으로 떼어 두면 자리가 모자랄 때 이것부터 뺄 수 있다(아래 사다리).
|
|
65
|
+
* 판 번호는 밀려도 되는 것이고, 승인 방식은 아니다.
|
|
66
|
+
*/
|
|
67
|
+
ver: {
|
|
68
|
+
get desc() { return 말('seg.ver'); },
|
|
69
|
+
make: () => c.gray(VERSION),
|
|
70
|
+
short: () => null,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* 지금 어느 대화 갈래인가.
|
|
75
|
+
*
|
|
76
|
+
* 갈래를 안 쓰면 **아무것도 안 그린다.** 상태줄은 이미 빽빽해서, 안 쓰는
|
|
77
|
+
* 사람에게까지 한 칸을 더 내주면 좁은 터미널에서 뒤엣것이 떨어져 나간다.
|
|
78
|
+
* 갈래가 둘 이상일 때만 repl 이 session.갈래표 를 채운다(agent/threads.js).
|
|
79
|
+
*/
|
|
80
|
+
thread: {
|
|
81
|
+
get desc() { return 말('seg.thread'); },
|
|
82
|
+
make: (s) => (s.갈래표 ? c.hcyan(`⑂ ${clip(s.갈래표, 12)}`) : null),
|
|
83
|
+
short: (s) => (s.갈래표 ? c.hcyan(`⑂ ${clip(s.갈래표, 6)}`) : null),
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* 모델 이름.
|
|
88
|
+
*
|
|
89
|
+
* 좁을 때 더 줄인다. 사내 게이트웨이 이름은 `databricks-gpt-5-6-luna` 처럼
|
|
90
|
+
* 스물세 칸을 그냥 먹는데, 자리가 없을 때 이것 하나 때문에 **셋째 덩이가
|
|
91
|
+
* 통째로 떨어져 나간다** — 승인 방식이 화면에서 사라진다는 뜻이다.
|
|
92
|
+
* 무엇으로 돌고 있는지는 이미 알고 있다. 내 파일이 안 물어보고 바뀌는지는
|
|
93
|
+
* 지금 봐야 안다. 둘 중 하나를 접어야 하면 이쪽이다.
|
|
94
|
+
*/
|
|
95
|
+
model: {
|
|
96
|
+
get desc() { return 말('seg.model'); },
|
|
97
|
+
make: (s) => c.hcyan(clip(s.conn.model, 24)),
|
|
98
|
+
short: (s) => c.hcyan(clip(s.conn.model, 14)),
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* 모델 급 — 얼마나 알아서 하나 (agent/grade.js).
|
|
103
|
+
*
|
|
104
|
+
* 컨텍스트 게이지 바로 옆에 둔다. 그 둘이 다른 축이라는 것이 화면에서
|
|
105
|
+
* 나란히 보여야 한다 — 창이 128k 인데 급이 '작음' 인 경우가 실제로 있고,
|
|
106
|
+
* 그때 "왜 이렇게 조심스럽게 하지" 의 답이 이 한 글자다.
|
|
107
|
+
*
|
|
108
|
+
* 짐작일 때는 흐리게 그린다. 정한 것과 짐작한 것을 같은 색으로 보여주면,
|
|
109
|
+
* 사람은 프로그램이 확인한 사실이라고 읽는다.
|
|
110
|
+
*/
|
|
111
|
+
grade: {
|
|
112
|
+
get desc() { return 말('seg.grade'); },
|
|
113
|
+
make: (s) => {
|
|
114
|
+
const g = 볼만한급(s);
|
|
115
|
+
if (!g) return '';
|
|
116
|
+
const 이름 = 급이름(g.급);
|
|
117
|
+
return g.짐작 ? c.gray(`◈ ${이름}?`) : c.white(`◈ ${이름}`);
|
|
118
|
+
},
|
|
119
|
+
short: (s) => {
|
|
120
|
+
const g = 볼만한급(s);
|
|
121
|
+
if (!g) return '';
|
|
122
|
+
// 첫 글자만. 한글이든 영어든 한 글자면 뜻이 남는다 — 작/보/큼, s/m/l.
|
|
123
|
+
const 첫 = [...급이름(g.급)][0] ?? '';
|
|
124
|
+
return g.짐작 ? c.gray(`◈${첫}?`) : c.white(`◈${첫}`);
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* 컨텍스트 사용량.
|
|
130
|
+
*
|
|
131
|
+
* 게이지에 **눈금 두 개**를 그어 둔다. 55% 를 넘으면 오래된 도구 결과를 접기
|
|
132
|
+
* 시작하고, 80% 를 넘으면 대화를 요약한다. 둘 다 사람 눈에는 갑자기 일어나는
|
|
133
|
+
* 일이라 — 어느 날 갑자기 "앞선 대화를 줄였습니다" 가 뜨고, 모델이 방금 읽은
|
|
134
|
+
* 파일을 잊는다. 막대가 눈금에 다가가는 것이 보이면 먼저 손을 쓸 수 있다:
|
|
135
|
+
* 못 박아 두거나(/pin), 갈래를 새로 파거나, 지금 하던 것을 먼저 끝내거나.
|
|
136
|
+
*
|
|
137
|
+
* 자리를 코드에 두 번 적지 않는다. compact.js 의 값을 그대로 가져온다 —
|
|
138
|
+
* 문턱을 옮겼는데 화면 눈금만 옛 자리에 남으면, 그 눈금은 거짓말이 된다.
|
|
139
|
+
*/
|
|
140
|
+
ctx: {
|
|
141
|
+
get desc() { return 말('seg.ctx'); },
|
|
142
|
+
make: (s) => {
|
|
143
|
+
const { r, pct, tone, b } = 참값(s);
|
|
144
|
+
return `${눈금게이지(r, 10, [FOLD_AT, COMPACT_AT])} ${tone(pct + '%')} ${c.gray(short(b.used) + '/' + short(b.total))}`;
|
|
145
|
+
},
|
|
146
|
+
// 자리가 모자라면 정확한 숫자를 접는다. 게이지와 %가 이미 같은 말을 하고 있다.
|
|
147
|
+
short: (s) => {
|
|
148
|
+
const { r, pct, tone } = 참값(s);
|
|
149
|
+
return `${눈금게이지(r, 10, [FOLD_AT, COMPACT_AT])} ${tone(pct + '%')}`;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// 지금 무슨 일을 하는 중인가. 파일을 못 바꾸는 모드면 자물쇠를 같이 그린다 —
|
|
154
|
+
// 계획만 세우는 중인지 실제로 고치는 중인지 한눈에 보여야 한다.
|
|
155
|
+
work: {
|
|
156
|
+
get desc() { return 말('seg.work'); },
|
|
157
|
+
make: (s) => {
|
|
158
|
+
const 지금 = s.effectiveWork ? s.effectiveWork() : s.work;
|
|
159
|
+
const w = workMode(지금);
|
|
160
|
+
const 잠김 = !canWrite(지금);
|
|
161
|
+
// 저절로 옮겨 간 것인지 사람이 고른 것인지 구분해서 보여준다.
|
|
162
|
+
// 이게 없으면 "왜 갑자기 읽기만 되지" 를 알 길이 없다.
|
|
163
|
+
const 저절로 = s.routed ? c.gray('~') : '';
|
|
164
|
+
// 모드 이름은 표에 영어가 이미 있다(modes.js 의 en). 화면 말이 영어면 그걸 쓴다 —
|
|
165
|
+
// 굳이 i18n 에 같은 말을 또 적어 두면 언젠가 둘이 갈라진다.
|
|
166
|
+
const 이름 = 보일이름(지금);
|
|
167
|
+
const 잠김말 = 언어() === 'en' ? ' read-only' : ' 읽기만';
|
|
168
|
+
return `${c.hcyan(w.glyph)} ${저절로}${잠김 ? c.green(이름) : c.white(이름)}${잠김 ? c.green(잠김말) : ''}`;
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
think: {
|
|
173
|
+
get desc() { return 말('seg.think'); },
|
|
174
|
+
make: (s) => {
|
|
175
|
+
const p = PROFILES[s.effort] ?? PROFILES.save;
|
|
176
|
+
// 배분 이름도 화면 말을 따라간다. 강도(medium)는 어느 말에서나 같은
|
|
177
|
+
// 낱말이라, 여기만 한국어로 남으면 `medium·절약` 처럼 반씩 섞인다.
|
|
178
|
+
const 이름 = 언어() === 'en' ? (p.en ?? p.name) : p.name;
|
|
179
|
+
return `${c.gray('◇')} ${c.white(s.think)}${c.gray('·')}${c.magenta(이름)}`;
|
|
180
|
+
},
|
|
181
|
+
// 배분은 강도보다 덜 급하다. 좁으면 강도만 남긴다.
|
|
182
|
+
short: (s) => `${c.gray('◇')} ${c.white(s.think)}`,
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/*
|
|
186
|
+
* 승인 방식 — 내 파일이 물어보고 바뀌는가, 안 물어보고 바뀌는가.
|
|
187
|
+
*
|
|
188
|
+
* 전에는 `auto` 라는 영문 한 낱말이었다. 옆에 `종합`·`medium·절약` 이 나란히
|
|
189
|
+
* 있으니 셋 다 그냥 '모드' 로 보였고, 그중 하나가 **묻지 않고 파일을 고친다**
|
|
190
|
+
* 는 뜻이라는 것은 화면 어디에도 없었다. 이건 꾸미기가 아니라 안전 표시다.
|
|
191
|
+
*/
|
|
192
|
+
mode: {
|
|
193
|
+
get desc() { return 말('seg.mode'); },
|
|
194
|
+
make: (s) => 승인표시(s.mode),
|
|
195
|
+
short: (s) => 승인표시(s.mode, { 짧게: true }),
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
tok: {
|
|
199
|
+
get desc() { return 말('seg.tok'); },
|
|
200
|
+
// ↑ 는 **보낸 것**이다. 캐시에 맞은 몫도 보낸 것이라 같이 센다.
|
|
201
|
+
// 서버가 새로 읽은 몫만 세면 캐시가 잘 맞을수록 숫자가 줄어서, 대화가
|
|
202
|
+
// 짧아진 것처럼 보인다 (backend/adapter.js 의 보낸토큰).
|
|
203
|
+
make: (s) => (s.usage.prompt || s.usage.in) || s.usage.out
|
|
204
|
+
? c.gray(`↑${short(s.usage.prompt || s.usage.in)} ↓${short(s.usage.out)}`)
|
|
205
|
+
: null,
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* 여태 쓴 돈.
|
|
210
|
+
*
|
|
211
|
+
* 요금을 모르면 **아무것도 안 그린다.** 여기에 '요금 모름' 을 적으면
|
|
212
|
+
* 로컬로만 쓰는 사람 화면에 평생 그 다섯 글자가 서 있게 된다 — 로컬은
|
|
213
|
+
* 원래 공짜라 그 자리는 영원히 안 채워진다. 모른다는 말은 물어본 자리
|
|
214
|
+
* (/cost)에서 하고, 상태줄은 아는 것만 그린다.
|
|
215
|
+
*
|
|
216
|
+
* 반대로 아는데 안 그리면 안 된다. 바깥 API 는 몇 턴 만에 몇 달러가
|
|
217
|
+
* 나가고, 그걸 화면 밖에 두면 다 쓴 뒤에나 안다.
|
|
218
|
+
*/
|
|
219
|
+
cost: {
|
|
220
|
+
get desc() { return 말('seg.cost'); },
|
|
221
|
+
make: (s) => {
|
|
222
|
+
const 돈 = 돈셈(s.usage, 세션요금(s));
|
|
223
|
+
if (!돈 || !(돈.달러 > 0)) return null;
|
|
224
|
+
return c.gray(돈말(돈.달러));
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
/*
|
|
229
|
+
* 이번 대화에서 **내 폴더에 무슨 일이 있었나** — 다른 축이라 따로 묶는다.
|
|
230
|
+
*
|
|
231
|
+
* 셋 다 아무 일도 없으면 아무것도 안 그린다. 갓 켠 화면에 `✎ 0 · ↩ 0` 이
|
|
232
|
+
* 서 있으면 자리만 먹고, 좁은 터미널에서는 그것 때문에 뒤엣것이 떨어져 나간다.
|
|
233
|
+
*/
|
|
234
|
+
edits: {
|
|
235
|
+
get desc() { return 말('seg.edits'); },
|
|
236
|
+
make: (s) => (s.changes?.size ? c.white(`✎ ${s.changes.size}`) : null),
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* 확인한 것 (Verify).
|
|
241
|
+
*
|
|
242
|
+
* 탈이 있으면 빨갛게, 없으면 초록으로. 이게 중요한 이유는 모델이 "다
|
|
243
|
+
* 됐습니다" 로 답을 맺는 것과 **실제로 돌려 본 것**이 다른 일이기 때문이다.
|
|
244
|
+
* 화면에 초록 ✓ 가 없으면 아직 아무도 안 돌려 본 것이다.
|
|
245
|
+
*/
|
|
246
|
+
verify: {
|
|
247
|
+
get desc() { return 말('seg.verify'); },
|
|
248
|
+
make: (s) => {
|
|
249
|
+
const v = s.검증;
|
|
250
|
+
if (!v?.돈횟수) return null;
|
|
251
|
+
return v.탈 ? c.hred(`✓${v.확인} ✗${v.탈}`) : c.hgreen(`✓${v.확인}`);
|
|
252
|
+
},
|
|
253
|
+
short: (s) => (s.검증?.돈횟수 ? (s.검증.탈 ? c.hred(`✗${s.검증.탈}`) : c.hgreen(`✓${s.검증.확인}`)) : null),
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
/*
|
|
257
|
+
* 되돌릴 수 있는 턴 수.
|
|
258
|
+
*
|
|
259
|
+
* 이 프로젝트는 승인 관문 대신 **되돌리기**로 가기로 했다. 그러면 되돌릴
|
|
260
|
+
* 것이 얼마나 남아 있는지가 안전망의 잔량이다 — 그게 화면 어디에도 없으면
|
|
261
|
+
* "지금 /undo 를 누르면 어디까지 돌아가나" 를 눌러 봐야만 알 수 있다.
|
|
262
|
+
* 이력은 오래되면 잘려 나가므로(undo.js) 이 숫자는 줄어들기도 한다.
|
|
263
|
+
*/
|
|
264
|
+
undoable: {
|
|
265
|
+
get desc() { return 말('seg.undoable'); },
|
|
266
|
+
make: (s) => (s.되돌릴턴 > 0 ? c.gray(`↩ ${s.되돌릴턴}`) : null),
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
tools: { get desc() { return 말('seg.tools'); }, make: (s) => (s.usage.calls ? c.gray(`호출 ${s.usage.calls}`) : null) },
|
|
270
|
+
|
|
271
|
+
skills: {
|
|
272
|
+
get desc() { return 말('seg.skills'); },
|
|
273
|
+
make: (s) => (s.skills?.length ? c.gray(`스킬 ${s.skills.length}`) : null),
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
time: {
|
|
277
|
+
get desc() { return 말('seg.time'); },
|
|
278
|
+
make: (s) => {
|
|
279
|
+
const ms = Date.now() - s.startedAt;
|
|
280
|
+
return c.gray(ms < 60000 ? `${Math.round(ms / 1000)}초` : `${Math.round(ms / 60000)}분`);
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 상태줄에 무엇을, 어떤 순서로.
|
|
287
|
+
*
|
|
288
|
+
* 전에는 칸 여섯 개가 같은 굵기의 막대로 나란히 서 있었다.
|
|
289
|
+
* ▏폴더 ▏모델 ▏게이지 ▏◎ 종합 ▏◇ medium·절약 ▏auto
|
|
290
|
+
* 뒤의 셋은 셋 다 '모드' 처럼 생겨서, 무엇이 무엇인지 읽으려면 멈춰서 세어야 했다.
|
|
291
|
+
*
|
|
292
|
+
* 세 덩이로 묶는다 — **어디서(폴더·모델) · 얼마나 찼나(게이지) · 어떻게 도나(모드)**.
|
|
293
|
+
* 덩이 사이만 굵은 칸막이(▏)로 가르고, 덩이 안은 가운뎃점으로 잇는다.
|
|
294
|
+
* 그러면 눈이 세 번만 멈춘다.
|
|
295
|
+
*/
|
|
296
|
+
export const SEGMENT_GROUPS = [
|
|
297
|
+
['dir', 'ver', 'thread', 'model'],
|
|
298
|
+
['ctx', 'grade'],
|
|
299
|
+
['work', 'think', 'mode'],
|
|
300
|
+
// 내 폴더에 무슨 일이 있었나. 아무 일도 없으면 이 덩이는 아예 안 선다.
|
|
301
|
+
['edits', 'verify', 'undoable'],
|
|
302
|
+
['tok', 'cost'],
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
// 옛 이름. 조각 이름을 직접 넘기던 자리(검사·설정)가 그대로 돌게 남긴다.
|
|
306
|
+
export const DEFAULT_SEGMENTS = SEGMENT_GROUPS.flat();
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 이 급을 화면에 낼 값어치가 있나.
|
|
310
|
+
*
|
|
311
|
+
* '보통' 을 짐작으로 잡은 것은 **아무것도 못 알아낸 상태**다. 사내 게이트웨이가
|
|
312
|
+
* 그렇고, 그건 기본값이지 알아낸 사실이 아니다. 그걸 상태줄에 적으면 자리만
|
|
313
|
+
* 먹고, 좁은 터미널에서는 그 한 조각 때문에 승인 방식이 밀려 사라진다 —
|
|
314
|
+
* 내 파일이 안 물어보고 바뀌는지가 화면에서 없어지는 것이라 훨씬 비싸다.
|
|
315
|
+
*
|
|
316
|
+
* 알아낸 것이 있거나(짐작이 아님) 기본이 아닌 급일 때만 낸다.
|
|
317
|
+
*/
|
|
318
|
+
function 볼만한급(s) {
|
|
319
|
+
if (typeof s.급 !== 'function') return null;
|
|
320
|
+
const g = s.급();
|
|
321
|
+
if (g.급 === '보통' && g.짐작) return null;
|
|
322
|
+
return g;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 컨텍스트 게이지가 쓰는 숫자. 자세한 꼴과 짧은 꼴이 같은 값을 봐야 한다.
|
|
326
|
+
function 참값(s) {
|
|
327
|
+
const b = s.breakdown();
|
|
328
|
+
// 넘칠 수는 있지만 화면에 219% 라고 적으면 고장난 것처럼 보인다. 100 에서 멈춘다.
|
|
329
|
+
const r = Math.min(1, b.total > 0 ? b.used / b.total : 0);
|
|
330
|
+
const pct = Math.round(r * 100);
|
|
331
|
+
/*
|
|
332
|
+
* %의 색을 게이지와 **같은 자리에서** 바꾼다.
|
|
333
|
+
*
|
|
334
|
+
* 전에는 0.6·0.85 로 따로 잡혀 있어서, 62% 에서 숫자만 노래지고 눈금은
|
|
335
|
+
* 아직 멀쩡했다. 두 개가 다른 말을 하면 사람은 둘 다 안 믿는다.
|
|
336
|
+
* 이제 접기(55%)·요약(80%) 자리에서 같이 바뀐다 — 색이 바뀌는 그 순간이
|
|
337
|
+
* 실제로 무슨 일이 나는 순간이다.
|
|
338
|
+
*/
|
|
339
|
+
const tone = r > COMPACT_AT ? c.hred : r > FOLD_AT ? c.hyellow : c.gray;
|
|
340
|
+
return { b, r, pct, tone };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 경계 한 글자.
|
|
345
|
+
*
|
|
346
|
+
* 못 읽는 주소면 아무것도 안 그린다. 여기서 애매하면 **초록을 안 쓴다** —
|
|
347
|
+
* 초록은 '안 나간다' 는 약속이고, 확인 못 한 것을 확인한 낯으로 내밀면 안 된다.
|
|
348
|
+
*/
|
|
349
|
+
function 경계표(s) {
|
|
350
|
+
let 로컬 = null;
|
|
351
|
+
try { 로컬 = isLocalHost(new URL(s.conn.base).hostname); } catch { 로컬 = null; }
|
|
352
|
+
if (로컬 === null) return c.gray('?');
|
|
353
|
+
if (!로컬) return c.hyellow('↗');
|
|
354
|
+
// 오프라인 잠금은 '이 안' 보다 더 센 상태지만 글자는 같게 둔다 — 사람이
|
|
355
|
+
// 봐야 하는 판단은 '나가나 안 나가나' 하나뿐이고, 그 위에 글자를 하나 더
|
|
356
|
+
// 얹으면 정작 그 판단이 흐려진다. 잠금 여부는 켤 때 머리말에 적힌다.
|
|
357
|
+
return c.hgreen('⌂');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/*
|
|
361
|
+
* 실행 모드 한 조각 — `⌂ 이 안 (기본)`.
|
|
362
|
+
*
|
|
363
|
+
* 모드를 안 심어 준 화면(옛 호출부·검사)에서는 아무것도 안 그린다. 모르는
|
|
364
|
+
* 것을 기본값으로 지어내면, 봉인으로 켠 줄 알았는데 화면은 「이 안」 이라고
|
|
365
|
+
* 하는 자리가 생긴다 — 안 보여 주느니만 못하다.
|
|
366
|
+
*
|
|
367
|
+
* 색은 여기서 칠한다. 모드 표(safety/runmode.js)는 자물쇠지 화면이 아니다.
|
|
368
|
+
*/
|
|
369
|
+
const 모드색 = { default: c.hgreen, online: c.hyellow, offline: c.hgreen };
|
|
370
|
+
|
|
371
|
+
function 모드칸(s) {
|
|
372
|
+
const m = s?.실행모드;
|
|
373
|
+
if (!m?.영) return '';
|
|
374
|
+
const 칠 = 모드색[m.영] ?? c.gray;
|
|
375
|
+
return ` ${칠(m.글자)} ${c.gray(말(`head.mode.${m.영}`))}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function base(p) {
|
|
379
|
+
const parts = String(p ?? '').split(/[\\/]/).filter(Boolean);
|
|
380
|
+
return parts[parts.length - 1] ?? '~';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 12345 → 12.1k. 상태줄은 자리가 없다.
|
|
384
|
+
//
|
|
385
|
+
// 1000 이 아니라 1024 로 나눈다. 컨텍스트 길이는 죄다 2의 거듭제곱이라
|
|
386
|
+
// 1000 으로 나누면 131,072 가 '131k' 로 나온다 — 아무도 그렇게 안 부른다.
|
|
387
|
+
// 1024 로 나눠야 128k · 32k · 640k 처럼 실제로 부르는 이름이 나온다.
|
|
388
|
+
// 무엇보다 /ctx 가 받는 단위와 같아야 한다. 화면에 655k 라고 띄워 놓고
|
|
389
|
+
// 655k 를 치면 다른 값이 되는 게 가장 나쁘다.
|
|
390
|
+
function short(n) {
|
|
391
|
+
const v = Number(n) || 0;
|
|
392
|
+
if (v < 1024) return String(v);
|
|
393
|
+
if (v < 1024 * 1024) return (v / 1024).toFixed(v < 10240 ? 1 : 0) + 'k';
|
|
394
|
+
return (v / (1024 * 1024)).toFixed(1) + 'M';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 상태줄 한 줄을 만든다. 폭이 모자라면 덜 중요한 조각부터 뺀다.
|
|
399
|
+
* @param {import('../agent/session.js').Session} session
|
|
400
|
+
*/
|
|
401
|
+
export function statusLine(session, { segments = null, max = cols() - 2 } = {}) {
|
|
402
|
+
const 칸막이 = ` ${c.gray('▏')} `;
|
|
403
|
+
const 안쪽 = c.gray(' · ');
|
|
404
|
+
|
|
405
|
+
// 설정으로 조각 이름을 직접 준 경우에는 그대로 한 줄로 잇는다(옛 방식).
|
|
406
|
+
const 덩이들 = segments
|
|
407
|
+
? segments.map((k) => [k])
|
|
408
|
+
: SEGMENT_GROUPS;
|
|
409
|
+
|
|
410
|
+
const 그리기 = (짧게, 뺄것 = null) => 덩이들
|
|
411
|
+
.map((그룹) => 그룹
|
|
412
|
+
.map((k) => {
|
|
413
|
+
if (뺄것?.has(k)) return null;
|
|
414
|
+
const seg = SEGMENTS[k];
|
|
415
|
+
if (!seg) return null;
|
|
416
|
+
return 짧게 && seg.short ? seg.short(session) : seg.make(session);
|
|
417
|
+
})
|
|
418
|
+
.filter((x) => x != null && x !== '')
|
|
419
|
+
.join(안쪽))
|
|
420
|
+
.filter((x) => x !== '');
|
|
421
|
+
|
|
422
|
+
const 맞나 = (ps) => width(ps.join(칸막이)) + 2 <= max;
|
|
423
|
+
|
|
424
|
+
/*
|
|
425
|
+
* 떨구기 전에 **먼저 줄여 본다.**
|
|
426
|
+
*
|
|
427
|
+
* 처음엔 곧바로 뒤에서부터 떨궜다. 그러면 80칸 터미널에서 모델 이름이 조금만
|
|
428
|
+
* 길어도 '어떻게 도나'(모드·강도·승인) 덩이가 통째로 사라졌다 — 세 덩이로
|
|
429
|
+
* 묶어 놓고 정작 세 번째를 못 보는 화면이 된 것이다.
|
|
430
|
+
*
|
|
431
|
+
* 컨텍스트의 '20k/32k' 는 게이지·%와 같은 말을 하고, 배분 이름은 강도보다
|
|
432
|
+
* 덜 급하다. 그 둘을 접으면 대개 다시 들어간다. 정보를 통째로 잃는 것보다
|
|
433
|
+
* 덜 급한 자리를 접는 편이 낫다.
|
|
434
|
+
*/
|
|
435
|
+
let parts = 그리기(false);
|
|
436
|
+
/*
|
|
437
|
+
* 자리가 모자라면 **판 번호부터** 뺀다. 접기보다도 먼저다.
|
|
438
|
+
*
|
|
439
|
+
* 여기 있는 것 중에 지금 안 봐도 되는 것은 판 번호 하나뿐이다. 나머지는
|
|
440
|
+
* 전부 「지금 어떤 상태냐」 를 말한다. 순서를 안 두고 그냥 접기부터 들어가면
|
|
441
|
+
* 판 번호 여덟 칸 때문에 온 줄이 짧은 꼴로 바뀌고, 그 바람에 승인 방식이
|
|
442
|
+
* `위험만 확인` 에서 `위험만` 이 된다 — 이 파일이 여러 번 적어 둔 그 손해다.
|
|
443
|
+
*/
|
|
444
|
+
if (!맞나(parts) && !segments) parts = 그리기(false, new Set(['ver']));
|
|
445
|
+
if (!맞나(parts)) parts = 그리기(true);
|
|
446
|
+
|
|
447
|
+
/*
|
|
448
|
+
* 그래도 모자라면 **급부터** 접는다.
|
|
449
|
+
*
|
|
450
|
+
* 뒤에서부터 떨구면 승인 방식이 먼저 사라진다. 모델 급은 알아 두면 좋은
|
|
451
|
+
* 것이고, 승인 방식은 지금 봐야 하는 것이다 — 내 파일이 안 물어보고 바뀌는지가
|
|
452
|
+
* 거기 적혀 있다. 둘 중 하나를 접어야 하면 언제나 이쪽이다.
|
|
453
|
+
*/
|
|
454
|
+
if (!맞나(parts) && !segments) parts = 그리기(true, new Set(['grade']));
|
|
455
|
+
|
|
456
|
+
// 여전히 모자라면 뒤에서부터 떨군다. 앞쪽(폴더·모델·컨텍스트)이 마지막까지 남는다.
|
|
457
|
+
while (parts.length > 1 && !맞나(parts)) parts.pop();
|
|
458
|
+
|
|
459
|
+
return ` ${c.gray('▏')}${parts.join(칸막이)}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 컨텍스트가 위험하면 한마디 붙인다. 상태줄만 보고 넘기지 않도록.
|
|
463
|
+
export function contextWarning(session) {
|
|
464
|
+
const b = session.breakdown();
|
|
465
|
+
const r = Math.min(1, b.total > 0 ? b.used / b.total : 0);
|
|
466
|
+
if (r > 0.9) return `${mark.warn} 컨텍스트 ${Math.round(r * 100)}% — ${c.cyan('/compact')} 또는 ${c.cyan('/clear')} 를 권합니다.`;
|
|
467
|
+
if (r > 0.8) return `${c.gray('컨텍스트 ' + Math.round(r * 100) + '% — 곧 오래된 대화를 줄입니다.')}`;
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/*
|
|
472
|
+
* 서버 할당량이 바닥나 가면 한마디 (backend/quota.js).
|
|
473
|
+
*
|
|
474
|
+
* 넉넉할 때는 아무 말도 안 한다. 늘 떠 있는 줄은 사람이 안 읽게 되고,
|
|
475
|
+
* 안 읽는 줄은 정작 급할 때도 안 읽힌다.
|
|
476
|
+
*
|
|
477
|
+
* 여기서 하는 말은 전부 **서버가 말한 숫자**다. 우리가 짐작한 것이 아니다.
|
|
478
|
+
*/
|
|
479
|
+
export function quotaWarning() {
|
|
480
|
+
const 것 = 마지막할당량();
|
|
481
|
+
if (!것 || !아슬아슬한가(것)) return null;
|
|
482
|
+
return `${mark.warn} 서버 할당량 ${할당량말(것)} — 게이트웨이가 알려 준 값입니다.`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 켤 때 한 번 그리는 머리말 상자 안쪽 줄들.
|
|
487
|
+
*/
|
|
488
|
+
/** 머리말 모델 줄에 붙는 급 표시. 못 매기면 아무것도 안 붙는다. */
|
|
489
|
+
/** 급 이름을 화면 말로. 표에는 한글 id 로 들어 있다(agent/grade.js). */
|
|
490
|
+
export function 급이름(급) {
|
|
491
|
+
const 짝 = { 작음: 'grade.small', 보통: 'grade.medium', 큼: 'grade.large' }[급];
|
|
492
|
+
return 짝 ? 말(짝) : String(급 ?? '');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function 급머리말(session) {
|
|
496
|
+
if (typeof session.급 !== 'function') return '';
|
|
497
|
+
const g = session.급();
|
|
498
|
+
return c.gray(말('head.gradeSuffix', {
|
|
499
|
+
급: 말('head.grade'),
|
|
500
|
+
급이름: 급이름(g.급),
|
|
501
|
+
짐작: g.짐작 ? ` ${말('head.guess')}` : '',
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* 켤 때 한 번 보여 주는 머리말.
|
|
507
|
+
*
|
|
508
|
+
* @param {boolean} 상자쓰나 입력 상자를 쓰는 화면인가. Shift+Tab · Alt+Enter ·
|
|
509
|
+
* 줄 끝 백틱은 전부 상자 안에서만 도는 것이라(repl.js 의 `if (상자쓰나)`),
|
|
510
|
+
* 줄 화면(`--no-tui`·파이프·CI)에서 이 줄들을 그대로 찍으면 **되지도 않는
|
|
511
|
+
* 조작법을 알려 주는 셈**이 된다. 안 되는 것을 알려 주는 안내는 없느니만 못하다.
|
|
512
|
+
*/
|
|
513
|
+
export function headerLines(session, found, 상자쓰나 = true) {
|
|
514
|
+
const conn = session.conn;
|
|
515
|
+
const 능력 = [
|
|
516
|
+
conn.streaming ? c.green(말('head.streaming')) : c.gray(말('head.noStreaming')),
|
|
517
|
+
conn.tools ? c.green(말('head.tools')) : c.yellow(말('head.toolsUnknown')),
|
|
518
|
+
conn.think ? c.green(말('head.thinkCtl')) : c.gray(말('head.noThinkCtl')),
|
|
519
|
+
].join(c.gray(' · '));
|
|
520
|
+
|
|
521
|
+
// 코딩 에이전트는 소스를 통째로 모델에 보낸다. '어디로' 가 가장 중요한 한 줄이다.
|
|
522
|
+
let 목적지;
|
|
523
|
+
try {
|
|
524
|
+
const u = new URL(conn.base);
|
|
525
|
+
const 로컬 = isLocalHost(u.hostname);
|
|
526
|
+
목적지 = `${로컬 ? c.green(말('head.inside')) : c.yellow(말('head.outside'))} ${c.white(u.host)}`
|
|
527
|
+
+ (isOffline() ? c.green(` ${말('head.offlineLock')}`) : '');
|
|
528
|
+
// 프록시를 거치면 그것도 첫 화면에. "연결 실패" 가 프록시 탓인지는 이 한 마디로 갈린다.
|
|
529
|
+
const 프록시 = 프록시고르기(conn.base);
|
|
530
|
+
if (프록시) 목적지 += c.gray(` ${말('head.viaProxy', { 프록시: `${프록시.host}:${프록시.port}` })}`);
|
|
531
|
+
// 적어 놨는데 못 쓰는 프록시(socks5 등)는 여기서 한 번은 말해야 한다 — 조용히 직접 가면
|
|
532
|
+
// 사람은 프록시를 탄 줄 알고, 막히면 엉뚱한 데를 고친다.
|
|
533
|
+
else if (프록시설정().탈) 목적지 += c.yellow(` ${말('head.proxyBad')}`);
|
|
534
|
+
} catch { 목적지 = c.gray(String(conn.base)); }
|
|
535
|
+
|
|
536
|
+
/*
|
|
537
|
+
* 이름칸 폭을 **말에 맞춰 잰다.**
|
|
538
|
+
*
|
|
539
|
+
* 8칸으로 못 박아 두었더니 영어에서 'approval'(8칸)만 한 칸도 못 벌려
|
|
540
|
+
* 그 줄만 오른쪽으로 밀렸다. 다른 말을 넣을 때마다 이 숫자를 다시 고르게
|
|
541
|
+
* 하는 대신, 제일 긴 이름에서 뽑는다.
|
|
542
|
+
*/
|
|
543
|
+
const 이름들 = ['head.model', 'head.send', 'head.conn', 'head.folder', 'head.approve', 'head.thisPC'];
|
|
544
|
+
const 이름칸 = Math.max(...이름들.map((k) => width(말(k)))) + 2;
|
|
545
|
+
const 자리 = (글) => `${글}${' '.repeat(Math.max(1, 이름칸 - width(글)))}`;
|
|
546
|
+
const 빈자리 = ' '.repeat(이름칸);
|
|
547
|
+
const lines = [
|
|
548
|
+
/*
|
|
549
|
+
* 판 번호와 실행 모드를 첫 줄에 같이 놓는다.
|
|
550
|
+
*
|
|
551
|
+
* 여태 판 번호는 화면 어디에도 안 떴다 — `deel --version` 을 따로 쳐야
|
|
552
|
+
* 알았다. 그런데 그 질문이 나오는 자리는 대개 뭔가 이상할 때고, 그때
|
|
553
|
+
* 사람은 화면을 캡처해서 보낸다. 캡처에 판 번호가 없으면 무슨 판
|
|
554
|
+
* 이야기인지부터 다시 물어야 한다.
|
|
555
|
+
*
|
|
556
|
+
* 실행 모드도 같은 줄이다. 「지금 나가나 안 나가나」 는 아래 `보냄` 줄이
|
|
557
|
+
* 주소로 말해 주지만, 그건 **어디로** 이지 **나가도 되나** 가 아니다.
|
|
558
|
+
* 봉인(--offline)으로 켠 사람은 그 사실을 첫 줄에서 봐야 한다.
|
|
559
|
+
*/
|
|
560
|
+
`${c.hcyan(c.bold('deel'))} ${c.gray(VERSION)}${모드칸(session)}`
|
|
561
|
+
+ ` ${c.gray(말(`head.spec.${['ollama', 'anthropic'].includes(conn.kind) ? conn.kind : 'openai'}`))}`,
|
|
562
|
+
'',
|
|
563
|
+
/*
|
|
564
|
+
* 모델 줄에 급을 같이 적는다.
|
|
565
|
+
*
|
|
566
|
+
* 창 크기(토큰)와 급은 다른 축인데, 켤 때 이 둘을 나란히 보지 않으면
|
|
567
|
+
* 나중에 상태줄의 `◈ 작음?` 이 무슨 뜻인지 알 길이 없다. 여기서 한 번
|
|
568
|
+
* 같이 읽고 나면 그 다음부터는 글자만 봐도 안다.
|
|
569
|
+
* 짐작이면 그렇다고 적는다 — 알아낸 사실과 같은 낯으로 내밀면 안 된다.
|
|
570
|
+
*/
|
|
571
|
+
`${c.gray(자리(말('head.model')))}${c.white(conn.model)} ${c.gray(`(${short(conn.ctx)} ${말('head.tokens')}`)}`
|
|
572
|
+
+ `${급머리말(session)}${c.gray(')')}`,
|
|
573
|
+
/*
|
|
574
|
+
* 상태줄 맨 앞의 한 글자가 무슨 뜻인지 여기서 한 번 가르친다.
|
|
575
|
+
*
|
|
576
|
+
* 승인 표시(⏵⏵)·급(◈)에 하던 것과 같은 방식이다. 켤 때 한 번 사람 말로
|
|
577
|
+
* 읽고 나면, 그 뒤로는 글자만 봐도 안다. 이 한 글자는 스크롤에 안 밀린다 —
|
|
578
|
+
* 그게 이 머리말 한 줄과 다른 점이고, 이 줄을 넣은 이유다.
|
|
579
|
+
*/
|
|
580
|
+
`${c.gray(자리(말('head.send')))}${목적지} ${c.gray(말('head.nowhereElse'))}`,
|
|
581
|
+
`${빈자리}${c.gray(말('head.glyphHint', { 안: c.hgreen('⌂'), 밖: c.hyellow('↗') }))}`,
|
|
582
|
+
`${c.gray(자리(말('head.conn')))}${능력}`,
|
|
583
|
+
`${c.gray(자리(말('head.folder')))}${c.white(clip(session.root, 56))}`,
|
|
584
|
+
// 켤 때 한 번은 사람 말로 알려 준다. 상태줄의 `⏵⏵ 자동 승인` 이 무슨
|
|
585
|
+
// 뜻인지 여기서 한 번 읽고 나면 그 다음부터는 글자만 봐도 안다.
|
|
586
|
+
`${c.gray(자리(말('head.approve')))}${승인표시(session.mode)} ${c.gray('— ' + 승인고르기(session.mode).한줄)}`,
|
|
587
|
+
];
|
|
588
|
+
if (상자쓰나) {
|
|
589
|
+
lines.push(`${빈자리}${c.gray(말('head.shiftTab'))}${c.gray(말('head.shiftTabHint'))}${c.gray(말('head.tabHint'))}`);
|
|
590
|
+
lines.push(`${빈자리}${c.gray(말('head.newlineHint'))}`);
|
|
591
|
+
}
|
|
592
|
+
if (found.skills.length || found.commands.length) {
|
|
593
|
+
lines.push(`${c.gray(자리(말('head.thisPC')))}${c.white(말('head.skills', { n: found.skills.length }))}${c.gray(' · ')}${c.white(말('head.commands', { n: found.commands.length }))}${c.gray(' · ')}${c.white(말('head.plugins', { n: found.plugins.length }))}`);
|
|
594
|
+
}
|
|
595
|
+
/*
|
|
596
|
+
* 별 부탁 한 줄.
|
|
597
|
+
*
|
|
598
|
+
* 맨 아래에 둔다. 위쪽은 **어디로 소스가 나가는지**를 읽는 자리라, 그
|
|
599
|
+
* 사이에 부탁을 끼우면 정작 읽어야 할 줄이 묻힌다.
|
|
600
|
+
*
|
|
601
|
+
* 회색 한 줄로만 둔다. 켤 때마다 나오는 것이라, 눈에 띄게 만들면 열 번째
|
|
602
|
+
* 켤 때부터는 부탁이 아니라 방해가 된다.
|
|
603
|
+
*
|
|
604
|
+
* 사람이 보고 있을 때만 낸다. 상자를 쓰나(상자쓰나)가 아니라 화면이 터미널인가로
|
|
605
|
+
* 가른다 — `--no-tui` 로 쓰는 사람도 사람이지만, 파이프·CI 로 흘러 들어가는
|
|
606
|
+
* 기록에 부탁 줄이 섞이면 그건 그냥 잡음이다.
|
|
607
|
+
*/
|
|
608
|
+
if (process.stdout.isTTY) {
|
|
609
|
+
lines.push('');
|
|
610
|
+
lines.push(`${빈자리}${c.gray(말('head.star'))} ${c.cyan(GITHUB)}`);
|
|
611
|
+
}
|
|
612
|
+
return lines;
|
|
613
|
+
}
|