deel-local-cli 1.20.8 → 1.20.9

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.
@@ -72,6 +72,8 @@ const 자가치유주기 = 4000;
72
72
  const 앞머리 = 5;
73
73
  // 상자가 차지할 최대 줄 수. 긴 글을 붙여넣어도 화면을 다 먹지 않게.
74
74
  export const 안쪽최대 = 8;
75
+ // 일하는 중에 미리 쳐 두는 글은 이보다 좁게 보인다 — 위쪽 '일하는 중' 줄을 안 밀어내게.
76
+ export const 대기최대 = 3;
75
77
 
76
78
  /**
77
79
  * 상자를 이룰 줄들과 커서 자리를 값으로 내놓는다.
@@ -117,8 +119,23 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
117
119
  */
118
120
  if (사무실?.length) 줄들.push(...사무실);
119
121
 
120
- if (상태) 줄들.push(상태);
121
- if (경고) 줄들.push(` ${경고}`);
122
+ /*
123
+ * ── 여기 얹는 줄은 **반드시 폭 안에 넣는다** ─────────────────────────
124
+ *
125
+ * 상자는 아래 몇 줄을 붙박아 두고 그 자리에서 다시 그린다 — 그 셈이
126
+ * `줄들.length` 다. 한 줄이 터미널보다 넓으면 터미널은 그것을 두 줄로
127
+ * 접는데 상자는 한 줄로 세고, 그만큼 커서를 덜 올린다. 그러면 키를 칠
128
+ * 때마다 위쪽 대화가 한 줄씩 갉여 나가고 테두리 잔상이 쌓인다.
129
+ *
130
+ * 실제로 재 본 자리: 폭 80 에서 할당량 경고가 101칸이었다. 상자는 5줄로
131
+ * 셌고 터미널은 7줄을 썼다.
132
+ *
133
+ * 상태줄은 자기 자리에서 이미 줄이지만(ui/status.js), 덩이 하나가 폭보다
134
+ * 넓으면 그대로 나온다 — 그래서 여기서 한 번 더 자른다. 경고는 뜻이
135
+ * 뒤쪽에 있는 문장이라 자르지 않고 **접는다.**
136
+ */
137
+ if (상태) 줄들.push(clip(상태, 칸));
138
+ if (경고) for (const 줄 of 접어쓰기(` ${경고}`, 칸)) 줄들.push(줄);
122
139
 
123
140
  const 가로 = '─'.repeat(칸 - 3);
124
141
  줄들.push(테(` ╭${가로}╮`));
@@ -144,8 +161,16 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
144
161
  const 왼쪽 = `${c.hcyan(일감.돌림 ?? '⠋')} ${c.white(일감.말 ?? '')}${c.gray('…')}`;
145
162
  const 오른쪽 = c.gray(일감.곁 ?? '');
146
163
  const 남 = 일안쪽 - width(왼쪽) - width(오른쪽);
147
- // 자리가 모자라면 오른쪽(걸린 시간·중단 안내)을 버린다. 왼쪽이 본문이다.
148
- const = >= 2 ? `${왼쪽}${' '.repeat(남)}${오른쪽}` : 왼쪽;
164
+ /*
165
+ * 자리가 모자라면 오른쪽(걸린 시간·중단 안내)을 버린다. 왼쪽이 본문이다.
166
+ *
167
+ * 오른쪽을 버려도 **왼쪽 하나가 폭보다 넓을 수 있다.** 일감의 말은 부르는
168
+ * 쪽에서 오는 글이라(하위 작업 목적·도구 이름·파일 경로) 길이를 여기서
169
+ * 정하지 못한다. 안 자르고 내보내면 위 머리글이 적어 둔 그 손해가 그대로
170
+ * 난다 — 터미널은 두 줄로 접고 상자는 한 줄로 세서, 키를 칠 때마다
171
+ * 위쪽 대화가 한 줄씩 갉여 나간다.
172
+ */
173
+ const 안 = 남 >= 2 ? `${왼쪽}${' '.repeat(남)}${오른쪽}` : clip(왼쪽, 일안쪽);
149
174
  줄들.push(` ${테('│')} ${안}${' '.repeat(Math.max(0, 일안쪽 - width(안)))} ${테('│')}`);
150
175
 
151
176
  /*
@@ -157,22 +182,31 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
157
182
  * 아무것도 안 보여서, 쳐도 안 먹는 줄 알았을 뿐이다.
158
183
  *
159
184
  * 그래서 보여만 준다. 새로 받는 게 아니라 **이미 받고 있던 것**이다.
185
+ *
186
+ * 세 줄을 넘으면 **뒤에서 세 줄**을 보인다.
187
+ *
188
+ * 앞서는 앞에서 세 줄을 보였다. 사람은 끝에서 치고 있는데 화면에는 처음
189
+ * 세 줄이 붙어 있으니, 네 줄째부터는 친 글자가 아무 데도 안 나타났다 —
190
+ * 그리고 커서는 셋째 줄 끝에 서 있었다. 키가 안 먹는 것처럼 보이는데
191
+ * 실은 다 받고 있었다. 가린 줄이 있으면 ❯ 를 … 로 바꿔 알린다
192
+ * (아래 본 상자가 쓰는 것과 같은 표시다).
160
193
  */
161
- if (일감.대기) {
162
- const 대기줄들 = 접어쓰기(일감.대기, 안쪽).slice(0, 3);
163
- for (const [i, 한줄] of 대기줄들.entries()) {
164
- const 표 = i === 0 ? c.hcyan('❯') : ' ';
165
- 줄들.push(` ${테('│')} ${} ${한줄}${' '.repeat(Math.max(0, 안쪽 - width(한줄)))} ${테('')}`);
166
- }
194
+ const 대기줄들 = 일감.대기 ? 접어쓰기(일감.대기, 안쪽) : [];
195
+ const 대기보일 = 대기줄들.slice(-대기최대);
196
+ const 대기넘침 = 대기줄들.length > 대기최대;
197
+ for (const [i, 한줄] of 대기보일.entries()) {
198
+ const= i === 0 ? (대기넘침 ? c.gray('…') : c.hcyan('')) : ' ';
199
+ 줄들.push(` ${테('│')} ${표} ${한줄}${' '.repeat(Math.max(0, 안쪽 - width(한줄)))} ${테('│')}`);
167
200
  }
168
201
 
169
202
  줄들.push(테(` ╰${가로}╯`));
170
- if (일감.대기) 줄들.push(` ${c.gray('Enter 치면 지금 일이 끝난 뒤에 보냅니다')}`);
171
- else if (곁말) 줄들.push(` ${c.gray(곁말)}`);
203
+ // 아래 줄도 안에 넣는다 안내 문구는 43칸이라 좁은 창에서 접힌다.
204
+ if (일감.대기) 줄들.push(clip(` ${c.gray('Enter 를 치면 지금 일이 끝난 뒤에 보냅니다')}`, 칸));
205
+ else if (곁말) 줄들.push(clip(` ${c.gray(곁말)}`, 칸));
172
206
 
173
207
  // 커서는 미리 치는 중일 때만 보여 준다. 아니면 칠 자리가 아니다.
174
208
  if (일감.대기) {
175
- const 마지막 = 접어쓰기(일감.대기, 안쪽).slice(0, 3).at(-1) ?? '';
209
+ const 마지막 = 대기보일.at(-1) ?? '';
176
210
  return { 줄들, 커서: { 위: 2, 열: Math.min(앞머리 + 1 + width(마지막), 칸 - 1) } };
177
211
  }
178
212
  return { 줄들, 커서: { 위: (곁말 ? 1 : 0), 열: 1 } };
@@ -184,7 +218,24 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
184
218
  */
185
219
  const 안쪽줄 = 글 ? 접어쓰기(글, 안쪽) : [''];
186
220
  const 넘침 = 안쪽줄.length > 안쪽최대;
187
- const 보일줄 = 넘침 ? 안쪽줄.slice(안쪽줄.length - 안쪽최대) : 안쪽줄;
221
+ /*
222
+ * 보일 자리는 **커서를 따라간다.**
223
+ *
224
+ * 앞서는 언제나 마지막 여덟 줄만 보여 줬다. 그래서 열두 줄짜리 글을
225
+ * 붙여넣고 Ctrl+A 로 맨 앞에 가면, 커서가 든 줄이 화면에 없었다 — 그때
226
+ * 커서 줄 번호는 0 으로 깎이므로 커서는 **보이는 첫 줄**에 그려지고,
227
+ * 치는 글자는 안 보이는 줄에 들어간다. 커서 자리에 아무것도 안 나타나니
228
+ * 키가 안 먹는 것처럼 보인다.
229
+ *
230
+ * 끝에서 치는 보통 경우에는 예전과 똑같이 마지막 여덟 줄이 보인다.
231
+ */
232
+ const 커서앞줄수 = (커서자리 == null ? 글 : String(글).slice(0, 커서자리))
233
+ ? 접어쓰기(커서자리 == null ? 글 : String(글).slice(0, 커서자리), 안쪽).length
234
+ : 1;
235
+ const 첫줄 = 넘침
236
+ ? Math.min(Math.max(0, 커서앞줄수 - 안쪽최대), 안쪽줄.length - 안쪽최대)
237
+ : 0;
238
+ const 보일줄 = 안쪽줄.slice(첫줄, 첫줄 + 안쪽최대);
188
239
 
189
240
  for (const [i, 한줄] of 보일줄.entries()) {
190
241
  const 표 = i === 0 ? (넘침 ? c.gray('…') : c.hcyan('❯')) : ' ';
@@ -227,7 +278,8 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
227
278
  }
228
279
  }
229
280
 
230
- if (곁말) 줄들.push(` ${c.gray(곁말)}`);
281
+ // 곁말도 폭 안에 넣는다 — 위 머리글의 그 셈( )이 어긋난다.
282
+ if (곁말) 줄들.push(clip(` ${c.gray(곁말)}`, 칸));
231
283
 
232
284
  /*
233
285
  * 커서 자리.
@@ -239,9 +291,8 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
239
291
  const 앞글 = 커서자리 == null ? 글 : String(글).slice(0, 커서자리);
240
292
  const 앞줄들 = 앞글 ? 접어쓰기(앞글, 안쪽) : [''];
241
293
 
242
- // 넘쳐서 앞부분을 잘라 냈으면 그만큼 줄 번호가 밀린다.
243
- const 숨긴줄 = 안쪽줄.length - 보일줄.length;
244
- const 커서줄 = Math.min(Math.max(0, 앞줄들.length - 1 - 숨긴줄), 보일줄.length - 1);
294
+ // 위로 접어 줄만큼 줄 번호가 밀린다 (보일 자리는 커서를 따라 옮긴다).
295
+ const 커서줄 = Math.min(Math.max(0, 앞줄들.length - 1 - 첫줄), 보일줄.length - 1);
245
296
 
246
297
  // 커서 아래에 몇 줄이 더 있나 — 닫는 테두리 + 추천 목록 + 곁말.
247
298
  const 아래 = 1 + 추천줄수 + (곁말 ? 1 : 0);
@@ -400,7 +451,7 @@ export class InputBox {
400
451
  일시작(session, 갈래 = '기본') {
401
452
  this.session = session ?? this.session;
402
453
  // 대기 = 일하는 도중에 미리 치고 있는 글, 예약수 = 이미 Enter 를 친 것들.
403
- this.일감 = { 갈래, 시작: Date.now(), 곁정보: '', 대기: '', 예약수: 0 };
454
+ this.일감 = { 갈래, 시작: Date.now(), 바뀐때: Date.now(), 곁정보: '', 대기: '', 예약수: 0 };
404
455
  this.틱 = 0;
405
456
  this.일그리기();
406
457
  if (this.박자) clearInterval(this.박자);
@@ -414,7 +465,7 @@ export class InputBox {
414
465
  if (!this.일감) return;
415
466
  // 갈래가 바뀌면 문구를 그 자리에서 갈아 준다. 4초를 기다리면 이미 다음
416
467
  // 일로 넘어가 있어서, 화면이 늘 한 걸음 늦은 말을 하게 된다.
417
- if (갈래 && 갈래 !== this.일감.갈래) { this.일감.갈래 = 갈래; this.틱 = 0; }
468
+ if (갈래 && 갈래 !== this.일감.갈래) { this.일감.갈래 = 갈래; this.일감.바뀐때 = Date.now(); this.틱 = 0; }
418
469
  if (곁정보 !== null) this.일감.곁정보 = 곁정보;
419
470
  this.일그리기();
420
471
  }
@@ -452,7 +503,18 @@ export class InputBox {
452
503
  * 「멈추는 중」이 화면에 영영 안 뜨고, 사람은 안 먹었다고 여겨 또 누른다.
453
504
  */
454
505
  const 멈추나 = this.일감.갈래 === '멈춤';
455
- const 갈래 = (!멈추나 && 지난 >= 느긋해질때) ? '느긋' : this.일감.갈래;
506
+ /*
507
+ * 느긋으로 넘어가는 셈은 **턴이 시작된 뒤**가 아니라 **갈래가 바뀐 뒤**로
508
+ * 잰다.
509
+ *
510
+ * 시작부터 재면 45초가 지난 순간부터 턴이 끝날 때까지 갈래가 영영
511
+ * 느긋에 붙박인다 — 그 뒤로 파일을 다섯 개 읽고 세 군데를 고쳐도 화면은
512
+ * 계속 '아직 하는 중' 만 돈다. 로컬 모델은 한 턴이 몇 분이라 사실상 늘
513
+ * 그 상태다. 이 문구를 넣은 뜻이 「아까 하던 말을 계속 돌리면 붙박이처럼
514
+ * 보인다」 였는데, 시작부터 재면 그 붙박이를 이 문구가 만든다.
515
+ */
516
+ const 멎은지 = Date.now() - (this.일감.바뀐때 ?? this.일감.시작);
517
+ const 갈래 = (!멈추나 && 멎은지 >= 느긋해질때) ? '느긋' : this.일감.갈래;
456
518
  const 회차 = Math.floor((this.틱 * 돌림주기) / 문구주기);
457
519
  this.그리기(this.session, '', 0, {
458
520
  // 갈래에 맞는 작은 그림. 무엇을 하는 중인지 글자와 그림이 같이 말한다.
@@ -551,16 +613,34 @@ export class InputBox {
551
613
  *
552
614
  * 높이만 바뀌었다 — 줄 접힘은 폭이 정한다. 폭이 그대로면 접힘도 그대로라
553
615
  * 그린줄·커서위 가 아직 맞다. 평소처럼 제대로 지운다.
554
- * 폭이 바뀌었다 — 옛 수는 다 틀어졌다. 대신 커서 아래를 통째로 쓸어낸다.
555
- * 상자는 늘 맨 아래에 있어서 그 아래에는 아무것도 없다.
556
- * 위로는 올라가니 대화를 건드릴 일이 없다.
616
+ * 폭이 바뀌었다 — 옛 수는 다 틀어졌다. 대신 상자 윗줄로 올라가
617
+ * 거기서 아래를 통째로 쓸어낸다. 상자는 늘 맨 아래에
618
+ * 있어서 아래에는 아무것도 없고, 윗줄보다 위로는
619
+ * 안 올라가니 대화를 건드릴 일이 없다.
557
620
  */
558
621
  const 높이 = process.stdout.rows ?? null;
559
622
  if (this.그린폭 !== 폭 || this.그린높이 !== 높이) {
560
623
  if (this.그린폭 === 폭) {
561
624
  this.지우기();
562
625
  } else {
563
- if (this.그린줄) process.stdout.write('\r\x1b[J');
626
+ /*
627
+ * 쓸어내기 전에 **상자 맨 윗줄로 올라간다.**
628
+ *
629
+ * 커서는 상자 한가운데(아래에서 커서위 번째 줄)에 서 있다. 거기서
630
+ * 곧바로 쓸어내면 커서 아래만 지워지고 **위쪽 절반이 그대로 남는다** —
631
+ * 사무실·상태줄·윗테두리·그 위 입력 줄이 유령으로 붙어 있고, 새 상자는
632
+ * 그 아래에 또 그려진다. 상자가 두 개로 보이고 유령은 대화와 함께 위로
633
+ * 밀려 올라가 영영 안 없어진다. 흉내 화면으로 재 보니 80→60 에서 네 줄
634
+ * (윗테두리·입력 줄이 접힌 것)이 남았다.
635
+ *
636
+ * 올라가는 거리는 **옛 폭에서 세던 그 거리**를 쓴다. 좁아지면 터미널이
637
+ * 다시 접어 상자가 더 여러 줄을 차지하므로 이 거리는 모자랄 수 있는데,
638
+ * 모자라면 유령이 몇 줄 남을 뿐이다. 넘치면 대화를 지운다 — 이 저장소가
639
+ * 제일 나쁜 것으로 꼽는 그 손해다. 그래서 모자라는 쪽으로 잡는다.
640
+ */
641
+ if (this.그린줄) {
642
+ process.stdout.write('\r' + 위로(Math.max(0, this.그린줄 - 1 - this.커서위)) + '\x1b[J');
643
+ }
564
644
  this.앞줄들 = null;
565
645
  this.그린줄 = 0;
566
646
  this.커서위 = 0;
package/src/ui/level.js CHANGED
@@ -58,7 +58,15 @@ export function normalize(v) {
58
58
  '쉬움': '쉬움', '초보': '쉬움', '초보자': '쉬움', 'beginner': '쉬움', 'easy': '쉬움', 'b': '쉬움',
59
59
  '개발자': '개발자', '고급': '개발자', 'developer': '개발자', 'dev': '개발자', 'advanced': '개발자', 'd': '개발자',
60
60
  };
61
- return 별명[s] ?? (LEVELS[v] ? v : null);
61
+ /*
62
+ * 표에 **제 것으로 있는** 열쇠만 본다.
63
+ *
64
+ * `별명[s]` 는 물려받은 열쇠에도 값을 준다. `/level constructor` 면 함수
65
+ * 하나가 수준 이름으로 통과해서, 그 뒤 shows() 가 `lv.show` 를 읽다가
66
+ * 터진다 — 명령 목록을 그리는 자리라 대화가 통째로 끝난다.
67
+ */
68
+ if (Object.hasOwn(별명, s)) return 별명[s];
69
+ return Object.hasOwn(LEVELS, v) ? v : null;
62
70
  }
63
71
 
64
72
  export function get(id) {
package/src/ui/office.js CHANGED
@@ -577,7 +577,21 @@ export function 상태만들기(session, 일감, 틱 = 0, 할일들 = [], 도는
577
577
  const s = session ?? {};
578
578
  const 쓴것 = s.usage ?? {};
579
579
  const 총토큰 = (쓴것.in ?? 0) + (쓴것.out ?? 0);
580
- const 창크기 = s.conn?.ctx ?? 0;
580
+ /*
581
+ * 창밖 하늘은 **지금 창이 얼마나 찼나**다. 상태줄 게이지와 같은 값을 봐야 한다.
582
+ *
583
+ * 앞서는 누적 토큰(in+out)으로 셌다. 그건 부를 때마다 늘기만 하는 값이라
584
+ * 지금 창과 아무 상관이 없다 — 32k 모델에서 도구를 열 번 부르면 42,000 이
585
+ * 되어 창이 1.00, 하늘이 **밤으로 굳는다.** 같은 화면 아래쪽 게이지는 18%
586
+ * 초록이었다. 「노을이면 슬슬 접을 때고 밤이면 이미 늦은 것」 이라고 아래
587
+ * 주석에 뜻을 못 박아 둔 신호가, 켠 직후부터 거짓말을 하고 있었다.
588
+ * `/clear` 로도 안 밝아진다 — 누적은 지우지 않으니까.
589
+ *
590
+ * breakdown() 은 상태줄이 이미 프레임마다 부르는 것이다(ui/status.js).
591
+ * 여기서 한 번 더 부르는 값은 그 자리에서 본 값과 **어긋나지 않는 것**이다.
592
+ */
593
+ const 예산 = typeof s.breakdown === 'function' ? s.breakdown() : null;
594
+ const 창비율 = 예산 && 예산.total > 0 ? Math.min(1, 예산.used / 예산.total) : 0;
581
595
 
582
596
  /*
583
597
  * 자리에 앉는 사람들.
@@ -658,7 +672,7 @@ export function 상태만들기(session, 일감, 틱 = 0, 할일들 = [], 도는
658
672
  할일: 할일들.filter((t) => t.state !== 'done').length,
659
673
  부른횟수: 쓴것.calls ?? 0,
660
674
  토큰: 총토큰,
661
- 창: 창크기 ? Math.min(1, 총토큰 / 창크기) : 0,
675
+ 창: 창비율,
662
676
  틱,
663
677
  };
664
678
  }
package/src/ui/status.js CHANGED
@@ -142,7 +142,11 @@ export const SEGMENTS = {
142
142
  get desc() { return 말('seg.ctx'); },
143
143
  make: (s) => {
144
144
  const { r, pct, tone, b } = 참값(s);
145
- return `${눈금게이지(r, 10, [FOLD_AT, COMPACT_AT])} ${tone(pct + '%')} ${c.gray(short(b.used) + '/' + short(b.total))}`;
145
+ // 크기를 서버가 알려 줬으면 `32k?` 적는다 바로 옆 모델 급이
146
+ // 짐작일 때 `◈ 보통?` 인 것과 같은 규칙이다. 짐작을 잰 것처럼 적으면,
147
+ // 실제 창이 8k 라서 답이 잘리는 동안에도 게이지는 초록이다.
148
+ const 창말 = short(b.total) + (b.총잰것 === false ? '?' : '');
149
+ return `${눈금게이지(r, 10, [FOLD_AT, COMPACT_AT])} ${tone(pct + '%')} ${c.gray(short(b.used) + '/' + 창말)}`;
146
150
  },
147
151
  // 자리가 모자라면 정확한 숫자를 접는다. 게이지와 %가 이미 같은 말을 하고 있다.
148
152
  short: (s) => {
@@ -222,7 +226,17 @@ export const SEGMENTS = {
222
226
  make: (s) => {
223
227
  const 돈 = 돈셈(s.usage, 세션요금(s));
224
228
  if (!돈 || !(돈.달러 > 0)) return null;
225
- return c.gray(돈말(돈.달러));
229
+ /*
230
+ * 확정된 금액이 아닐 때는 물음표를 붙인다.
231
+ *
232
+ * 두 가지가 그렇게 만든다. 창구가 usage 를 안 준 부름(`못잰것`)은 그
233
+ * 몫이 **덜 세어졌고**, 캐시를 썼는데 캐시 요금을 모르는 경우
234
+ * (`캐시모름`)는 제값으로 세어 **더 많게** 나온다. `/cost` 는 이미
235
+ * 둘 다 적는다 — 그런데 늘 떠 있는 이 자리가 더 확정적으로 보이면
236
+ * 사람은 이쪽을 믿는다. 두 화면이 다른 말을 하면 안 된다.
237
+ */
238
+ const 흐린가 = (s.usage?.못잰것 ?? 0) > 0 || 돈.캐시모름 === true;
239
+ return c.gray(돈말(돈.달러) + (흐린가 ? '?' : ''));
226
240
  },
227
241
  },
228
242
 
@@ -457,7 +471,16 @@ export function statusLine(session, { segments = null, max = cols() - 2 } = {})
457
471
  // 여전히 모자라면 뒤에서부터 떨군다. 앞쪽(폴더·모델·컨텍스트)이 마지막까지 남는다.
458
472
  while (parts.length > 1 && !맞나(parts)) parts.pop();
459
473
 
460
- return ` ${c.gray('▏')}${parts.join(칸막이)}`;
474
+ /*
475
+ * 덩이가 **하나만 남아도** 폭을 넘을 수 있다.
476
+ *
477
+ * 위 고리는 `parts.length > 1` 에서 멈추므로, 첫 덩이 하나가 폭보다 넓으면
478
+ * 그대로 나간다 — 30칸으로 좁힌 창에서 32칸이 나왔다. 넘친 줄은 터미널이
479
+ * 두 줄로 접는데 입력 상자는 한 줄로 세고, 그만큼 커서를 덜 올려 위쪽
480
+ * 대화를 한 줄씩 갉아먹는다(ui/inputbox.js 의 프레임).
481
+ */
482
+ const 줄 = ` ${c.gray('▏')}${parts.join(칸막이)}`;
483
+ return width(줄) > max ? clip(줄, max) : 줄;
461
484
  }
462
485
 
463
486
  // 컨텍스트가 위험하면 한마디 붙인다. 상태줄만 보고 넘기지 않도록.