deel-local-cli 1.6.0 → 1.6.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.
- package/README.en.md +8 -2
- package/README.md +9 -2
- package/package.json +2 -2
- package/src/acp/serve.js +6 -0
- package/src/agent/askcheck.js +281 -0
- package/src/agent/asks.js +186 -0
- package/src/agent/compact.js +28 -1
- package/src/agent/loop.js +127 -2
- package/src/agent/route.js +54 -3
- package/src/agent/session.js +2 -0
- package/src/repl.js +197 -10
- package/src/safety/guard.js +61 -0
- package/src/tools/convert.js +315 -0
- package/src/tools/desc.en.js +7 -3
- package/src/tools/docs.js +72 -1
- package/src/tools/fastgrep.js +8 -11
- package/src/tools/fsutil.js +13 -1
- package/src/tools/index.js +270 -17
- package/src/tools/outline.js +2 -1
- package/src/tools/pdf.js +44 -2
- package/src/tools/spawn.js +99 -0
- package/src/ui/inputbox.js +11 -2
- package/src/ui/motion.js +30 -0
- package/src/ui/pastechip.js +87 -0
- package/src/ui/working.js +14 -0
package/src/agent/loop.js
CHANGED
|
@@ -17,6 +17,7 @@ import { allowTemporarily, isOffline } from '../safety/network.js';
|
|
|
17
17
|
import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴도구 } from '../safety/secrets.js';
|
|
18
18
|
import { get as workMode } from './modes.js';
|
|
19
19
|
import { 지시말 } from '../i18n/index.js';
|
|
20
|
+
import { 빠진것, 빠졌다는말 } from './asks.js';
|
|
20
21
|
|
|
21
22
|
/*
|
|
22
23
|
* 콜백으로만 소식을 주는 부름을, 제너레이터가 중간에 내보낼 수 있는 모양으로 바꾼다.
|
|
@@ -60,6 +61,36 @@ function 빈답인가(msg) {
|
|
|
60
61
|
return !String(msg.content ?? '').trim();
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* 도구 결과 중에서 **대화에 실을 글**을 고른다.
|
|
66
|
+
*
|
|
67
|
+
* ── 왜 따로 두나 ────────────────────────────────────────────────────────
|
|
68
|
+
*
|
|
69
|
+
* 여태 여기는 `result.content ?? ''` 한 줄이었다. 그런데 도구가 `summary` 만
|
|
70
|
+
* 돌려주는 자리가 있다 — Recall 이 지난 대화를 다 뒤졌지만 못 찾았을 때가
|
|
71
|
+
* 그렇다. 그 경우 모델에게 가는 것은 **빈 글**이다.
|
|
72
|
+
*
|
|
73
|
+
* 사람 화면에는 「지난 대화 12개를 다 뒤졌지만 없습니다」 가 멀쩡히 찍힌다.
|
|
74
|
+
* 그래서 아무도 눈치를 못 챈다. 정작 모델은 아무 말도 못 들었으므로,
|
|
75
|
+
* 못 찾았다는 것도 모르고 **찾아본 적이 있다는 것조차** 모른다. 그 자리에서
|
|
76
|
+
* 모델이 하는 일은 둘 중 하나다 — 없는 것을 지어내거나, 사람에게 엉뚱한
|
|
77
|
+
* 선택지를 들이민다. 실제 제보가 딱 그 모양이었다.
|
|
78
|
+
*
|
|
79
|
+
* 그래서 규칙을 셋으로 못 박는다.
|
|
80
|
+
* 1) 글이 있으면 그대로.
|
|
81
|
+
* 2) 글이 없고 요약이 있으면 요약을 싣는다 — 사람이 본 것과 같은 말이다.
|
|
82
|
+
* 3) 둘 다 없으면 **없다고 말한다.** 침묵은 지어내기를 부른다.
|
|
83
|
+
*/
|
|
84
|
+
export function 실을글(result) {
|
|
85
|
+
if (result?.error) return `오류: ${result.error}`;
|
|
86
|
+
const 글 = result?.content;
|
|
87
|
+
if (String(글 ?? '').trim()) return String(글);
|
|
88
|
+
const 요약 = String(result?.summary ?? '').trim();
|
|
89
|
+
if (요약) return 요약;
|
|
90
|
+
return '도구는 돌았지만 돌려준 것이 없습니다.'
|
|
91
|
+
+ ' 없는 것을 있다고 지어내지 말고, 못 얻었다는 것을 그대로 두고 다음으로 넘어가세요.';
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
// think 값을 규격에 맞게. 'off' 는 사고를 끈다.
|
|
64
95
|
function thinkFor(conn, level) {
|
|
65
96
|
if (level === 'off') return conn.kind === 'ollama' ? false : undefined;
|
|
@@ -122,6 +153,22 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
122
153
|
*/
|
|
123
154
|
ctx.signal = signal;
|
|
124
155
|
|
|
156
|
+
/*
|
|
157
|
+
* 이번에 사람이 실제로 친 말. 도구가 볼 수 있게 여기 걸어 둔다.
|
|
158
|
+
*
|
|
159
|
+
* Ask 관문(agent/askcheck.js)이 이걸 본다 — "사용자가 이미 적어 준 것을
|
|
160
|
+
* 되묻고 있나" 는 원문 없이는 판단할 수 없다. 프롬프트에는 들어 있지만
|
|
161
|
+
* 그건 모델이 보는 것이고, 우리 코드가 볼 수 있는 자리가 없었다.
|
|
162
|
+
*
|
|
163
|
+
* 하위 작업에서는 그 하위가 받은 할일이 들어온다. 그게 맞다 — 하위에게
|
|
164
|
+
* '사용자의 말' 은 자기가 받은 할일이다.
|
|
165
|
+
*/
|
|
166
|
+
ctx.요청 = String(userText ?? '');
|
|
167
|
+
// 이번 턴에 물어본 것. 관문이 "같은 것을 또 묻나" 를 보는 자리다 (agent/askcheck.js).
|
|
168
|
+
ctx.물은것 = [];
|
|
169
|
+
// 접힐 때 요약에 뭉개지지 않게 원문을 세션에도 박아 둔다 (compact.js 의 못박은요청).
|
|
170
|
+
session.이번요청 = ctx.요청;
|
|
171
|
+
|
|
125
172
|
const conn = session.conn;
|
|
126
173
|
|
|
127
174
|
/**
|
|
@@ -320,11 +367,30 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
320
367
|
* 여기까지 오지 않는다 — 막히는 것은 글로 되묻고 턴을 닫는 쪽뿐이다.
|
|
321
368
|
*/
|
|
322
369
|
let 민적 = false;
|
|
370
|
+
/*
|
|
371
|
+
* 인사에는 안 민다.
|
|
372
|
+
*
|
|
373
|
+
* 「안녕」 한 마디에 모델이 "안녕하세요! 무엇을 도와드릴까요?" 라고 답하면
|
|
374
|
+
* 그건 **맞는 답**이다. 그런데 그 말이 되묻는말에 걸려서 되밀렸다 —
|
|
375
|
+
*
|
|
376
|
+
* ❯ 안녕
|
|
377
|
+
* ▌ 안녕하세요! 무엇을 도와드릴까요?
|
|
378
|
+
* ↺ 읽기만 하고 끝내려고 해서 한 번 되밀었습니다
|
|
379
|
+
* ▌ 안녕하세요! 반갑습니다.
|
|
380
|
+
* ── 6.8초
|
|
381
|
+
*
|
|
382
|
+
* 인사 한 마디에 모델을 두 번 부른다. 로컬 모델에서 그 왕복이 6.8초다.
|
|
383
|
+
* 되밀기는 「시킨 일을 안 하고 되돌려준」 자리를 잡으라고 만든 것인데,
|
|
384
|
+
* 인사는 시킨 일이 없다. 시킨 것이 없으면 안 한 것도 없다.
|
|
385
|
+
*/
|
|
386
|
+
const 인사인가 = /^(안녕[가-힣]*|반(가|갑)[가-힣]*|하이|ㅎㅇ+|헬로[우가-힣]*|고마[가-힣]*|감사[가-힣]*|수고[가-힣]*|hi|hello|hey|yo|thanks?|thank you|테스트|test|ok(ay)?|네|응)[\s!.~?ㅎㅋ,]*$/i
|
|
387
|
+
.test(String(userText ?? '').trim());
|
|
323
388
|
const 되묻는말 = /무엇을\s*도와|무엇을\s*해\s*드릴|원하시는\s*(작업|것)|어떤\s*(작업|것)\s*(을|부터)|말씀해\s*주세요|알려\s*주세요|지시해\s*주세요|어떻게\s*할까요|해\s*드릴까요|진행할까요|what would you like|how can i (help|assist)|let me know (what|which|how)|shall i\b|would you like me to/i;
|
|
324
389
|
|
|
325
390
|
/** 이번 답을 되밀까. 밀 이유를 돌려주고, 아니면 null. */
|
|
326
391
|
const 밀어줄까 = (글) => {
|
|
327
392
|
if (민적 || 깊이) return null;
|
|
393
|
+
if (인사인가) return null; // 시킨 것이 없으면 안 한 것도 없다
|
|
328
394
|
if (손댄파일.size) return null; // 뭐라도 바꿨으면 일은 한 것이다
|
|
329
395
|
if (!모드.tools.includes('Write')) return null; // 안 바꾸는 모드는 그게 맞다
|
|
330
396
|
const 말 = String(글 ?? '').trim();
|
|
@@ -335,6 +401,34 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
335
401
|
return null;
|
|
336
402
|
};
|
|
337
403
|
|
|
404
|
+
/*
|
|
405
|
+
* ── 시킨 것을 다 했나 (agent/asks.js) ────────────────────────────────
|
|
406
|
+
*
|
|
407
|
+
* 위의 되밀기는 **아무것도 안 바꿨을 때**만 본다(손댄파일.size 로 빠져나간다).
|
|
408
|
+
* 그런데 제보받은 것은 그게 아니었다 — 네 가지를 시키면 셋을 고치고 끝냈다.
|
|
409
|
+
* 뭐라도 고쳤으니 되밀기는 안 걸리고, 안 한 하나는 말없이 사라진다.
|
|
410
|
+
*
|
|
411
|
+
* 그래서 여기서 따로 대조한다. 시킨 것을 항목으로 쪼개 두고, 항목마다
|
|
412
|
+
* 자국이 남았는지 본다 — 할 일 목록에 있나, 손댄 파일 이름에 있나,
|
|
413
|
+
* 마지막 보고에 있나. 하나도 없으면 잊은 것이다.
|
|
414
|
+
*
|
|
415
|
+
* 한 번만 민다. 밀고도 그대로면 사람에게 말한다 — 두 번 세 번 미는 것은
|
|
416
|
+
* 모델이 못 하는 일을 억지로 시키는 꼴이고, 그 왕복은 사람이 낸다.
|
|
417
|
+
*/
|
|
418
|
+
let 빠뜨림민적 = false;
|
|
419
|
+
const 빠뜨린것 = (글) => {
|
|
420
|
+
// 하위 작업은 사람이 시킨 말이 아니다. 거기 할일은 부모가 이미 쪼개 준 것이다.
|
|
421
|
+
if (깊이) return [];
|
|
422
|
+
return 빠진것({
|
|
423
|
+
요청: userText,
|
|
424
|
+
자국: [
|
|
425
|
+
...(ctx.todos ?? []).map((x) => x.text),
|
|
426
|
+
...손댄파일,
|
|
427
|
+
String(글 ?? ''),
|
|
428
|
+
],
|
|
429
|
+
});
|
|
430
|
+
};
|
|
431
|
+
|
|
338
432
|
const 되미는말 = () => {
|
|
339
433
|
const 시킨 = String(userText ?? '').replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
340
434
|
return 지시말() === 'en'
|
|
@@ -479,15 +573,38 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
479
573
|
*/
|
|
480
574
|
const full = Math.max(cap, fullCap(room));
|
|
481
575
|
const 낮춘생각 = level === 'off' || level === 'low' ? level : shiftLevel(level, -1);
|
|
576
|
+
let 마지막상한 = cap;
|
|
482
577
|
if (wasCut(msg) && (full > cap || 낮춘생각 !== level)) {
|
|
483
578
|
yield {
|
|
484
579
|
type: 'retry',
|
|
485
580
|
why: full > cap ? '대답이 상한에서 잘렸습니다' : '대답이 잘렸습니다 — 생각을 줄여 자리를 냅니다',
|
|
486
581
|
from: cap, to: full, think: 낮춘생각,
|
|
487
582
|
};
|
|
583
|
+
마지막상한 = full;
|
|
488
584
|
yield* askModel(full, 낮춘생각);
|
|
489
585
|
}
|
|
490
586
|
|
|
587
|
+
/*
|
|
588
|
+
* 우리가 할 수 있는 것을 다 했는데도 잘렸다 — 여기서 입을 다물면 안 된다.
|
|
589
|
+
*
|
|
590
|
+
* 상한도 올려 봤고 생각도 줄여 봤는데 여전히 잘렸으면, 남은 일은 사람
|
|
591
|
+
* 몫이다. 그런데 여태 그냥 조용히 끝냈다. 화면에는 중간에서 끊긴 답만
|
|
592
|
+
* 남는다 — 사람 눈에는 모델이 게을러서 대충 답한 것으로 보이니 같은 것을
|
|
593
|
+
* 다시 시키고, 같은 자리에서 또 잘린다. 몇 분씩 가는 로컬 모델에서
|
|
594
|
+
* 이 왕복은 비싸다.
|
|
595
|
+
*
|
|
596
|
+
* 안 되는 것은 **까닭과 손댈 자리를 같이** 말한다 (`/out`).
|
|
597
|
+
* 다시 부른 뒤에 보는 것이 중요하다. 부르기 전에 판정하면, 상한을 올려
|
|
598
|
+
* 멀쩡히 끝난 답에까지 경고가 붙는다 — 그러면 곧 아무도 안 읽는다.
|
|
599
|
+
*/
|
|
600
|
+
if (wasCut(msg)) {
|
|
601
|
+
yield {
|
|
602
|
+
type: 'capped',
|
|
603
|
+
cap: 마지막상한,
|
|
604
|
+
정한값: conn.maxTokens ?? conn.maxOut ?? null,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
491
608
|
/*
|
|
492
609
|
* 빈 답을 성공으로 넘기지 않는다.
|
|
493
610
|
*
|
|
@@ -602,7 +719,15 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
602
719
|
yield { type: 'nudge', why: 왜, text: msg.content };
|
|
603
720
|
continue;
|
|
604
721
|
}
|
|
605
|
-
|
|
722
|
+
const 빠진 = 빠뜨린것(msg.content);
|
|
723
|
+
if (빠진.length && !빠뜨림민적) {
|
|
724
|
+
빠뜨림민적 = true;
|
|
725
|
+
session.push({ role: 'user', content: 빠졌다는말(빠진, { 영어: 지시말() === 'en' }) });
|
|
726
|
+
yield { type: 'nudge', why: '요청누락', 빠진, text: msg.content };
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
// 밀고도 그대로면 조용히 넘어가지 않는다. 사람이 알아야 다음을 정한다.
|
|
730
|
+
yield { type: 'done', steps, text: msg.content, files: 마무리(), 빠진 };
|
|
606
731
|
return;
|
|
607
732
|
}
|
|
608
733
|
|
|
@@ -1016,7 +1141,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
|
|
|
1016
1141
|
for (const f of result.여럿 ?? []) if (f.ok && f.path) 손댄파일.add(f.path);
|
|
1017
1142
|
|
|
1018
1143
|
// 앞에서 똑같이 부른 적이 있고 결과도 같으면, 결과를 다시 싣지 않는다.
|
|
1019
|
-
let 실을것 = result
|
|
1144
|
+
let 실을것 = 실을글(result);
|
|
1020
1145
|
if (!result.error) {
|
|
1021
1146
|
const 서명 = 서명만들기(call);
|
|
1022
1147
|
// 결과도 한 모양으로 맞춰 견준다. 여러 도구가 받은 경로를 답에 그대로
|
package/src/agent/route.js
CHANGED
|
@@ -154,6 +154,38 @@ export function 겹친요청(text) {
|
|
|
154
154
|
return { 겹침: true, why: `'${앞[0].trim()}' 뒤에 '${뒤[0].trim()}'` };
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/*
|
|
158
|
+
* ── 손대라는 말 ─────────────────────────────────────────────────────────
|
|
159
|
+
*
|
|
160
|
+
* "폴더 구조 개선해줘" 가 설계 모드로 갔다. 설계는 **읽기만 하는 모드**라,
|
|
161
|
+
* 사람은 고쳐 달라고 했는데 설계안 한 장을 받고 끝났다. 이 파일 맨 위에
|
|
162
|
+
* 적어 둔 것이 정확히 그 경우다 — "읽기 전용 모드로 잘못 보내면 사용자는
|
|
163
|
+
* '왜 파일을 안 고쳐?' 하고 막힌다."
|
|
164
|
+
*
|
|
165
|
+
* 낱말 점수로는 못 막는다. '구조' 와 '개선' 이 둘 다 설계의 강한 신호라서,
|
|
166
|
+
* 뒤에 붙은 '해줘' 를 아무리 봐도 점수로는 안 뒤집힌다.
|
|
167
|
+
*
|
|
168
|
+
* 그래서 **동사를 따로 본다.** 손대라는 말이 있으면 읽기만 하는 모드
|
|
169
|
+
* (설계·계획·묻기)는 후보에서 뺀다. 남은 것 중에 고르고, 없으면 종합에
|
|
170
|
+
* 그대로 둔다 — 종합은 파일을 고칠 수 있다.
|
|
171
|
+
*
|
|
172
|
+
* 가르는 자리가 미묘하다. '설명해줘' 와 '개선해줘' 는 둘 다 '…해줘' 지만
|
|
173
|
+
* 하나는 읽고 말하라는 말이고 하나는 고치라는 말이다. 그래서 어미가 아니라
|
|
174
|
+
* **동사 목록**으로 본다. 목록에 없으면 손대라는 말이 아니다.
|
|
175
|
+
*/
|
|
176
|
+
const 손대는동사 = '만들|고쳐|고치|바꿔|바꾸|추가|넣어|지워|삭제|옮겨|나눠|나누|합쳐|정리|개선|구현|작성|적용|설치|배포|리팩터링|리팩터|분리|통일|세팅|갈아|손봐|손보';
|
|
177
|
+
// 동사 바로 뒤에 시킴꼴이 붙어야 한다. 빈칸을 안 넘으므로 "나누는 설계만 봐줘"
|
|
178
|
+
// 는 안 걸린다 — 그건 나누라는 말이 아니라 나누는 안을 보자는 말이다.
|
|
179
|
+
const 손대라는말 = new RegExp(`(${손대는동사})\\S{0,3}(줘|주세요|주라|해라|하라|하자|해다오|해줄래|할래)`);
|
|
180
|
+
|
|
181
|
+
// 파일을 못 고치는 모드. modes.js 에서 쓰기 도구를 안 주는 것들과 같아야 한다.
|
|
182
|
+
const 읽기만하는모드 = new Set(['architect', 'plan', 'ask']);
|
|
183
|
+
|
|
184
|
+
/** 파일을 손대라는 말이 들었나. */
|
|
185
|
+
export function 손대라했나(text) {
|
|
186
|
+
return 손대라는말.test(String(text ?? ''));
|
|
187
|
+
}
|
|
188
|
+
|
|
157
189
|
/**
|
|
158
190
|
* 이 한마디가 무슨 일인지 고른다.
|
|
159
191
|
*
|
|
@@ -186,10 +218,28 @@ export function route(text) {
|
|
|
186
218
|
근거[mode] = 걸린것;
|
|
187
219
|
}
|
|
188
220
|
|
|
189
|
-
|
|
221
|
+
/*
|
|
222
|
+
* 손대라는 말이 있으면 읽기만 하는 모드는 후보에서 뺀다.
|
|
223
|
+
*
|
|
224
|
+
* 점수를 깎지 않고 **줄에서 뺀다.** 깎으면 얼마나 깎을지가 또 하나의 눈금이
|
|
225
|
+
* 되고, 그 눈금은 아무도 못 지킨다. 여기서 가르는 것은 정도의 문제가 아니라
|
|
226
|
+
* 종류의 문제다 — 고칠 수 있는 자리인가 아닌가.
|
|
227
|
+
*/
|
|
228
|
+
const 손대라 = 손대라했나(s);
|
|
229
|
+
const 뺀것 = [];
|
|
230
|
+
let 순위 = Object.entries(점수들).sort((a, b) => b[1] - a[1]);
|
|
231
|
+
if (손대라) {
|
|
232
|
+
for (const [m, n] of 순위) if (읽기만하는모드.has(m) && n > 0) 뺀것.push(m);
|
|
233
|
+
순위 = 순위.filter(([m]) => !읽기만하는모드.has(m));
|
|
234
|
+
}
|
|
190
235
|
const [으뜸, 점수] = 순위[0] ?? [null, 0];
|
|
191
236
|
if (!으뜸 || 점수 < (문턱[으뜸] ?? 4)) {
|
|
192
|
-
|
|
237
|
+
// 왜 안 바꿨는지는 화면에 그대로 뜬다. "설계로 갈 뻔했는데 고치라는 말이라
|
|
238
|
+
// 안 보냈다" 와 "그냥 모르겠다" 는 사람에게 아주 다른 말이다.
|
|
239
|
+
const 왜 = 뺀것.length
|
|
240
|
+
? `${뺀것.join('·')} 신호가 있었지만 고치라는 말이라 읽기 전용 모드로 안 보냄`
|
|
241
|
+
: '무슨 일인지 뚜렷하지 않음';
|
|
242
|
+
return { mode: null, score: 점수, why: 왜, 겹침: false, 점수들, 뺀것 };
|
|
193
243
|
}
|
|
194
244
|
|
|
195
245
|
// 1·2등이 붙으면 고르지 않는다. 반반인 것을 억지로 정하면 절반은 틀린다.
|
|
@@ -201,7 +251,8 @@ export function route(text) {
|
|
|
201
251
|
return {
|
|
202
252
|
mode: 으뜸,
|
|
203
253
|
score: 점수,
|
|
204
|
-
why: 근거[으뜸].slice(0, 2).map((x) => `'${x}'`).join(', '),
|
|
254
|
+
why: [근거[으뜸].slice(0, 2).map((x) => `'${x}'`).join(', '),
|
|
255
|
+
뺀것.length ? `(${뺀것.join('·')} 는 고치라는 말이라 뺐습니다)` : ''].filter(Boolean).join(' '),
|
|
205
256
|
겹침: false,
|
|
206
257
|
점수들,
|
|
207
258
|
};
|
package/src/agent/session.js
CHANGED
|
@@ -50,6 +50,7 @@ const BASE_RULES = `너는 deel 다. 사용자의 작업 폴더 안에서 코드
|
|
|
50
50
|
- 같은 도구를 같은 인자로 다시 부르지 않는다. 결과는 같다. 본 것은 기억하고 다음으로 넘어간다.
|
|
51
51
|
- 사용자가 볼 범위를 못 박아 말하면 그 범위를 지킨다. 안 그러면 필요한 만큼 찾아본다.
|
|
52
52
|
- 명령 실행이 필요하면 Bash 를 쓴다. 되돌릴 수 없는 명령은 막히니 다른 방법을 찾는다.
|
|
53
|
+
- 중간 파일이 필요하면 /tmp 말고 .deel/tmp/ 에 쓴다. 작업 폴더 밖은 막힌다.
|
|
53
54
|
- 사용자에게 답할 때는 한국어로, 짧게. 코드를 통째로 붙여넣지 말고 무엇이 달라졌는지 말한다.
|
|
54
55
|
|
|
55
56
|
다음에도 쓸 것은 남긴다:
|
|
@@ -130,6 +131,7 @@ Rules:
|
|
|
130
131
|
- Do not call the same tool with the same arguments twice. The result will be the same. Remember what you saw and move on.
|
|
131
132
|
- If the user names the scope to look at, stay inside it. Otherwise look as far as you need.
|
|
132
133
|
- Use Bash when you need to run a command. Commands that cannot be undone are blocked, so find another way.
|
|
134
|
+
- If you need a scratch file, write it under .deel/tmp/, not /tmp. Outside the working folder is blocked.
|
|
133
135
|
- Answer the user in English, briefly. Do not paste whole files back — say what changed.
|
|
134
136
|
|
|
135
137
|
Keep what will be needed again:
|
package/src/repl.js
CHANGED
|
@@ -36,6 +36,7 @@ import { explain, shows as levelShows } from './ui/level.js';
|
|
|
36
36
|
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
37
37
|
import { 추천, 채울글 } from './ui/complete.js';
|
|
38
38
|
import { 접어쓰기 } from './ui/wrap.js';
|
|
39
|
+
import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as 붙임쓴번호들 } from './ui/pastechip.js';
|
|
39
40
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
40
41
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
41
42
|
import { expand as expandMentions } from './agent/mention.js';
|
|
@@ -430,9 +431,39 @@ export async function chatLoop(opts = {}) {
|
|
|
430
431
|
* (TDZ). 켜자마자 아무 키나 누르면 죽는 프로그램이 되는 셈이다.
|
|
431
432
|
*/
|
|
432
433
|
let turn = null;
|
|
434
|
+
|
|
435
|
+
/*
|
|
436
|
+
* 멈추라는 것이 닿았다고 **곧바로** 화면에 적는다.
|
|
437
|
+
*
|
|
438
|
+
* 멈추기는 즉시가 아니다. 돌던 자식 프로그램(rg·soffice)을 죽이고, 안 돈
|
|
439
|
+
* 도구 자리를 채우고, 대화를 성하게 닫는 데 잠깐이 걸린다. 그 잠깐 동안
|
|
440
|
+
* 화면이 아까 하던 말을 그대로 하고 있으면 사람은 안 먹었다고 판단한다.
|
|
441
|
+
* 그러면 또 누르고, 그러다 Ctrl+C 로 손이 가고, 두 번 누르면 대화가 닫힌다.
|
|
442
|
+
*
|
|
443
|
+
* "ESC 를 눌러도 안 멈춘다" 는 제보의 절반은 진짜로 안 멈춘 것이었고
|
|
444
|
+
* (동기 호출이 키를 안 배달했다), 나머지 절반이 이것이다 — 멈추고는
|
|
445
|
+
* 있는데 그렇게 보이지 않았다.
|
|
446
|
+
*/
|
|
447
|
+
const 멈추는중 = () => { try { 화면.일바꿈('멈춤'); } catch { /* 상자가 없는 화면도 있다 */ } };
|
|
448
|
+
|
|
433
449
|
// 붙여넣는 중인가 (bracketed paste). 키 처리와 'line' 이 같이 본다.
|
|
434
450
|
let 붙여넣는중 = false;
|
|
435
451
|
|
|
452
|
+
/*
|
|
453
|
+
* ── 붙여넣은 덩이를 접어 둔다 ────────────────────────────────────────
|
|
454
|
+
*
|
|
455
|
+
* 상자에 남는 것은 `[붙여넣기 #1 · 47줄 · 2.1KB]` 한 줄이고, 원문은
|
|
456
|
+
* 여기 그대로 있다가 보낼 때 되돌아간다. 왜 접는지는 pastechip.js 에.
|
|
457
|
+
*
|
|
458
|
+
* 접기 전 화면이 어디까지였는지도 같이 적어 둔다. 붙여넣기는 커서
|
|
459
|
+
* 자리에 끼어 들어오므로, 무엇이 새로 온 것인지는 시작 표를 받은
|
|
460
|
+
* 순간을 기억해 두어야만 안다.
|
|
461
|
+
*/
|
|
462
|
+
const 붙인것들 = new Map();
|
|
463
|
+
let 붙임번호 = 0;
|
|
464
|
+
let 붙임앞줄수 = 0;
|
|
465
|
+
let 붙임앞입력 = '';
|
|
466
|
+
|
|
436
467
|
/** 줄을 쌓아 두고 "다음 줄" 이라고 알린다. 백틱 표시와 Alt+Enter 가 같이 쓴다. */
|
|
437
468
|
const 줄쌓기 = (줄) => {
|
|
438
469
|
const 첫줄 = 이어쓰기줄들 === null;
|
|
@@ -529,8 +560,22 @@ export async function chatLoop(opts = {}) {
|
|
|
529
560
|
화면.대기갱신('', queue.length + 1);
|
|
530
561
|
}
|
|
531
562
|
}
|
|
532
|
-
|
|
533
|
-
|
|
563
|
+
/*
|
|
564
|
+
* ── 접어 둔 붙여넣기를 여기서 편다 ──────────────────────────────────
|
|
565
|
+
*
|
|
566
|
+
* 화면에 남는 것과 모델에게 가는 것이 여기서만 일부러 다르다. 화면에는
|
|
567
|
+
* `[붙여넣기 #1 · 47줄 · 2.1KB]` 이 남고, 모델에게는 그 47줄이 그대로
|
|
568
|
+
* 간다. 표가 무엇을 담고 있었는지를 말해 주므로 나중에 스크롤을 올려도
|
|
569
|
+
* 무엇을 시켰는지 알아볼 수 있다.
|
|
570
|
+
*
|
|
571
|
+
* 펴는 자리는 여기 한 군데다. 미리 쳐 둔 것이든 기다리다 보낸 것이든
|
|
572
|
+
* 결국 이 줄을 지나가므로, 여기서 펴면 어느 길로 와도 원문이 간다.
|
|
573
|
+
*/
|
|
574
|
+
const 펴진것 = 붙임펼치기(보낼것, 붙인것들);
|
|
575
|
+
// 다 쓴 것은 치운다. 안 치우면 대화가 길어질수록 붙인 원문이 계속 쌓인다.
|
|
576
|
+
for (const 번호 of 붙임쓴번호들(보낼것)) 붙인것들.delete(번호);
|
|
577
|
+
if (waiter) { const w = waiter; waiter = null; w(펴진것); }
|
|
578
|
+
else queue.push(펴진것);
|
|
534
579
|
});
|
|
535
580
|
rl.on('close', () => {
|
|
536
581
|
closed = true;
|
|
@@ -592,11 +637,53 @@ export async function chatLoop(opts = {}) {
|
|
|
592
637
|
process.on('exit', () => { try { process.stdout.write('\x1b[?2004l'); } catch { /* 이미 닫혔다 */ } });
|
|
593
638
|
|
|
594
639
|
process.stdin.on('keypress', (_ch, key) => {
|
|
595
|
-
if (key?.sequence === '\x1b[200~') {
|
|
640
|
+
if (key?.sequence === '\x1b[200~') {
|
|
641
|
+
붙여넣는중 = true;
|
|
642
|
+
붙임앞줄수 = 이어쓰기줄들?.length ?? 0;
|
|
643
|
+
붙임앞입력 = rl.line ?? '';
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
596
646
|
if (key?.sequence === '\x1b[201~') {
|
|
597
647
|
붙여넣는중 = false;
|
|
598
|
-
|
|
599
|
-
|
|
648
|
+
/*
|
|
649
|
+
* 이번에 들어온 것만 골라낸다.
|
|
650
|
+
*
|
|
651
|
+
* 붙여넣기에 줄바꿈이 있었으면 readline 이 줄마다 'line' 을 쐈고,
|
|
652
|
+
* 그 줄들은 붙임앞줄수 뒤에 쌓여 있다. 마지막 조각은 아직 입력칸에
|
|
653
|
+
* 남아 있다. 그리고 붙이기 전에 치고 있던 글(앞머리)은 **첫 줄
|
|
654
|
+
* 앞에 붙어서** 같이 나갔으므로 떼어 내야 한다.
|
|
655
|
+
*
|
|
656
|
+
* 커서를 글 가운데 두고 붙이면 어디까지가 붙인 것인지 가릴 수
|
|
657
|
+
* 없다. 그때는 접지 않는다 — 접기는 되돌릴 수 있어야 하고, 잘못
|
|
658
|
+
* 가른 접기는 못 되돌린다.
|
|
659
|
+
*/
|
|
660
|
+
const 새줄들 = (이어쓰기줄들 ?? []).slice(붙임앞줄수);
|
|
661
|
+
const 지금입력 = rl.line ?? '';
|
|
662
|
+
const 앞머리 = 붙임앞입력;
|
|
663
|
+
let 붙인줄들 = null;
|
|
664
|
+
if (새줄들.length) {
|
|
665
|
+
if (!앞머리 || 새줄들[0].startsWith(앞머리)) {
|
|
666
|
+
붙인줄들 = [새줄들[0].slice(앞머리.length), ...새줄들.slice(1), 지금입력];
|
|
667
|
+
}
|
|
668
|
+
} else if (지금입력.startsWith(앞머리)) {
|
|
669
|
+
붙인줄들 = [지금입력.slice(앞머리.length)];
|
|
670
|
+
}
|
|
671
|
+
const 붙인것 = 붙인줄들 ? 펴기(붙인줄들.join('\n')) : '';
|
|
672
|
+
const 폭 = Math.max(20, (process.stdout.columns ?? 80) - 8);
|
|
673
|
+
|
|
674
|
+
if (붙인줄들 && 입력기다림 && 묻는중 === null && 붙임접을까(붙인것, { 폭 })) {
|
|
675
|
+
const 번호 = ++붙임번호;
|
|
676
|
+
붙인것들.set(번호, 붙인것);
|
|
677
|
+
이어쓰기줄들 = 붙임앞줄수 ? (이어쓰기줄들 ?? []).slice(0, 붙임앞줄수) : null;
|
|
678
|
+
if (이어쓰기줄들 === null) 이어쓴것 = false;
|
|
679
|
+
const 표 = 붙임표(번호, 붙인것);
|
|
680
|
+
rl.line = 앞머리 + 표;
|
|
681
|
+
rl.cursor = rl.line.length;
|
|
682
|
+
화면.입력지움();
|
|
683
|
+
say(` ${c.gray(`${표} — 접어 뒀습니다. 보낼 때 그대로 펴집니다.`)}`);
|
|
684
|
+
} else if (이어쓰기줄들?.length && 입력기다림) {
|
|
685
|
+
// 접지 않은 덩이는 몇 줄인지라도 알려 준다. 안 보이면 사라진 줄
|
|
686
|
+
// 알고 다시 붙인다.
|
|
600
687
|
화면.입력지움();
|
|
601
688
|
say(` ${c.gray(`${이어쓰기줄들.length + 1}줄을 붙여넣었습니다 — Enter 로 한 번에 보냅니다`)}`);
|
|
602
689
|
}
|
|
@@ -615,7 +702,7 @@ export async function chatLoop(opts = {}) {
|
|
|
615
702
|
* 치던 글이 날아가면 그게 더 놀랍다.
|
|
616
703
|
*/
|
|
617
704
|
if (key?.name === 'escape' && !key.ctrl && !key.meta && !key.shift) {
|
|
618
|
-
if (turn && !turn.signal.aborted) { turn.abort(); return; }
|
|
705
|
+
if (turn && !turn.signal.aborted) { turn.abort(); 멈추는중(); return; }
|
|
619
706
|
return;
|
|
620
707
|
}
|
|
621
708
|
// Shift+Tab — 승인 방식 (자동 → 위험만 → 모두)
|
|
@@ -909,11 +996,26 @@ export async function chatLoop(opts = {}) {
|
|
|
909
996
|
say('');
|
|
910
997
|
},
|
|
911
998
|
|
|
912
|
-
ask물음: async (물음, 고를것 = []) => {
|
|
999
|
+
ask물음: async (물음, 고를것 = [], 이해 = '') => {
|
|
913
1000
|
if (closed) return null;
|
|
1001
|
+
const 폭 = Math.max(40, Math.min(88, (process.stdout.columns || 80) - 6)) - 4;
|
|
914
1002
|
say('');
|
|
915
1003
|
say(` ${c.hcyan('┌')} ${c.bold(옮긴말('ask.title'))}`);
|
|
916
|
-
|
|
1004
|
+
/*
|
|
1005
|
+
* 물음보다 **이해를 먼저** 보여 준다.
|
|
1006
|
+
*
|
|
1007
|
+
* 사람이 이 상자에서 제일 먼저 알고 싶은 것은 "얘가 내 말을 알아듣긴
|
|
1008
|
+
* 했나" 이다. 그걸 모르는 채 선택지부터 보면, 답을 고르는 것이 아니라
|
|
1009
|
+
* 딴소리를 하는지 아닌지를 먼저 가늠해야 한다. 순서가 뒤바뀌면
|
|
1010
|
+
* 물음 자체가 짐이 된다.
|
|
1011
|
+
*/
|
|
1012
|
+
if (String(이해 ?? '').trim()) {
|
|
1013
|
+
for (const 줄 of 접어쓰기(`알아들은 것: ${String(이해).trim()}`, 폭)) {
|
|
1014
|
+
say(` ${c.hcyan('│')} ${c.gray(줄)}`);
|
|
1015
|
+
}
|
|
1016
|
+
say(` ${c.hcyan('│')}`);
|
|
1017
|
+
}
|
|
1018
|
+
for (const 줄 of 접어쓰기(물음, 폭)) {
|
|
917
1019
|
say(` ${c.hcyan('│')} ${c.white(줄)}`);
|
|
918
1020
|
}
|
|
919
1021
|
if (고를것.length) {
|
|
@@ -1138,6 +1240,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1138
1240
|
}
|
|
1139
1241
|
if (turn && !turn.signal.aborted) {
|
|
1140
1242
|
turn.abort();
|
|
1243
|
+
멈추는중();
|
|
1141
1244
|
return; // 화면 정리는 루프 쪽 'aborted' 이벤트가 한다
|
|
1142
1245
|
}
|
|
1143
1246
|
if (interrupted) { rl.close(); return; }
|
|
@@ -1298,6 +1401,15 @@ export async function chatLoop(opts = {}) {
|
|
|
1298
1401
|
// 이 턴이 탈 없이 끝났나. 끊겼거나 터진 뒤에 승인 창을 띄우면 안 된다 —
|
|
1299
1402
|
// 계획이 반만 나온 것을 두고 "이대로 진행할까요?" 를 묻는 꼴이 된다.
|
|
1300
1403
|
let 턴탈났나 = false;
|
|
1404
|
+
/*
|
|
1405
|
+
* 걸음을 다 써서 중간에 끊긴 턴인가. 끊겼을 때 남은 할 일을 여기 담는다.
|
|
1406
|
+
*
|
|
1407
|
+
* 「중간에 끊기는 이슈가 생기는거 같은데」 — 여기가 그 자리다. 여태는
|
|
1408
|
+
* "이어서 하려면 다시 말씀하세요" 라고만 하고 끝났다. 사람은 방금 화면에
|
|
1409
|
+
* 다 적혀 있는 남은 할 일을 손으로 다시 옮겨 적거나, "이어서 해줘" 라고만
|
|
1410
|
+
* 쳤다. 그러면 모델은 무엇을 잇는지 몰라서 처음부터 다시 뒤진다.
|
|
1411
|
+
*/
|
|
1412
|
+
let 끊긴할일 = null;
|
|
1301
1413
|
let thinkChars = 0;
|
|
1302
1414
|
let streamed = false;
|
|
1303
1415
|
/*
|
|
@@ -1700,14 +1812,29 @@ export async function chatLoop(opts = {}) {
|
|
|
1700
1812
|
if (session.level === '개발자' && ev.from) say(` ${c.gray(clip(ev.from, 110))}`);
|
|
1701
1813
|
break;
|
|
1702
1814
|
|
|
1815
|
+
/*
|
|
1816
|
+
* 답이 천장에서 잘렸고 우리가 더 해 줄 것이 없다.
|
|
1817
|
+
*
|
|
1818
|
+
* 조용히 넘기면 화면에는 중간에서 끊긴 답만 남는다 — 사람은 모델이
|
|
1819
|
+
* 대충 답한 줄 알고 같은 것을 다시 시키고, 같은 자리에서 또 잘린다.
|
|
1820
|
+
* 손댈 자리를 그 자리에서 알려 준다.
|
|
1821
|
+
*/
|
|
1822
|
+
case 'capped':
|
|
1823
|
+
clearThinking();
|
|
1824
|
+
say(` ${mark.warn} ${c.gray(`답이 ${c.white(ev.cap.toLocaleString())} 토큰에서 잘렸습니다`)}`
|
|
1825
|
+
+ `${ev.정한값 ? c.gray(' (직접 정하신 상한입니다)') : c.gray(' — 더 못 올리는 천장입니다')}`);
|
|
1826
|
+
say(` ${c.gray('한 번에 더 길게 받으려면')} ${c.cyan('/out 32k')}${c.gray('. 파일을 쓰는 중이었다면 Append 로 나눠 쓰게 하세요.')}`);
|
|
1827
|
+
break;
|
|
1828
|
+
|
|
1703
1829
|
case 'compact_failed':
|
|
1704
1830
|
접기멈춤();
|
|
1705
1831
|
say(` ${c.gray(`(접지 못했습니다: ${ev.why})`)}`);
|
|
1706
1832
|
break;
|
|
1707
1833
|
|
|
1708
1834
|
case 'limit':
|
|
1835
|
+
끊긴할일 = ev.남은할일 ?? [];
|
|
1709
1836
|
say('');
|
|
1710
|
-
say(` ${mark.warn} 도구 호출 ${ev.steps}회에서
|
|
1837
|
+
say(` ${mark.warn} 도구 호출 ${ev.steps}회에서 멈췄습니다.`);
|
|
1711
1838
|
// 무엇이 안 끝났는지 그 자리에 적는다. 위로 스크롤해 도구 줄을
|
|
1712
1839
|
// 세어 보게 하면, 이어서 시킬 때 무엇을 시켜야 할지 알 수 없다.
|
|
1713
1840
|
if (ev.남은할일?.length) {
|
|
@@ -1774,13 +1901,28 @@ export async function chatLoop(opts = {}) {
|
|
|
1774
1901
|
clearThinking();
|
|
1775
1902
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1776
1903
|
say('');
|
|
1777
|
-
say(
|
|
1904
|
+
say(ev.why === '요청누락'
|
|
1905
|
+
? ` ${c.gray(`↺ 시킨 것 ${ev.빠진?.length ?? 0}가지를 빼놓고 끝내려고 해서 되밀었습니다`)}`
|
|
1906
|
+
: ` ${c.gray('↺ 읽기만 하고 끝내려고 해서 한 번 되밀었습니다')}`);
|
|
1778
1907
|
break;
|
|
1779
1908
|
|
|
1780
1909
|
case 'done':
|
|
1781
1910
|
clearThinking();
|
|
1782
1911
|
if (streamed) { 답비우기(); say(''); }
|
|
1783
1912
|
만든파일보이기(ev.files);
|
|
1913
|
+
/*
|
|
1914
|
+
* 되밀고도 그대로면 사람에게 말한다.
|
|
1915
|
+
*
|
|
1916
|
+
* 조용히 넘어가면 사람은 다 된 줄 알고 그 위에 다음 것을 쌓는다.
|
|
1917
|
+
* 나중에 발견했을 때는 이미 늦다. 「몇 번 까먹거나 누락되거나」라는
|
|
1918
|
+
* 제보가 정확히 그 모양이었다 — 빠진 것 자체보다, 빠졌다는 말을
|
|
1919
|
+
* 안 한 것이 문제다.
|
|
1920
|
+
*/
|
|
1921
|
+
if (ev.빠진?.length) {
|
|
1922
|
+
say('');
|
|
1923
|
+
say(` ${c.yellow('!')} ${c.white(`시킨 것 중 ${ev.빠진.length}가지가 안 다뤄졌습니다`)}`);
|
|
1924
|
+
for (const x of ev.빠진) say(` ${c.gray(`${x.번호}. ${clip(x.글, 68)}`)}`);
|
|
1925
|
+
}
|
|
1784
1926
|
break;
|
|
1785
1927
|
}
|
|
1786
1928
|
}
|
|
@@ -1886,6 +2028,51 @@ export async function chatLoop(opts = {}) {
|
|
|
1886
2028
|
say('');
|
|
1887
2029
|
say(` ${c.hcyan('☰')} ${c.gray('그 점을 반영해 계획을 다시 냅니다.')}`);
|
|
1888
2030
|
}
|
|
2031
|
+
} else if (끊긴할일) {
|
|
2032
|
+
/*
|
|
2033
|
+
* ── 끊긴 자리에서 ⏎ 하나로 잇는다 ────────────────────────────────
|
|
2034
|
+
*
|
|
2035
|
+
* 「중간에 끊기는 이슈가 생기는거 같은데 해당 문제점들이 극복되면 좋겠어」
|
|
2036
|
+
*
|
|
2037
|
+
* 걸음 상한은 안전장치라 없앨 수 없다 — 그게 없으면 헛도는 턴이
|
|
2038
|
+
* 컨텍스트를 다 먹을 때까지 안 멈춘다. 없앨 게 아니라 **잇는 길**을
|
|
2039
|
+
* 놔야 한다.
|
|
2040
|
+
*
|
|
2041
|
+
* 이어갈 말에는 남은 단계를 **글자 그대로 싣는다.** 계획 승인에서
|
|
2042
|
+
* 배운 것과 같은 이유다 — "아까 하던 것 이어서 해라" 는 가리키기만
|
|
2043
|
+
* 하고 안 주는 말이라, 작은 모델은 그 '아까' 를 찾으러 지난 대화를
|
|
2044
|
+
* 뒤지기 시작한다.
|
|
2045
|
+
*/
|
|
2046
|
+
// 끝난 것을 빼는 것은 loop.js 가 이미 한 번 한다. 여기서 또 하는 것은
|
|
2047
|
+
// 이 말이 **끝난 일을 다시 시키는 말**이 되면 안 되기 때문이다 —
|
|
2048
|
+
// 저쪽 걸러내기가 언젠가 바뀌어도 이 자리는 안 흔들려야 한다.
|
|
2049
|
+
const 남은 = 끊긴할일.filter((t) => t && t.state !== 'done');
|
|
2050
|
+
say('');
|
|
2051
|
+
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
2052
|
+
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
2053
|
+
const 답 = String(await ask(
|
|
2054
|
+
`이어서 할까요? ${c.gray('⏎ 이어서 · n 그만')}`,
|
|
2055
|
+
{ def: 'y' },
|
|
2056
|
+
)).trim().toLowerCase();
|
|
2057
|
+
|
|
2058
|
+
if (['n', 'no', 'ㄴ', '취소', '그만', '아니', '아니요', '아니오'].includes(답)) {
|
|
2059
|
+
say(` ${c.gray('여기서 멈췄습니다. 나중에 「이어서 해줘」 라고 하셔도 됩니다.')}`);
|
|
2060
|
+
} else {
|
|
2061
|
+
const 단계글 = 남은.map((t, i) => `${i + 1}. ${String(t.text ?? '')}`).join('\n');
|
|
2062
|
+
이어갈것 = (남은.length
|
|
2063
|
+
? '걸음 수를 다 써서 중간에 멈췄다. 사람이 이어서 하라고 했다.\n\n'
|
|
2064
|
+
+ `아직 안 끝난 것은 아래 그대로다.\n\n${단계글}\n\n`
|
|
2065
|
+
+ '이것부터 이어서 실제로 해라.'
|
|
2066
|
+
: '걸음 수를 다 써서 중간에 멈췄다. 사람이 이어서 하라고 했다.'
|
|
2067
|
+
+ ' 하던 자리에서 이어서 해라.')
|
|
2068
|
+
+ ' **이미 끝낸 것은 다시 하지 마라.** 지난 대화를 뒤지지 마라 —'
|
|
2069
|
+
+ ' 필요한 것은 이 대화 안에 다 있다. 처음부터 다시 시작하지도 마라.';
|
|
2070
|
+
// 모드를 다시 고르지 않는다. 이 말에는 '해라' 가 들어 있어서 다시
|
|
2071
|
+
// 고르면 엉뚱한 데로 가고, 애초에 하던 일을 잇는 것이라 바뀔 이유가 없다.
|
|
2072
|
+
이어갈모드 = session.routed ?? (session.work === 'auto' ? 'code' : session.work);
|
|
2073
|
+
say('');
|
|
2074
|
+
say(` ${c.hgreen('▶')} ${c.gray(`끊긴 자리에서 이어서 합니다${남은.length ? ` (남은 ${남은.length}가지)` : ''}.`)}`);
|
|
2075
|
+
}
|
|
1889
2076
|
}
|
|
1890
2077
|
}
|
|
1891
2078
|
|