deel-local-cli 1.4.3 → 1.5.2

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.
@@ -0,0 +1,431 @@
1
+ /**
2
+ * 붙박이 사무실 — 입력 상자 위에 얹히는 방 한 칸.
3
+ *
4
+ * ── 무엇인가 ────────────────────────────────────────────────────────────
5
+ *
6
+ * 로컬 모델은 한 턴에 몇 분이 걸린다. 그 시간을 견디게 하려고 motion.js 가
7
+ * 작은 그림을 돌리는데, 이건 그 생각을 한 뼘 더 밀고 간 것이다 — 지금 무슨
8
+ * 일이 얼마나 돌아가고 있는지를 **방 하나로** 보여 준다.
9
+ *
10
+ * ── 규칙 하나 ───────────────────────────────────────────────────────────
11
+ *
12
+ * **화면에 있는 것은 전부 진짜 숫자다.** 꾸미려고 놓은 것은 하나도 없다.
13
+ * 자리가 셋 차 있으면 지금 도는 일이 셋이고, 서랍이 차 있으면 그만큼 파일을
14
+ * 읽은 것이고, 서버 불이 길면 모델을 그만큼 부른 것이다. 이걸 안 지키면
15
+ * 그냥 배경화면이 되고, 지키면 상태줄을 방으로 그린 것이 된다.
16
+ *
17
+ * ── 왜 반칸(▀)인가 ──────────────────────────────────────────────────────
18
+ *
19
+ * 한 칸에 픽셀 둘을 넣는다 — 위는 글자색, 아래는 배경색, 글자는 ▀. 세로
20
+ * 해상도가 두 배가 되고 칸이 정사각형에 가까워져 그림이 안 눌린다.
21
+ * motion.js 의 점자와 길이 갈리는 이유는 **색** 이다. 점자는 켜짐/꺼짐뿐이라
22
+ * 사무실이 갈색 죽이 된다.
23
+ *
24
+ * ── 가벼움 ──────────────────────────────────────────────────────────────
25
+ *
26
+ * 한 판을 통째로 보내면 9KB 가 넘는다. 그런데 판이 바뀔 때 실제로 달라지는
27
+ * 것은 **열두 줄 중 한 줄** 뿐이다(벽·창문·서류함·빈 책상은 그대로다).
28
+ * 그래서 상자가 바뀐 줄만 보낸다(inputbox 의 그리기). 그 덕에 사무실을 켜도
29
+ * 터미널로 나가는 양이 켜기 전과 비슷하다.
30
+ */
31
+ import { 느긋해질때 } from './working.js';
32
+
33
+ /*
34
+ * 방을 그릴 만큼 색이 되는 화면인가.
35
+ *
36
+ * ansi.js 의 색256가능() 은 여기서 못 쓴다. 그것이 보는 것은 TERM 이 dumb
37
+ * 인지 하나뿐인데, 상자 자체가 이미 dumb 를 거부한다(screen.js 상자쓸까).
38
+ * 즉 상자 안에서는 **늘 참**이라 있으나 마나 한 관문이었고, 16색짜리 콘솔에서도
39
+ * 사무실이 켜졌다.
40
+ *
41
+ * 그래서 여기서는 반대로 **확실할 때만** 참이라고 한다. 틀렸을 때 잃는 것이
42
+ * 양쪽으로 다르기 때문이다 — 못 켜면 그냥 사무실이 안 보이지만, 잘못 켜면
43
+ * 색 번호가 글자로 쏟아지거나 열두 줄이 갈색 죽이 된다. 모르면 안 켠다.
44
+ */
45
+ function 색충분한가() {
46
+ // 색을 끈 사람에게 방을 그리면 안 된다. 방은 색이 전부다.
47
+ if (process.env.NO_COLOR !== undefined) return false;
48
+ if (process.env.FORCE_COLOR === '1') return true;
49
+
50
+ const term = String(process.env.TERM ?? '');
51
+ if (term === 'dumb') return false;
52
+
53
+ const 컬러텀 = String(process.env.COLORTERM ?? '').toLowerCase();
54
+ if (컬러텀 === 'truecolor' || 컬러텀 === '24bit') return true;
55
+ if (/256/.test(term)) return true;
56
+
57
+ // 윈도우는 Node 20 이 Win10 이상을 요구하고, 거기 콘솔은 256색을 한다.
58
+ if (process.platform === 'win32') return true;
59
+ if (process.env.WT_SESSION) return true; // 윈도우 터미널
60
+ if (process.env.TERM_PROGRAM) return true; // iTerm · VS Code · Apple Terminal
61
+ if (process.env.ConEmuANSI === 'ON') return true;
62
+
63
+ return false;
64
+ }
65
+
66
+ const ESC = '\x1b';
67
+ const 투명 = -1;
68
+
69
+ export const 세로 = 24; // 픽셀. 칸으로는 12줄
70
+ export const 줄수 = 세로 / 2;
71
+ const 벽 = 6; // 벽은 눌러 뒀다 — 사람 자리를 벌기 위해
72
+
73
+ // ── 색 (xterm 256) ──────────────────────────────────────────────────────
74
+ const C = {
75
+ 바닥: 138, 바닥줄: 95, 벽: 238, 굽도리: 145,
76
+ 나무: 137, 나무어둠: 94, 나무진: 58,
77
+ 쇠: 145, 쇠어둠: 102, 쇠진: 59, 검정: 235,
78
+ 화면: 45, 화면꺼짐: 239, 화면글: 123, 살: 223,
79
+ 머리: [52, 240, 94, 130], 옷: [67, 131, 71, 97],
80
+ 밤: 17, 별: 226, 낮: 111, 구름: 255,
81
+ 종이: 255, 종이어둠: 250, 쪽지: 227, 쪽지끝: 120, 초록: 46, 넘침: 208,
82
+ };
83
+
84
+ // ── 픽셀 판 ─────────────────────────────────────────────────────────────
85
+ class 판 {
86
+ constructor(가로, 세로칸) {
87
+ this.가로 = 가로;
88
+ this.세로 = 세로칸;
89
+ this.점 = new Int16Array(가로 * 세로칸).fill(투명);
90
+ }
91
+
92
+ 찍기(x, y, 색) {
93
+ if (색 === 투명 || 색 == null) return;
94
+ if (x < 0 || y < 0 || x >= this.가로 || y >= this.세로) return;
95
+ this.점[y * this.가로 + x] = 색;
96
+ }
97
+
98
+ 칠하기(x, y, w, h, 색) {
99
+ for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) this.찍기(x + i, y + j, 색);
100
+ }
101
+
102
+ 올리기(x, y, 그림, 색표) {
103
+ for (let j = 0; j < 그림.length; j++) {
104
+ const 줄 = 그림[j];
105
+ for (let i = 0; i < 줄.length; i++) {
106
+ const 색 = 색표[줄[i]];
107
+ if (색 !== undefined) this.찍기(x + i, y + j, 색);
108
+ }
109
+ }
110
+ }
111
+
112
+ /** 화면에 내보낼 줄들. 색이 안 바뀌면 제어문자를 다시 안 쓴다. */
113
+ 줄들() {
114
+ const 결과 = [];
115
+ for (let y = 0; y < this.세로; y += 2) {
116
+ let s = '';
117
+ let 앞위 = null;
118
+ let 앞아래 = null;
119
+ for (let x = 0; x < this.가로; x++) {
120
+ const 위 = this.점[y * this.가로 + x];
121
+ const 아래 = this.점[(y + 1) * this.가로 + x];
122
+ if (위 === 투명 && 아래 === 투명) {
123
+ if (앞위 !== null || 앞아래 !== null) { s += `${ESC}[0m`; 앞위 = null; 앞아래 = null; }
124
+ s += ' ';
125
+ continue;
126
+ }
127
+ if (위 !== 앞위) { s += `${ESC}[38;5;${위 === 투명 ? 0 : 위}m`; 앞위 = 위; }
128
+ if (아래 !== 앞아래) { s += `${ESC}[48;5;${아래 === 투명 ? 0 : 아래}m`; 앞아래 = 아래; }
129
+ s += '▀';
130
+ }
131
+ 결과.push(s + `${ESC}[0m`);
132
+ }
133
+ return 결과;
134
+ }
135
+ }
136
+
137
+ /*
138
+ * ── 일하는 자리 18×14 ───────────────────────────────────────────────────
139
+ *
140
+ * 사람과 책상을 따로 그렸더니 반 픽셀씩 어긋나 팔이 책상을 뚫었다. 그래서
141
+ * 한 장으로 그린다 — 한 그림 안에 있으면 어긋날 자리가 없다.
142
+ *
143
+ * 짧은 판이라고 사람을 줄이면 자세가 안 읽힌다(조는 머리가 화면 뒤로 숨는다).
144
+ * 줄여야 할 것은 사람이 아니라 **여백**이다. 그래서 벽을 눌렀다.
145
+ */
146
+ const 화면줄 = (내용) => ` M${내용.padEnd(10, 'S').slice(0, 10)}M `;
147
+ const 빈 = ' '.repeat(18);
148
+ const 사람 = [
149
+ ' hhhhhh ',
150
+ ' hhhhhhhh ',
151
+ ' hffffffh ',
152
+ ' hffffffh ',
153
+ ' ffffff ',
154
+ ' bbbbbbbbbb ',
155
+ ];
156
+ const 책상 = [' MMMMMMMMMMMM ', 'DDDDDDDDDDDDDDDDDD', 'dddddddddddddddddd', 'd d'];
157
+ const 자리짜기 = (위, 화면들) => [...위, ' MMMMMMMMMMMM ', ...화면들.map(화면줄), ...책상];
158
+
159
+ const 자세들 = {
160
+ 치기A: 자리짜기(사람, ['GGGGGGGSSS', 'GGGGSSSSSS']),
161
+ 치기B: 자리짜기(사람, ['GGGSSSSSSS', 'GGGGGGGSSS']),
162
+ 읽기A: 자리짜기([빈, ...사람.slice(0, 5)], ['GGGGGGGGGG', 'SSSSSSSSSS']),
163
+ 읽기B: 자리짜기([빈, 빈, ...사람.slice(0, 4)], ['SSSSSSSSSS', 'GGGGGGGGGG']),
164
+ 졸기: 자리짜기([빈, 빈, 빈, ' hhhhhh ', ' hhffffffhh ', ' ffffff '],
165
+ ['SSSSSSSSSS', 'SSSSSSSSSS']),
166
+ 대기: 자리짜기(사람, ['SSSSSSSSSS', 'SSSSSSSSSS']),
167
+ 빈자리: 자리짜기([빈, 빈, 빈, 빈, 빈, 빈], ['SSSSSSSSSS', 'SSSSSSSSSS']),
168
+ };
169
+ const 자리폭 = 18;
170
+ const 자리높이 = 14;
171
+
172
+ function 자리색(자세, 머리, 옷) {
173
+ const 꺼짐 = 자세 === '빈자리';
174
+ return {
175
+ h: 머리, f: C.살, b: 옷,
176
+ M: C.검정, S: 꺼짐 ? C.화면꺼짐 : C.화면, G: C.화면글,
177
+ D: 꺼짐 ? C.나무어둠 : C.나무, d: 꺼짐 ? C.나무진 : C.나무어둠,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * 방 한 판을 줄들로.
183
+ *
184
+ * @param {object} 상태 아래 상태만들기() 가 만든 것
185
+ * @param {number} 폭 터미널 칸 수
186
+ */
187
+ export function 사무실줄들(상태, 폭) {
188
+ const {
189
+ 일꾼들 = [], 읽은파일 = 0, 고친파일 = 0, 할일 = 0, 끝난일 = 0,
190
+ 부른횟수 = 0, 창 = 0, 틱 = 0,
191
+ } = 상태;
192
+
193
+ const 가로 = Math.max(60, 폭);
194
+ const p = new 판(가로, 세로);
195
+ p.칠하기(0, 0, 가로, 벽, C.벽);
196
+ p.칠하기(0, 벽, 가로, 1, C.굽도리);
197
+ p.칠하기(0, 벽 + 1, 가로, 세로 - 벽 - 1, C.바닥);
198
+
199
+ const 밤인가 = 창 > 0.55;
200
+
201
+ // 화이트보드 18×5 — 할 일. 끝난 것은 초록, 남은 것은 노랑.
202
+ {
203
+ const 그림 = ['FFFFFFFFFFFFFFFFFF'];
204
+ for (let r = 0; r < 3; r++) 그림.push(`F${'W'.repeat(16)}F`);
205
+ 그림.push('FFFFFFFFFFFFFFFFFF');
206
+ /*
207
+ * 쪽지는 다섯 장뿐이라 열 개짜리 목록은 눌러 담아야 한다. 그런데 끝난
208
+ * 것부터 채우면 「열 개 다 끝남」과 「열 개 끝나고 하나 남음」이 똑같이
209
+ * 초록 다섯 장으로 나온다 — 남은 일이 화면에서 사라진다.
210
+ *
211
+ * 그래서 남은 일이 하나라도 있으면 **노란 쪽지 한 장은 반드시 남긴다.**
212
+ * 눌러 담더라도 '아직 안 끝났다' 는 사실만은 안 지운다.
213
+ */
214
+ const 총 = Math.min(5, 할일 + 끝난일);
215
+ const 초록수 = 할일 > 0 ? Math.min(끝난일, 총 - 1) : Math.min(끝난일, 총);
216
+ for (let n = 0; n < 총; n++) {
217
+ const x = 2 + n * 3;
218
+ for (const r of [1, 2, 3]) {
219
+ const 줄 = 그림[r].split('');
220
+ 줄[x] = n < 초록수 ? 'D' : 'N';
221
+ 줄[x + 1] = n < 초록수 ? 'D' : 'N';
222
+ 그림[r] = 줄.join('');
223
+ }
224
+ }
225
+ p.올리기(1, 0, 그림, { F: C.쇠어둠, W: C.종이, N: C.쪽지, D: C.쪽지끝 });
226
+ }
227
+
228
+ // 서버 불빛 14×5 — 모델 호출. 오른쪽 벽 끝.
229
+ {
230
+ /*
231
+ * 불은 열두 개뿐이다. 여섯 번 부른 것과 백 번 부른 것이 똑같이 다 켜진
232
+ * 채로 나오면, 이 불은 어느 순간부터 아무것도 안 말하게 된다.
233
+ *
234
+ * 다 차면 맨 끝 불을 다른 색으로 바꾼다 — '여기서부터는 더 있다' 는 뜻이다.
235
+ * 없는 눈금을 지어내지 않으면서 넘쳤다는 것만 알린다.
236
+ */
237
+ const 켠수 = Math.min(12, 부른횟수 * 2);
238
+ const 넘쳤나 = 부른횟수 * 2 > 12;
239
+ const 그림 = ['RRRRRRRRRRRRRR'];
240
+ for (let r = 0; r < 3; r++) {
241
+ let 줄 = 'R';
242
+ for (let k = 0; k < 12; k++) 줄 += (넘쳤나 && k === 11) ? 'O' : (k < 켠수 ? 'L' : 'l');
243
+ 그림.push(줄 + 'R');
244
+ }
245
+ 그림.push('RRRRRRRRRRRRRR');
246
+ p.올리기(가로 - 15, 0, 그림, { R: C.검정, L: C.초록, l: C.쇠진, O: C.넘침 });
247
+ }
248
+
249
+ // 창문 — 벽에 남는 자리만큼. 컨텍스트가 차면 밤이 된다.
250
+ {
251
+ const 시작 = 21;
252
+ const 남 = 가로 - 15 - 시작;
253
+ const 창수 = Math.max(0, Math.floor(남 / 18));
254
+ for (let i = 0; i < 창수; i++) {
255
+ const 그림 = ['FFFFFFFFFFFFFFFF'];
256
+ for (let r = 0; r < 3; r++) {
257
+ let 줄 = 'F';
258
+ for (let k = 0; k < 14; k++) {
259
+ const h = (i * 37 + r * 19 + k * 11) % 29;
260
+ 줄 += k === 7 ? 'F' : (밤인가 ? (h < 5 ? '*' : '.') : (h < 3 ? 'o' : ','));
261
+ }
262
+ 그림.push(줄 + 'F');
263
+ }
264
+ 그림.push('FFFFFFFFFFFFFFFF');
265
+ p.올리기(시작 + i * 18, 0, 그림, 밤인가
266
+ ? { F: C.쇠어둠, '.': C.밤, '*': C.별 }
267
+ : { F: C.쇠어둠, ',': C.낮, o: C.구름 });
268
+ }
269
+ }
270
+
271
+ // 서류함 12×12 — 읽은 파일. 서랍 하나가 일곱쯤.
272
+ {
273
+ const 찬칸 = Math.min(3, Math.ceil(읽은파일 / 7));
274
+ const 그림 = ['CCCCCCCCCCCC'];
275
+ for (let s = 0; s < 3; s++) {
276
+ const 안 = s < 찬칸 ? 'P' : 'c';
277
+ 그림.push(`C${안.repeat(4)}HH${안.repeat(4)}C`);
278
+ 그림.push(`C${안.repeat(10)}C`);
279
+ 그림.push('CCCCCCCCCCCC');
280
+ }
281
+ p.올리기(1, 벽 + 5, 그림.slice(0, 12), { C: C.쇠어둠, c: C.쇠, P: C.종이어둠, H: C.쇠진 });
282
+ }
283
+
284
+ /*
285
+ * 일하는 자리. **늘 같은 수를 그린다** — 일이 늘 때 자리가 차오르는 것으로
286
+ * 보여야 하는데, 없는 자리를 새로 그리면 방이 늘었다 줄었다 해서 어지럽다.
287
+ */
288
+ const 왼쪽 = 15;
289
+ const 자리수 = Math.max(1, Math.floor((가로 - 왼쪽 - 2) / (자리폭 + 1)));
290
+ /*
291
+ * 자리보다 사람이 많으면 그냥 잘려 나갔다 — 좁은 화면에서 둘이 도는 것과
292
+ * 셋이 도는 것이 **똑같은 그림**이 됐다. 이 화면의 규칙이 '전부 진짜 숫자'
293
+ * 인데 그러면 자리 수만 거짓이 된다.
294
+ *
295
+ * 못 그리는 것을 억지로 그릴 수는 없으니, 대신 마지막 자리의 책상 불을
296
+ * 넘침 색으로 바꿔 '여기 뒤로 더 있다' 를 남긴다.
297
+ */
298
+ const 넘친자리 = 일꾼들.length > 자리수;
299
+ const 짝 = Math.floor(틱 / 5) % 2 === 0;
300
+ for (let i = 0; i < 자리수; i++) {
301
+ const x = 왼쪽 + i * (자리폭 + 1);
302
+ const y = 세로 - 자리높이;
303
+ const 그사람 = 일꾼들[i];
304
+ const 자세 = !그사람 ? '빈자리'
305
+ : 그사람.갈래 === '쓰기' ? (짝 ? '치기A' : '치기B')
306
+ : 그사람.갈래 === '읽기' ? (짝 ? '읽기A' : '읽기B')
307
+ : 그사람.갈래 === '졸기' ? '졸기' : '대기';
308
+ p.올리기(x, y, 자세들[자세], 자리색(자세, C.머리[i % 4], C.옷[i % 4]));
309
+ // 마지막 자리인데 뒤에 더 있으면, 책상 모서리에 넘침 표시를 남긴다.
310
+ if (넘친자리 && i === 자리수 - 1) p.칠하기(x + 자리폭 - 2, y + 자리높이 - 3, 2, 2, C.넘침);
311
+ // 조는 사람 머리 위 z
312
+ if (자세 === '졸기' && 짝) p.칠하기(x + 13, y + 2, 2, 1, C.종이);
313
+ // 고친 파일은 책상 위 종이로
314
+ if (i < 고친파일) p.칠하기(x + 13, y + 9, 4, 1, C.종이);
315
+ }
316
+
317
+ return p.줄들();
318
+ }
319
+
320
+ /*
321
+ * ── 켜나 마나 ───────────────────────────────────────────────────────────
322
+ *
323
+ * 기본은 꺼져 있다. 화면 절반을 먹는 것을 묻지도 않고 켜면 안 된다.
324
+ *
325
+ * 켜 달라고 해도 못 켜는 자리가 있다.
326
+ * · 256색이 안 되는 터미널 — 색 번호가 글에 그대로 새어 나온다
327
+ * · 그림을 끈 사람 (DEEL_NO_MOTION) — 화면 읽기 프로그램을 쓰는 자리다
328
+ * · 화면이 낮은 터미널 — 사무실 12줄 + 상자 5줄이면 대화가 들어갈 자리가
329
+ * 없다. 사무실을 보려고 도구를 못 쓰게 되는 것은 앞뒤가 바뀐 것이다.
330
+ * · 화면이 **좁은** 터미널 — 아래 최소폭 참고. 이건 안 지키면 화면이 깨진다.
331
+ */
332
+ export const 최소높이 = 28;
333
+
334
+ /*
335
+ * 방은 늘 60칸을 쓴다(가로 = Math.max(60, 폭)). 더 좁게 그리면 책상과 사람이
336
+ * 뭉개져서 방으로 안 보인다 — 줄여야 할 것은 사람이 아니라 여백인데, 60칸이
337
+ * 여백을 다 뺀 자리다.
338
+ *
339
+ * 그래서 60칸보다 좁은 터미널에서는 **안 켠다**. 켜면 열두 줄이 스물네 줄로
340
+ * 접히는데 상자는 열두 줄로 세고 그만큼만 커서를 올린다. 어긋난 자리에 다음
341
+ * 판을 덮어써서 대화가 통째로 지워진다 — 50칸에서 200판을 돌리면 테두리도
342
+ * 대화도 남지 않는다. 좁으면 그냥 안 보여 주는 편이 낫다.
343
+ */
344
+ export const 최소폭 = 60;
345
+
346
+ /**
347
+ * 사람이 켜 달라고 했나 (화면이 되는지는 안 본다).
348
+ *
349
+ * 켜나() 와 갈라 두는 이유: 상자 안 그림(기사·동물)을 끌지 말지는 **뜻**으로
350
+ * 정해야지 화면 높이로 정하면 안 된다. 창을 줄였다 늘였다 할 때마다 상자 안
351
+ * 그림이 기사였다 돌림표였다 하면 무슨 규칙인지 아무도 모른다.
352
+ */
353
+ export function 켜달라했나() {
354
+ const v = String(process.env.DEEL_OFFICE ?? '').trim().toLowerCase();
355
+ return v === '1' || v === 'on' || v === '켬' || v === '사무실';
356
+ }
357
+
358
+ export function 켜나(rows = process.stdout.rows, cols = process.stdout.columns) {
359
+ if (!켜달라했나()) return false;
360
+ if (process.env.DEEL_NO_MOTION) return false;
361
+ if (!색충분한가()) return false;
362
+ if ((cols ?? 0) < 최소폭) return false;
363
+ return (rows ?? 0) >= 최소높이;
364
+ }
365
+
366
+ /**
367
+ * 세션에서 방 상태를 뽑는다.
368
+ *
369
+ * 여기가 '전부 진짜 숫자' 를 지키는 자리다. 없는 값은 0 으로 두지, 그럴듯한
370
+ * 것으로 채우지 않는다.
371
+ *
372
+ * @param {object} session
373
+ * @param {object} 일감 지금 도는 일 (없으면 null)
374
+ * @param {number} 틱
375
+ */
376
+ export function 상태만들기(session, 일감, 틱 = 0, 할일들 = [], 도는하위 = 0) {
377
+ const s = session ?? {};
378
+ const 쓴것 = s.usage ?? {};
379
+ const 총토큰 = (쓴것.in ?? 0) + (쓴것.out ?? 0);
380
+ const 창크기 = s.conn?.ctx ?? 0;
381
+
382
+ /*
383
+ * 자리에 앉는 사람들.
384
+ *
385
+ * 지금은 도는 일이 하나(또는 하위 작업만큼)다. 갈래는 일감이 말해 주는
386
+ * 것을 그대로 쓴다 — motion.js 가 쓰는 그 갈래다.
387
+ */
388
+ const 일꾼들 = [];
389
+ if (일감) {
390
+ /*
391
+ * 오래 끌면 존다.
392
+ *
393
+ * 여기서 직접 재야 한다. 부르는 쪽(inputbox)이 넘겨 주는 일감.갈래 는
394
+ * '생각'·'답'·'하위'·'접기'·도구이름 중 하나일 뿐, **'느긋' 이 되는
395
+ * 길이 없다.** 상자 안 돌림표는 자기 자리에서 따로 재고 있었으므로,
396
+ * 문서에 적어 둔 '45초 넘으면 존다' 가 방에서는 한 번도 일어나지
397
+ * 않았다 — 졸기 자세 코드가 통째로 죽어 있었다.
398
+ */
399
+ const 지난 = Number.isFinite(일감.시작) ? Date.now() - 일감.시작 : 0;
400
+ const 오래됐나 = 일감.갈래 === '느긋' || 지난 >= 느긋해질때;
401
+ const 갈래 = 오래됐나 ? '졸기'
402
+ : ['읽기', '웹'].includes(일감.갈래) ? '읽기'
403
+ : ['쓰기', '명령', '답'].includes(일감.갈래) ? '쓰기' : '대기';
404
+ /*
405
+ * 하위 작업이 돌면 그만큼 자리가 더 찬다. 부모 몫 하나는 늘 있다.
406
+ *
407
+ * 자세는 **부모 것만** 진짜다. 하위가 지금 읽는 중인지 쓰는 중인지는
408
+ * 여기까지 안 올라온다 — 세는 것은 개수뿐이다. 그런데 예전에는 그 자리에도
409
+ * 부모의 자세를 그대로 복사해 놨다. 부모가 자판을 두드리면 읽고 있던
410
+ * 하위까지 자판을 두드렸다. 자리 수는 진짜인데 자세는 지어낸 것이라,
411
+ * 이 화면의 규칙(전부 진짜 숫자)에 어긋나는 유일한 자리였다.
412
+ *
413
+ * 그래서 하위 자리는 '대기' 로 앉힌다. 모르는 것을 그럴듯하게 채우느니
414
+ * 모른다고 두는 편이 낫다 — 자리가 찼다는 사실은 그대로 보인다.
415
+ */
416
+ 일꾼들.push({ 갈래 });
417
+ for (let i = 0; i < Math.max(0, 도는하위); i++) 일꾼들.push({ 갈래: '대기' });
418
+ }
419
+
420
+ return {
421
+ 일꾼들,
422
+ 읽은파일: s.filesRead?.size ?? 0,
423
+ 고친파일: s.changes?.size ?? 0,
424
+ 끝난일: 할일들.filter((t) => t.state === 'done').length,
425
+ 할일: 할일들.filter((t) => t.state !== 'done').length,
426
+ 부른횟수: 쓴것.calls ?? 0,
427
+ 토큰: 총토큰,
428
+ 창: 창크기 ? Math.min(1, 총토큰 / 창크기) : 0,
429
+ 틱,
430
+ };
431
+ }