deel-local-cli 1.7.0 → 1.10.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 +1166 -0
- package/README.md +1223 -1107
- package/bin/deel.js +78 -12
- package/package.json +4 -4
- package/src/acp/map.js +1 -1
- package/src/acp/serve.js +40 -3
- package/src/agent/compact.js +314 -296
- package/src/agent/effort.js +27 -7
- package/src/agent/evidence.js +2 -0
- package/src/agent/filemem.js +141 -0
- package/src/agent/grade.js +51 -1
- package/src/agent/loop.js +218 -40
- package/src/agent/session.js +136 -14
- package/src/agent/store.js +186 -9
- package/src/agent/threads.js +26 -1
- package/src/backend/adapter.js +272 -23
- 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 +29 -5
- package/src/backend/retry.js +3 -0
- package/src/backend/scanui.js +1 -1
- package/src/backend/toolfit.js +325 -0
- package/src/commands.js +170 -61
- package/src/completion.js +20 -2
- package/src/config.js +47 -2
- package/src/i18n/en.js +210 -3
- package/src/i18n/index.js +40 -0
- package/src/i18n/ja.js +204 -3
- package/src/i18n/ko.js +270 -3
- package/src/i18n/zh.js +204 -3
- package/src/lsp/client.js +49 -5
- package/src/oneshot.js +92 -5
- package/src/pack/sbom.js +30 -4
- package/src/pack/selfpack.js +26 -10
- package/src/pack/sheet.en.js +288 -0
- package/src/pack/tar.js +65 -2
- package/src/plugins/manage.js +46 -9
- package/src/repl.js +344 -61
- package/src/reset.js +397 -0
- package/src/safety/audit.js +95 -8
- package/src/safety/authcmd.js +316 -0
- package/src/safety/guard.js +143 -0
- package/src/safety/keystore.js +114 -38
- package/src/safety/undo.js +25 -6
- package/src/tools/desc.en.js +19 -14
- package/src/tools/index.js +349 -75
- package/src/tools/jobs.js +175 -42
- package/src/tools/lsp.js +5 -4
- package/src/tools/outline.js +7 -3
- package/src/tools/task.js +10 -6
- package/src/tools/todo.js +9 -2
- package/src/tools/verify.js +42 -7
- package/src/tools/webfetch.js +94 -10
- package/src/ui/export.js +1 -1
- 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/README.en.md +0 -1117
package/src/repl.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 대화 화면. 루프가 보내는 이벤트를 Claude Code 풍으로 그린다.
|
|
2
2
|
import { createInterface, emitKeypressEvents } from 'node:readline';
|
|
3
|
-
import {
|
|
3
|
+
import { 규칙모으기, 정책읽기 } from './safety/policy.js';
|
|
4
|
+
import { 받기설정 } from './safety/authcmd.js';
|
|
4
5
|
import { 주소가리기 } from './safety/secrets.js';
|
|
5
6
|
import { homedir } from 'node:os';
|
|
6
7
|
import { resolve, basename } from 'node:path';
|
|
@@ -8,7 +9,7 @@ import { c, say as 바로쓰기, mark, clip } from './ui/ansi.js';
|
|
|
8
9
|
import { headerLines } from './ui/status.js';
|
|
9
10
|
import { 종, 창제목, 제목되돌리기, 알릴까, 제목글 } from './ui/notify.js';
|
|
10
11
|
import { 보이기 as 인트로, 기본곁말 } from './ui/intro.js';
|
|
11
|
-
import { 언어잡기, 말 as
|
|
12
|
+
import { 언어잡기, 말 as 옮긴말, 세말 } from './i18n/index.js';
|
|
12
13
|
import { 알림채움 } from './backend/retry.js';
|
|
13
14
|
import { 화면고르기 } from './ui/screen.js';
|
|
14
15
|
import { STAGES } from './agent/effort.js';
|
|
@@ -21,8 +22,8 @@ import { makeScope } from './safety/guard.js';
|
|
|
21
22
|
import { 언어서버있나 } from './tools/index.js';
|
|
22
23
|
import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
|
|
23
24
|
import { History } from './safety/undo.js';
|
|
24
|
-
import { Audit } from './safety/audit.js';
|
|
25
|
-
import { activeProfile, load, resolveKey, save as saveCfg, homeDir,
|
|
25
|
+
import { Audit, 열쇠묻기 } from './safety/audit.js';
|
|
26
|
+
import { activeProfile, load, resolveKey, save as saveCfg, homeDir, 잠금소식, 열쇠탈소식 } from './config.js';
|
|
26
27
|
import { discover } from './skills/discover.js';
|
|
27
28
|
import { allowEndpoint, setOffline, isOffline } from './safety/network.js';
|
|
28
29
|
import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
|
|
@@ -33,12 +34,13 @@ import { 못박기 } from './agent/pins.js';
|
|
|
33
34
|
import { 카드 } from './agent/card.js';
|
|
34
35
|
import { 배움 } from './agent/evolve.js';
|
|
35
36
|
import { 마크다운 } from './ui/md.js';
|
|
36
|
-
import { askHidden } from './ui/prompt.js';
|
|
37
|
+
import { askHidden, confirm } from './ui/prompt.js';
|
|
37
38
|
import { explain, shows as levelShows } from './ui/level.js';
|
|
38
39
|
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
39
40
|
import { 추천, 채울글 } from './ui/complete.js';
|
|
40
41
|
import { 접어쓰기 } from './ui/wrap.js';
|
|
41
|
-
import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as
|
|
42
|
+
import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as 붙임쓴번호들, 남길글 } from './ui/pastechip.js';
|
|
43
|
+
import { 고른것풀기, 계획답풀기 } from './ui/pick.js';
|
|
42
44
|
import { 이력지킴이 } from './ui/histline.js';
|
|
43
45
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
44
46
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
@@ -70,7 +72,7 @@ const TOOL_GLYPH = {
|
|
|
70
72
|
function toolLabel(name, args) {
|
|
71
73
|
const a = args ?? {};
|
|
72
74
|
const first =
|
|
73
|
-
a.file_path ?? a.pattern ?? a.path ?? a.url ?? a.name ?? a.목적 ??
|
|
75
|
+
a.file_path ?? a.pattern ?? a.path ?? a.url ?? a.name ?? a.purpose ?? a.목적 ??
|
|
74
76
|
(a.command ? String(a.command).replace(/\s+/g, ' ').slice(0, 52) : null) ??
|
|
75
77
|
// 뒤에서 도는 명령. 번호가 곧 그 일감의 이름이다 — 빈 괄호를 띄우면
|
|
76
78
|
// 어느 것을 보고 있는지가 화면에서 사라진다. 서너 개를 띄워 놓고 나면
|
|
@@ -79,27 +81,27 @@ function toolLabel(name, args) {
|
|
|
79
81
|
//
|
|
80
82
|
// 이름 고르기는 jobs.js 에 맡긴다. 모델이 `job` 으로 보낼 수도 있는데,
|
|
81
83
|
// 여기서 `a.번호` 만 보면 도구는 제대로 도는데 화면만 빈 괄호가 된다.
|
|
82
|
-
(() => { const g = 일감인자(a); return g.번호 != null ? `${g.번호}
|
|
84
|
+
(() => { const g = 일감인자(a); return g.번호 != null ? `${옮긴말('job.no', { n: g.번호 })}${g.끝내기 ? ` · ${옮긴말('job.stop')}` : ''}` : null; })() ??
|
|
83
85
|
// 한 번에 여러 파일을 쓸 때. 빈 괄호를 띄우면 화면만 보고는 무엇을
|
|
84
86
|
// 만들었는지 알 수 없다 — 첫 파일과 개수를 적는다.
|
|
85
87
|
(Array.isArray(a.files) && a.files.length
|
|
86
|
-
? `${a.files[0]?.file_path ?? '?'}${a.files.length > 1 ? `
|
|
88
|
+
? `${a.files[0]?.file_path ?? '?'}${a.files.length > 1 ? ` ${옮긴말('more.files', { n: a.files.length - 1 })}` : ''}`
|
|
87
89
|
: null) ??
|
|
88
90
|
// 한 번에 여러 군데를 고칠 때. 파일 수와 군데 수는 다르다 — 한 파일을
|
|
89
91
|
// 여섯 군데 고치는 것이 보통이라 '외 5개' 라고 적으면 거짓이 된다.
|
|
90
92
|
(Array.isArray(a.edits) && a.edits.length
|
|
91
|
-
? `${a.edits[0]?.file_path ?? '?'}${a.edits.length > 1 ? `
|
|
93
|
+
? `${a.edits[0]?.file_path ?? '?'}${a.edits.length > 1 ? ` ${옮긴말('more.spots', { n: a.edits.length - 1 })}` : ''}`
|
|
92
94
|
: null) ??
|
|
93
|
-
(Array.isArray(a.paths) && a.paths.length ?
|
|
95
|
+
(Array.isArray(a.paths) && a.paths.length ? 세말('count', a.paths.length) : null) ??
|
|
94
96
|
// 할 일 목록은 보여줄 경로가 없다. 빈 괄호를 띄우느니 개수를 적는다.
|
|
95
|
-
(Array.isArray(a.todos) ?
|
|
97
|
+
(Array.isArray(a.todos) ? 세말('hits', a.todos.length) : null) ?? '';
|
|
96
98
|
const g = TOOL_GLYPH[name] ?? c.cyan('⏺');
|
|
97
99
|
const 안 = clip(String(first ?? ''), 56);
|
|
98
100
|
return `${g} ${c.bold(name)}${안 ? `${c.gray('(')}${c.gray(안)}${c.gray(')')}` : ''}`;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
function toolResultLine(result, ms) {
|
|
102
|
-
const t = ms > 700 ? c.gray(` ${(ms / 1000).toFixed(1)}
|
|
104
|
+
const t = ms > 700 ? c.gray(` ${옮긴말('unit.sec', { n: (ms / 1000).toFixed(1) })}`) : '';
|
|
103
105
|
if (result?.error) return `${c.red('└')} ${c.red(clip(String(result.error).split('\n')[0], 80))}${t}`;
|
|
104
106
|
|
|
105
107
|
/*
|
|
@@ -111,16 +113,16 @@ function toolResultLine(result, ms) {
|
|
|
111
113
|
*/
|
|
112
114
|
if (typeof result?.탈 === 'number') {
|
|
113
115
|
const 조각 = [];
|
|
114
|
-
if (result.탈) 조각.push(c.red(
|
|
115
|
-
if (result.확인됨) 조각.push(c.green(
|
|
116
|
-
if (result.못확인) 조각.push(c.yellow(
|
|
116
|
+
if (result.탈) 조각.push(c.red(옮긴말('sum.broken', { n: result.탈 })));
|
|
117
|
+
if (result.확인됨) 조각.push(c.green(옮긴말('sum.verified', { n: result.확인됨 })));
|
|
118
|
+
if (result.못확인) 조각.push(c.yellow(옮긴말('sum.unverified', { n: result.못확인 })));
|
|
117
119
|
return `${result.탈 ? c.red('└') : c.gray('└')} `
|
|
118
|
-
+ `${조각.join(c.gray(' · ')) || c.gray('
|
|
120
|
+
+ `${조각.join(c.gray(' · ')) || c.gray(옮긴말('sum.nothingToCheck'))}${t}`;
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
// 고친 자리는 몇 줄이 늘고 줄었는지를 요약 옆에 붙인다.
|
|
122
124
|
const 셈 = result?.diff ? ` ${shortStat(result.diff)}` : '';
|
|
123
|
-
return `${c.gray('└')} ${c.gray(clip(result?.summary ?? '
|
|
125
|
+
return `${c.gray('└')} ${c.gray(clip(result?.summary ?? 옮긴말('sum.done'), 80))}${셈}${t}`;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// 수준별로 몇 줄까지 펼칠지. 초보에게 60줄을 쏟으면 아무것도 안 읽는다.
|
|
@@ -166,6 +168,22 @@ export async function chatLoop(opts = {}) {
|
|
|
166
168
|
바로쓰기(` ${mark.ok} ${잠금}`);
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
/*
|
|
172
|
+
* 잠근 열쇠를 못 풀었으면 **그 까닭을** 적는다.
|
|
173
|
+
*
|
|
174
|
+
* 이걸 안 하면 빈 열쇠가 그대로 나가서 Authorization 머리말이 아예 안 붙고,
|
|
175
|
+
* 사람은 401 만 보고 방화벽·주소·계정을 뒤진다. 진짜 까닭은 대개
|
|
176
|
+
* 「설정 파일을 다른 PC 로 옮겼다」 이고, 그건 우리만 알고 있다.
|
|
177
|
+
*
|
|
178
|
+
* 아래 conn 을 짓는 자리에서 resolveKey 가 불리므로, 읽는 것은 그 뒤여야 한다.
|
|
179
|
+
*/
|
|
180
|
+
const 열쇠탈보이기 = () => {
|
|
181
|
+
const 탈 = 열쇠탈소식();
|
|
182
|
+
if (!탈) return;
|
|
183
|
+
바로쓰기('');
|
|
184
|
+
바로쓰기(` ${mark.warn} ${탈}`);
|
|
185
|
+
};
|
|
186
|
+
|
|
169
187
|
/*
|
|
170
188
|
* 여기서부터 화면에 나가는 것은 전부 `화면` 을 거친다.
|
|
171
189
|
*
|
|
@@ -185,6 +203,14 @@ export async function chatLoop(opts = {}) {
|
|
|
185
203
|
const conn = {
|
|
186
204
|
kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
|
|
187
205
|
key: resolveKey(prof), model: prof.model,
|
|
206
|
+
/*
|
|
207
|
+
* 열쇠를 갖고 있는 대신 **받아 오는** 설정 (safety/authcmd.js).
|
|
208
|
+
*
|
|
209
|
+
* 여기서는 설정만 싣는다. 명령은 요청 직전에 부른다 — 켤 때 한 번
|
|
210
|
+
* 받아 두면 한 시간짜리 토큰이 세 시간짜리 대화 한복판에서 죽고,
|
|
211
|
+
* 그 401 은 화면에서 「열쇠가 틀렸다」 와 구별이 안 된다.
|
|
212
|
+
*/
|
|
213
|
+
열쇠받기: 받기설정(prof, { 정책값: 정책읽기().값 }),
|
|
188
214
|
// 컨텍스트 길이. 순서가 곧 우선순위다 —
|
|
189
215
|
// deel --ctx 655360 > 프로필에 저장된 값 > 기본값
|
|
190
216
|
// 기본값으로 떨어졌다는 것은 '아직 못 쟀다' 는 뜻이다. 아래에서 그렇다고 말해 준다.
|
|
@@ -198,6 +224,9 @@ export async function chatLoop(opts = {}) {
|
|
|
198
224
|
vision: prof.vision ?? false,
|
|
199
225
|
};
|
|
200
226
|
|
|
227
|
+
// conn 을 짓느라 resolveKey 가 방금 불렸다. 못 푼 것이 있으면 여기서 말한다.
|
|
228
|
+
열쇠탈보이기();
|
|
229
|
+
|
|
201
230
|
/*
|
|
202
231
|
* ── 주소와 허가를 뗀다 ───────────────────────────────────────────────
|
|
203
232
|
*
|
|
@@ -267,9 +296,49 @@ export async function chatLoop(opts = {}) {
|
|
|
267
296
|
* 여기서 한 번만 읽는다. 상태줄은 글자를 칠 때마다 다시 그려지는데,
|
|
268
297
|
* 그때마다 설정 파일을 열면 디스크를 초당 수십 번 두드리게 된다.
|
|
269
298
|
*/
|
|
270
|
-
|
|
299
|
+
// `pricing` 으로 적어도 받는다 — 설정은 사람이 손으로 적는 것이라, 한글
|
|
300
|
+
// 자판이 없는 사람이 못 적는 이름 하나에 기능이 통째로 막히면 안 된다.
|
|
301
|
+
session.요금표 = cfg?.요금 ?? cfg?.pricing ?? null;
|
|
271
302
|
session.제공자 = prof?.제공자 ?? null;
|
|
272
303
|
|
|
304
|
+
/*
|
|
305
|
+
* ── 열쇠를 받아 오는 연결 (safety/authcmd.js) ───────────────────────
|
|
306
|
+
*
|
|
307
|
+
* 두 자리를 여기서 잇는다.
|
|
308
|
+
*
|
|
309
|
+
* 묻기 — 우리가 남이 적어 준 명령을 띄우려는 참이다. 한 번은 물어야
|
|
310
|
+
* 한다. 다만 **한 판에 한 번만** 묻는다 (authcmd 가 기억한다) —
|
|
311
|
+
* 한 시간짜리 토큰으로 세 시간 일하면 세 번 받아 오는데, 세 번
|
|
312
|
+
* 다 물으면 사람은 손이 가는 대로 누른다. 관리 정책이 준
|
|
313
|
+
* 명령은 아예 안 묻는다.
|
|
314
|
+
* 표시 — 사내 로그인은 브라우저를 열고 사람을 기다린다. 그동안 화면이
|
|
315
|
+
* 조용하면 사람은 멈춘 줄 안다. 무엇을 기다리는지 적는다.
|
|
316
|
+
*/
|
|
317
|
+
if (conn.열쇠받기) {
|
|
318
|
+
session.열쇠물어보기 = async (설정) => {
|
|
319
|
+
say('');
|
|
320
|
+
say(` ${mark.warn} ${c.gray('이 연결은 열쇠를 받아 옵니다. 아래 명령을 띄웁니다:')}`);
|
|
321
|
+
say(` ${c.white(clip(설정.명령, 88))}`);
|
|
322
|
+
say(` ${c.gray('받은 열쇠는 이 판이 도는 동안 메모리에만 있고, 파일로 안 적습니다.')}`);
|
|
323
|
+
const 예 = await confirm(' 띄울까요?', true);
|
|
324
|
+
say('');
|
|
325
|
+
return 예;
|
|
326
|
+
};
|
|
327
|
+
session.onAuth = (것) => {
|
|
328
|
+
if (것?.type === '시작') return 화면.돌리기(옮긴말('auth.waiting'));
|
|
329
|
+
if (것?.type === '끝') return 화면.돌림멈춤('');
|
|
330
|
+
화면.돌림멈춤('');
|
|
331
|
+
if (것?.ok) {
|
|
332
|
+
const 초 = Math.max(0, Math.round((것.만료 - Date.now()) / 1000));
|
|
333
|
+
return say(` ${mark.ok} ${c.gray(옮긴말('auth.got', { 분: Math.round(초 / 60) }))}`);
|
|
334
|
+
}
|
|
335
|
+
// 못 받았어도 대화를 안 끊는다. 원래 열쇠로 가 보고, 그것도 안 되면
|
|
336
|
+
// 서버가 한 말이 화면에 나온다. 여기서 끊으면 무엇이 문제인지 못 본다.
|
|
337
|
+
say(` ${mark.warn} ${c.gray(옮긴말('auth.failed', { 왜: 것?.왜 ?? '?' }))}`);
|
|
338
|
+
if (것?.보인것) say(` ${c.gray(clip(String(것.보인것), 88))}`);
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
273
342
|
// ── 대화 이어하기 ─────────────────────────────────────────────────────
|
|
274
343
|
// 껐다 켜도 이어지도록, 메시지가 오갈 때마다 .deel/sessions/ 에 바로 적는다.
|
|
275
344
|
let store = null;
|
|
@@ -320,7 +389,7 @@ export async function chatLoop(opts = {}) {
|
|
|
320
389
|
*/
|
|
321
390
|
const mcp붙임 = await 다붙이기(root, {
|
|
322
391
|
offline: isOffline(),
|
|
323
|
-
audit: new Audit(root),
|
|
392
|
+
audit: new Audit(root, { 열쇠들: 열쇠묻기(conn) }),
|
|
324
393
|
});
|
|
325
394
|
|
|
326
395
|
// 이 PC 에 있는 스킬·명령·플러그인을 찾아 붙인다. 품고 다니지 않는다.
|
|
@@ -565,7 +634,7 @@ export async function chatLoop(opts = {}) {
|
|
|
565
634
|
* 백틱으로 이어 쓴 경우에는 앞줄이 이미 찍혀 있으므로 마지막 줄만
|
|
566
635
|
* 이어 찍는다 — 안 그러면 같은 줄이 두 번 보인다.
|
|
567
636
|
*/
|
|
568
|
-
const 찍을것 = 이어쓴것 ? l :
|
|
637
|
+
const 찍을것 = 남길글(이어쓴것 ? l : 보낼것, 붙인것들);
|
|
569
638
|
if (찍을것.trim() || 이었나) {
|
|
570
639
|
for (const [i, 한줄] of String(찍을것).split('\n').entries()) {
|
|
571
640
|
say(`${i === 0 && !이어쓴것 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(한줄)}`);
|
|
@@ -712,7 +781,23 @@ export async function chatLoop(opts = {}) {
|
|
|
712
781
|
const 붙인것 = 붙인줄들 ? 펴기(붙인줄들.join('\n')) : '';
|
|
713
782
|
const 폭 = Math.max(20, (process.stdout.columns ?? 80) - 8);
|
|
714
783
|
|
|
715
|
-
|
|
784
|
+
/*
|
|
785
|
+
* ── 일하는 도중에 붙여도 접는다 ──────────────────────────────────
|
|
786
|
+
*
|
|
787
|
+
* 여태 이 갈래는 `입력기다림` 일 때만 돌았다. 그런데 붙여넣기는
|
|
788
|
+
* **일하는 도중이 더 흔하다** — 몇 분짜리 턴이 도는 동안 다음에
|
|
789
|
+
* 시킬 것을 미리 붙여 두는 것이 그것이다.
|
|
790
|
+
*
|
|
791
|
+
* 그때 무슨 일이 났나. 붙인 마흔 줄 중 마지막 한 줄만 rl.line 에
|
|
792
|
+
* 남고 나머지는 상자 밖 버퍼로 간다. 일하는 중 화면(대기갱신)은
|
|
793
|
+
* rl.line 만 그리므로, 사람 눈에는 **한 줄만 들어간 것**으로 보인다.
|
|
794
|
+
* 그래서 다시 붙인다. 이번엔 진짜로 두 번 들어간다 — 접기가 애초에
|
|
795
|
+
* 막으려던 바로 그 사고가, 기다릴 때는 안 나고 일할 때만 났다.
|
|
796
|
+
*
|
|
797
|
+
* 펴는 자리는 그대로다(아래 붙임펼치기). 어느 길로 보내든 원문이
|
|
798
|
+
* 간다 — 접는 자리만 넓혔지 되돌리는 길은 안 건드렸다.
|
|
799
|
+
*/
|
|
800
|
+
if (붙인줄들 && 묻는중 === null && 붙임접을까(붙인것, { 폭 })) {
|
|
716
801
|
const 번호 = ++붙임번호;
|
|
717
802
|
붙인것들.set(번호, 붙인것);
|
|
718
803
|
이어쓰기줄들 = 붙임앞줄수 ? (이어쓰기줄들 ?? []).slice(0, 붙임앞줄수) : null;
|
|
@@ -720,13 +805,22 @@ export async function chatLoop(opts = {}) {
|
|
|
720
805
|
const 표 = 붙임표(번호, 붙인것);
|
|
721
806
|
rl.line = 앞머리 + 표;
|
|
722
807
|
rl.cursor = rl.line.length;
|
|
723
|
-
|
|
724
|
-
|
|
808
|
+
if (입력기다림) {
|
|
809
|
+
화면.입력지움();
|
|
810
|
+
say(` ${c.gray(옮긴말('paste.folded', { 표 }))}`);
|
|
811
|
+
} else {
|
|
812
|
+
/*
|
|
813
|
+
* 일하는 중에는 대화에 안 찍는다. 찍으면 이미 보낸 것처럼 보인다
|
|
814
|
+
* (아래 미리 쳐 둔 것 갈래와 같은 까닭이다). 일감 상자에 지금
|
|
815
|
+
* 들고 있는 것으로 그려 주면 그것으로 족하다.
|
|
816
|
+
*/
|
|
817
|
+
화면.대기갱신(펴기(rl.line), queue.length);
|
|
818
|
+
}
|
|
725
819
|
} else if (이어쓰기줄들?.length && 입력기다림) {
|
|
726
820
|
// 접지 않은 덩이는 몇 줄인지라도 알려 준다. 안 보이면 사라진 줄
|
|
727
821
|
// 알고 다시 붙인다.
|
|
728
822
|
화면.입력지움();
|
|
729
|
-
say(` ${c.gray(
|
|
823
|
+
say(` ${c.gray(옮긴말('paste.pasted', { n: 이어쓰기줄들.length + 1 }))}`);
|
|
730
824
|
}
|
|
731
825
|
화면.입력갱신?.(session, 펴기(rl.line), rl.cursor ?? 0);
|
|
732
826
|
return;
|
|
@@ -741,8 +835,25 @@ export async function chatLoop(opts = {}) {
|
|
|
741
835
|
* ESC 는 **멈추기만** 한다. 끝내지 않는다. 그래서 마음 놓고 누를 수 있다.
|
|
742
836
|
* 도는 중이 아닐 때는 아무것도 안 한다 — 입력칸에서 ESC 를 눌렀다고
|
|
743
837
|
* 치던 글이 날아가면 그게 더 놀랍다.
|
|
838
|
+
*
|
|
839
|
+
* ── meta 를 안 본다 ──────────────────────────────────────────────
|
|
840
|
+
*
|
|
841
|
+
* 여기 `!key.meta` 가 붙어 있었다. 그래서 이 갈래는 **한 번도 안
|
|
842
|
+
* 돌았다** — 사람이 ESC 를 눌러도 아무 일이 안 났다.
|
|
843
|
+
*
|
|
844
|
+
* 홑 ESC 는 그 자체가 escape 열쇠의 시작 바이트이기도 해서, Node 의
|
|
845
|
+
* readline 은 뒤에 뭐가 더 올지 500ms 를 기다렸다가 아무것도 안 오면
|
|
846
|
+
* `{ name:'escape', meta:true }` 로 배달한다. 즉 **ESC 는 언제나
|
|
847
|
+
* meta 가 참**이다. 직접 재어 봤다 —
|
|
848
|
+
*
|
|
849
|
+
* \x1b → {"name":"escape","meta":true}
|
|
850
|
+
* \x1b\x1b → {"name":"escape","meta":true}
|
|
851
|
+
*
|
|
852
|
+
* 그러니 meta 를 보고 거르면 ESC 를 통째로 거르는 셈이다. 이름이
|
|
853
|
+
* escape 인 것에 Ctrl·Shift 가 붙는 조합은 터미널이 아예 안 보내므로
|
|
854
|
+
* 나머지 둘만 본다.
|
|
744
855
|
*/
|
|
745
|
-
if (key?.name === 'escape' && !key.ctrl && !key.
|
|
856
|
+
if (key?.name === 'escape' && !key.ctrl && !key.shift) {
|
|
746
857
|
if (turn && !turn.signal.aborted) { turn.abort(); 멈추는중(); return; }
|
|
747
858
|
return;
|
|
748
859
|
}
|
|
@@ -880,12 +991,46 @@ export async function chatLoop(opts = {}) {
|
|
|
880
991
|
});
|
|
881
992
|
}
|
|
882
993
|
|
|
883
|
-
|
|
994
|
+
/*
|
|
995
|
+
* 다음 줄을 기다린다. 멈춤신호를 주면 **기다리다가도 손을 뗀다.**
|
|
996
|
+
*
|
|
997
|
+
* 신호 없이 기다리는 자리(맨 위 입력칸)는 여태 그대로다 — 거기서 ESC 는
|
|
998
|
+
* 아무 일도 안 하는 것이 맞다. 신호를 주는 쪽은 turn 이 도는 동안 사람
|
|
999
|
+
* 답을 기다리는 자리뿐이다.
|
|
1000
|
+
*/
|
|
1001
|
+
const nextLine = (멈춤 = null) => {
|
|
884
1002
|
if (queue.length) return Promise.resolve(queue.shift());
|
|
885
1003
|
if (closed) return Promise.resolve(null);
|
|
886
|
-
|
|
1004
|
+
if (멈춤?.aborted) return Promise.resolve(null);
|
|
1005
|
+
return new Promise((res) => {
|
|
1006
|
+
waiter = res;
|
|
1007
|
+
if (!멈춤) return;
|
|
1008
|
+
/*
|
|
1009
|
+
* 기다리던 자리를 **반드시 비우고** 돌아간다.
|
|
1010
|
+
*
|
|
1011
|
+
* 안 비우면 다음에 사람이 친 줄이 이미 없어진 물음에게 배달되고,
|
|
1012
|
+
* 그 줄은 화면에서 그냥 사라진다 — 「쳤는데 안 먹었다」 가 된다.
|
|
1013
|
+
*/
|
|
1014
|
+
멈춤.addEventListener('abort', () => {
|
|
1015
|
+
if (waiter === res) waiter = null;
|
|
1016
|
+
res(null);
|
|
1017
|
+
}, { once: true });
|
|
1018
|
+
});
|
|
887
1019
|
};
|
|
888
1020
|
|
|
1021
|
+
/*
|
|
1022
|
+
* ── 사람에게 되묻는 자리 ────────────────────────────────────────────────
|
|
1023
|
+
*
|
|
1024
|
+
* 여기도 ESC 로 손을 뗄 수 있어야 한다. 예전에는 이 자리가 **멈춤에서
|
|
1025
|
+
* 빠져 있었다** — 도구가 「실행할까요?」 를 띄운 채 답을 기다리면 ESC 를
|
|
1026
|
+
* 몇 번을 눌러도 아무 일도 안 났다. turn 은 이미 끊긴 상태인데 화면은
|
|
1027
|
+
* 물음을 그대로 들고 서 있으니, 사람은 Ctrl+C 로 손이 가고 두 번 누르면
|
|
1028
|
+
* 대화가 통째로 닫힌다. "ESC 를 눌러도 안 멈춘다" 는 제보의 한 갈래다.
|
|
1029
|
+
*
|
|
1030
|
+
* 멈췄을 때 무엇으로 답한 셈 칠지는 부르는 쪽이 정한다(`멈추면`). 기본값을
|
|
1031
|
+
* `def` 로 하면 안 된다 — confirm 의 def 는 'y' 라, 멈추라고 누른 것이
|
|
1032
|
+
* 승인으로 읽힌다.
|
|
1033
|
+
*/
|
|
889
1034
|
const ask = async (label, o = {}) => {
|
|
890
1035
|
const 앞 = ` ${c.gray('›')} ${label} ${o.def ? c.gray(`[${o.def}] `) : ''}`;
|
|
891
1036
|
화면.붙임(앞);
|
|
@@ -897,8 +1042,15 @@ export async function chatLoop(opts = {}) {
|
|
|
897
1042
|
*/
|
|
898
1043
|
묻는중 = 상자쓰나 ? 앞 : null;
|
|
899
1044
|
이어쓰기줄들 = null; 이어쓴것 = false; // 되묻는 사이에 걸쳐 있던 미완성 이어쓰기는 버린다
|
|
1045
|
+
const 멈춤 = o.signal ?? turn?.signal ?? null;
|
|
900
1046
|
try {
|
|
901
|
-
const a = await nextLine();
|
|
1047
|
+
const a = await nextLine(멈춤);
|
|
1048
|
+
if (멈춤?.aborted) {
|
|
1049
|
+
// 물음이 화면에 걸린 채로 남으면 안 된다. 줄을 끊고 멈췄다고 적는다.
|
|
1050
|
+
say('');
|
|
1051
|
+
say(` ${c.gray(옮긴말('ask.stopped'))}`);
|
|
1052
|
+
return o.멈추면 ?? '';
|
|
1053
|
+
}
|
|
902
1054
|
if (a === null) return o.def ?? '';
|
|
903
1055
|
return a.trim() || o.def || '';
|
|
904
1056
|
} finally { 묻는중 = null; }
|
|
@@ -936,15 +1088,33 @@ export async function chatLoop(opts = {}) {
|
|
|
936
1088
|
for (const f of files) {
|
|
937
1089
|
const 이름 = ctx?.scope ? ctx.scope.show(f.path) : f.path;
|
|
938
1090
|
if (f.missing) {
|
|
939
|
-
say(` ${c.yellow('⚠')} ${c.white(이름)} ${c.gray('
|
|
1091
|
+
say(` ${c.yellow('⚠')} ${c.white(이름)} ${c.gray(옮긴말('run.notMade'))}`);
|
|
940
1092
|
continue;
|
|
941
1093
|
}
|
|
942
1094
|
if (f.dir) continue;
|
|
943
1095
|
const kb = f.bytes >= 1024 ? `${(f.bytes / 1024).toFixed(1)}KB` : `${f.bytes}B`;
|
|
944
|
-
say(` ${c.green('✓')} ${c.white(이름)} ${c.gray(`· ${f.lines
|
|
1096
|
+
say(` ${c.green('✓')} ${c.white(이름)} ${c.gray(`· ${세말('lines', f.lines)} · ${kb}`)}`);
|
|
945
1097
|
}
|
|
946
1098
|
};
|
|
947
1099
|
|
|
1100
|
+
/*
|
|
1101
|
+
* 시킨 것 중 자국이 하나도 안 남은 항목을 적는다.
|
|
1102
|
+
*
|
|
1103
|
+
* 조용히 넘어가면 사람은 다 된 줄 알고 그 위에 다음 것을 쌓는다. 나중에
|
|
1104
|
+
* 발견했을 때는 이미 늦다. 「몇 번 까먹거나 누락되거나」라는 제보가 정확히
|
|
1105
|
+
* 그 모양이었다 — 빠진 것 자체보다, 빠졌다는 말을 안 한 것이 문제다.
|
|
1106
|
+
*
|
|
1107
|
+
* 세 자리(done·limit·stuck)가 같은 글을 내야 한다. 한 군데만 고치면
|
|
1108
|
+
* 「끝났을 때는 알려 주는데 끊겼을 때는 안 알려 주는」 화면이 된다 —
|
|
1109
|
+
* 정작 빠질 확률은 끊겼을 때가 훨씬 높다.
|
|
1110
|
+
*/
|
|
1111
|
+
const 빠진것보이기 = (빠진) => {
|
|
1112
|
+
if (!빠진?.length) return;
|
|
1113
|
+
say('');
|
|
1114
|
+
say(` ${c.yellow('!')} ${c.white(`시킨 것 중 ${빠진.length}가지가 안 다뤄졌습니다`)}`);
|
|
1115
|
+
for (const x of 빠진) say(` ${c.gray(`${x.번호}. ${clip(x.글, 68)}`)}`);
|
|
1116
|
+
};
|
|
1117
|
+
|
|
948
1118
|
const ctx = {
|
|
949
1119
|
scope: makeScope(root),
|
|
950
1120
|
// 도구가 한 번에 돌려줄 양을 이 값에서 뽑는다 (agent/budget.js).
|
|
@@ -955,7 +1125,7 @@ export async function chatLoop(opts = {}) {
|
|
|
955
1125
|
// 적어 둔 허락·금지 규칙 (safety/policy.js). 승인 모드보다 먼저 본다.
|
|
956
1126
|
규칙들: 규칙모으기(cfg),
|
|
957
1127
|
history: new History(root),
|
|
958
|
-
audit: new Audit(root),
|
|
1128
|
+
audit: new Audit(root, { 열쇠들: 열쇠묻기(conn) }),
|
|
959
1129
|
seen: new Set(),
|
|
960
1130
|
// 붙은 MCP 서버. 도구를 부를 때 여기서 찾는다.
|
|
961
1131
|
mcp: mcp붙임.서버들,
|
|
@@ -1099,10 +1269,21 @@ export async function chatLoop(opts = {}) {
|
|
|
1099
1269
|
창제목(제목글('도는중', { 폴더: 알림.폴더 }));
|
|
1100
1270
|
|
|
1101
1271
|
if (!답) return null;
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1272
|
+
/*
|
|
1273
|
+
* 숫자로 골랐으면 그 줄의 글을 그대로 돌려준다 — 모델에게 "2" 만 가면
|
|
1274
|
+
* 무엇을 고른 것인지 모른다.
|
|
1275
|
+
*
|
|
1276
|
+
* 그리고 **고르면서 한마디 덧붙일 수 있다.** 여태는 둘 중 하나였다 —
|
|
1277
|
+
* 숫자만 치면 고른 것이고, 그 밖의 글은 고른 것 없는 자유 답이었다.
|
|
1278
|
+
* 그래서 「2 파일은 나눠서」 라고 치면 2번을 골랐다는 사실이 사라졌다.
|
|
1279
|
+
* 가르는 규칙은 ui/pick.js 머리말에 적어 두었다.
|
|
1280
|
+
*/
|
|
1281
|
+
const 푼것 = 고른것풀기(답, 고를것);
|
|
1282
|
+
const 고른것 = 푼것.글;
|
|
1283
|
+
/*
|
|
1284
|
+
* 되비추는 줄에도 **둘 다** 보인다. 고른 것만 보이면 덧붙인 말이
|
|
1285
|
+
* 갔는지 안 갔는지 알 길이 없어서, 사람은 다음 턴에 그 말을 또 친다.
|
|
1286
|
+
*/
|
|
1106
1287
|
say(` ${c.hgreen('▶')} ${c.gray(clip(고른것, 70))}`);
|
|
1107
1288
|
say('');
|
|
1108
1289
|
return 고른것;
|
|
@@ -1117,7 +1298,8 @@ export async function chatLoop(opts = {}) {
|
|
|
1117
1298
|
*/
|
|
1118
1299
|
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
1119
1300
|
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
1120
|
-
|
|
1301
|
+
// 멈추라고 누른 것은 **거절**이다. def 인 'y' 로 물러나면 ESC 가 승인이 된다.
|
|
1302
|
+
const a = (await ask('실행할까요? (y/n)', { def: 'y', 멈추면: 'n' })).toLowerCase();
|
|
1121
1303
|
창제목(제목글('도는중', { 폴더: 알림.폴더 }));
|
|
1122
1304
|
return a === 'y' || a === 'yes' || a === 'ㅇ';
|
|
1123
1305
|
},
|
|
@@ -1171,7 +1353,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1171
1353
|
if (opts.ctx == null) {
|
|
1172
1354
|
// 이 컴퓨터 안의 서버면 눈 깜짝할 새다. 사내 게이트웨이는 몇 초 걸릴 수 있어
|
|
1173
1355
|
// 무슨 일이 일어나는 중인지 알려 준다 — 멈춘 것처럼 보이면 안 된다.
|
|
1174
|
-
화면.돌리기('
|
|
1356
|
+
화면.돌리기(옮긴말('run.ctxProbe'));
|
|
1175
1357
|
let r = null;
|
|
1176
1358
|
try { r = await probeCtx(conn, { timeout: 6000 }); } catch { /* 못 물어보면 아래에서 처리 */ }
|
|
1177
1359
|
화면.돌림멈춤('');
|
|
@@ -1257,15 +1439,15 @@ export async function chatLoop(opts = {}) {
|
|
|
1257
1439
|
}
|
|
1258
1440
|
// 안 뜬 것은 조용히 빠지면 안 된다. "왜 그 도구가 없지" 를 영영 알 수 없다.
|
|
1259
1441
|
for (const m of mcp붙임.못한것) warn.push(`MCP ${c.white(m.이름)} 을 못 붙였습니다 — ${m.왜}`);
|
|
1260
|
-
if (!conn.tools) warn.push('
|
|
1261
|
-
if (!conn.streaming) warn.push('
|
|
1442
|
+
if (!conn.tools) warn.push(옮긴말('run.noTools'));
|
|
1443
|
+
if (!conn.streaming) warn.push(옮긴말('run.noStream'));
|
|
1262
1444
|
warn.push(...길이경고);
|
|
1263
1445
|
// 잘 된 것은 경고 표시를 달지 않는다. ⚠ 가 붙으면 뭘 고쳐야 하나 싶어진다.
|
|
1264
1446
|
for (const l of 길이알림) say(` ${mark.ok} ${c.gray(l)}`);
|
|
1265
1447
|
for (const w of warn) say(` ${mark.warn} ${c.gray(w)}`);
|
|
1266
1448
|
// ESC 를 앞에 둔다 — 멈추는 길로는 이것이 먼저다. Ctrl+C 는 두 번 누르면
|
|
1267
1449
|
// 끝나 버려서, 급히 멈추려던 사람이 대화를 통째로 닫는 일이 실제로 있었다.
|
|
1268
|
-
say(` ${c.gray('
|
|
1450
|
+
say(` ${c.gray(옮긴말('run.hintHelp'))} ${c.gray(옮긴말('run.hintEsc'))} ${c.gray(옮긴말('run.hintQuit'))}`);
|
|
1269
1451
|
|
|
1270
1452
|
/*
|
|
1271
1453
|
* ── 바깥으로 나가도 되는지 한 번 묻는다 ─────────────────────────────
|
|
@@ -1606,7 +1788,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1606
1788
|
break;
|
|
1607
1789
|
|
|
1608
1790
|
case 'waiting':
|
|
1609
|
-
화면.기다림(c.gray(꼬리표(stage) + '
|
|
1791
|
+
화면.기다림(c.gray(꼬리표(stage) + 옮긴말('run.thinking')));
|
|
1610
1792
|
// 지워야 할 줄이 화면에 있다고 표시해 둔다.
|
|
1611
1793
|
//
|
|
1612
1794
|
// 전에는 이 표시를 안 세웠다. \r 로 커서만 앞으로 보내 놓고 지우지는
|
|
@@ -1617,7 +1799,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1617
1799
|
|
|
1618
1800
|
case 'thinking':
|
|
1619
1801
|
thinkChars += ev.text.length;
|
|
1620
|
-
화면.생각(`${mark.think} ${c.gray(꼬리표(stage))}${c.gray(
|
|
1802
|
+
화면.생각(`${mark.think} ${c.gray(꼬리표(stage))}${c.gray(옮긴말('run.thinkingChars', { n: thinkChars.toLocaleString() }))}`);
|
|
1621
1803
|
if (process.stdout.isTTY) thinkingShown = true;
|
|
1622
1804
|
break;
|
|
1623
1805
|
|
|
@@ -1898,6 +2080,30 @@ export async function chatLoop(opts = {}) {
|
|
|
1898
2080
|
break;
|
|
1899
2081
|
}
|
|
1900
2082
|
|
|
2083
|
+
/*
|
|
2084
|
+
* 자리가 다 차서 기억을 비웠다. **턴은 안 죽었다.**
|
|
2085
|
+
*
|
|
2086
|
+
* 여기 붉은 줄을 찍으면 사람은 하던 일이 엎어진 줄 알고 /clear 를
|
|
2087
|
+
* 치고 시킨 말을 다시 타이핑한다 — 정작 지금은 시킨 말과 남은 할 일을
|
|
2088
|
+
* 들고 그대로 이어 가는 중이다. 접기(compacted)와 같은 자리·같은
|
|
2089
|
+
* 빛깔로 한 줄만 적는다.
|
|
2090
|
+
*/
|
|
2091
|
+
case 'reset': {
|
|
2092
|
+
접기멈춤();
|
|
2093
|
+
clearThinking();
|
|
2094
|
+
const 할일수 = ev.kept?.할일 ?? 0;
|
|
2095
|
+
say(` ${c.cyan('◱')} ${c.gray(할일수
|
|
2096
|
+
? 옮긴말('ev.reset', { 버린수: ev.dropped, 할일: 할일수 })
|
|
2097
|
+
: 옮긴말('ev.resetBare', { 버린수: ev.dropped }))}`);
|
|
2098
|
+
/*
|
|
2099
|
+
* 비우면 이력이 통째로 바뀐다. 덧붙이기로는 못 맞춘다.
|
|
2100
|
+
* 여기서 안 적으면, 비운 뒤에 죽었을 때 /resume 이 옛 대화를
|
|
2101
|
+
* 통째로 되살려 놓는다 — 자리가 없어서 비운 것이 도로 차 있다.
|
|
2102
|
+
*/
|
|
2103
|
+
ctx.갈래.현재store().replace(session.messages, `자리부족 — 앞선 대화 ${ev.dropped}개를 비움`);
|
|
2104
|
+
saved = session.messages.length;
|
|
2105
|
+
break;
|
|
2106
|
+
}
|
|
1901
2107
|
// 서버가 거절하면서 알려 준 한계를 받아 적었다. 실패로 보이면 안 된다 —
|
|
1902
2108
|
// 사용자 눈에는 잠깐 멈췄다가 그냥 잘 되는 것으로 보여야 맞다.
|
|
1903
2109
|
case 'learned':
|
|
@@ -1919,13 +2125,38 @@ export async function chatLoop(opts = {}) {
|
|
|
1919
2125
|
* 대충 답한 줄 알고 같은 것을 다시 시키고, 같은 자리에서 또 잘린다.
|
|
1920
2126
|
* 손댈 자리를 그 자리에서 알려 준다.
|
|
1921
2127
|
*/
|
|
2128
|
+
/*
|
|
2129
|
+
* ── 붙잡아 둔 반 줄을 먼저 비운다 ────────────────────────────
|
|
2130
|
+
*
|
|
2131
|
+
* 표를 그리려면 줄이 다 모여야 해서, 마크다운 그림은 마지막
|
|
2132
|
+
* 몇 줄을 손에 쥐고 있다. `done` 은 그걸 비우고 끝나는데
|
|
2133
|
+
* capped·cutoff·error 는 안 비우고 있었다.
|
|
2134
|
+
*
|
|
2135
|
+
* 그러면 두 가지가 난다. 붙잡힌 글이 **화면에서 그냥 사라지거나**
|
|
2136
|
+
* (오류로 끝나면 뒤에 done 이 없다), 무엇이 잘렸는지 보라고 띄운
|
|
2137
|
+
* 경고가 정작 그 답의 마지막 줄보다 **위에** 찍힌다.
|
|
2138
|
+
*/
|
|
1922
2139
|
case 'capped':
|
|
1923
2140
|
clearThinking();
|
|
2141
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1924
2142
|
say(` ${mark.warn} ${c.gray(`답이 ${c.white(ev.cap.toLocaleString())} 토큰에서 잘렸습니다`)}`
|
|
1925
2143
|
+ `${ev.정한값 ? c.gray(' (직접 정하신 상한입니다)') : c.gray(' — 더 못 올리는 천장입니다')}`);
|
|
1926
2144
|
say(` ${c.gray('한 번에 더 길게 받으려면')} ${c.cyan('/out 32k')}${c.gray('. 파일을 쓰는 중이었다면 Append 로 나눠 쓰게 하세요.')}`);
|
|
1927
2145
|
break;
|
|
1928
2146
|
|
|
2147
|
+
/*
|
|
2148
|
+
* 상한에서 잘린 것이 아니라, 서버가 **끝난 까닭을 한 번도 안 주고**
|
|
2149
|
+
* 흘려보내기를 멈춘 것이다. 여태 이건 정상 종료와 구별이 안 돼서
|
|
2150
|
+
* 중간에서 끊긴 답이 온전한 답인 척 지나갔다.
|
|
2151
|
+
* /out 을 권하면 안 된다 — 상한 얘기가 아니라 연결 얘기다.
|
|
2152
|
+
*/
|
|
2153
|
+
case 'cutoff':
|
|
2154
|
+
clearThinking();
|
|
2155
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
2156
|
+
say(` ${mark.warn} ${c.gray('서버가 끝났다는 말 없이 답을 멈췄습니다 — 중간에서 끊겼을 수 있습니다.')}`);
|
|
2157
|
+
say(` ${c.gray('중계 프록시·게이트웨이를 거치면 나는 일입니다. 같은 것을 다시 물어 보세요.')}`);
|
|
2158
|
+
break;
|
|
2159
|
+
|
|
1929
2160
|
case 'compact_failed':
|
|
1930
2161
|
접기멈춤();
|
|
1931
2162
|
say(` ${c.gray(`(접지 못했습니다: ${ev.why})`)}`);
|
|
@@ -1946,6 +2177,9 @@ export async function chatLoop(opts = {}) {
|
|
|
1946
2177
|
if (ev.남은할일.length > 8) say(` ${c.gray(`… 그 밖에 ${ev.남은할일.length - 8}개`)}`);
|
|
1947
2178
|
}
|
|
1948
2179
|
만든파일보이기(ev.files);
|
|
2180
|
+
// 할 일 목록에 못 들어간 채로 빠진 것도 같이 적는다 — 목록에
|
|
2181
|
+
// 없으면 「안 끝난 것」 에도 안 뜨므로 여기서 말 안 하면 아무 데도 없다.
|
|
2182
|
+
빠진것보이기(ev.빠진);
|
|
1949
2183
|
break;
|
|
1950
2184
|
|
|
1951
2185
|
// 같은 자리를 계속 반복하고 있다. 두면 컨텍스트만 차고 아무것도 안 나온다.
|
|
@@ -1969,6 +2203,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1969
2203
|
// 멈췄어도 여기까지 만든 것은 있다. 그것부터 알려 준다 —
|
|
1970
2204
|
// 없는 줄 알고 다시 시키면 앞서 만든 것을 덮어쓴다.
|
|
1971
2205
|
만든파일보이기(ev.files);
|
|
2206
|
+
빠진것보이기(ev.빠진);
|
|
1972
2207
|
break;
|
|
1973
2208
|
|
|
1974
2209
|
case 'aborted':
|
|
@@ -1987,8 +2222,17 @@ export async function chatLoop(opts = {}) {
|
|
|
1987
2222
|
case 'error':
|
|
1988
2223
|
턴탈났나 = true;
|
|
1989
2224
|
clearThinking();
|
|
2225
|
+
// 붙잡아 둔 반 줄을 먼저 비운다 — 까닭은 위 capped 자리에 적어 두었다.
|
|
2226
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1990
2227
|
say('');
|
|
1991
2228
|
오류보이기(ev.text);
|
|
2229
|
+
/*
|
|
2230
|
+
* 반쪽이라도 남겼으면 그렇다고 말해 준다.
|
|
2231
|
+
*
|
|
2232
|
+
* 안 말하면 사람은 화면에 보이는 200자가 대화에도 들어갔는지를
|
|
2233
|
+
* 모른다. 모르면 "이어서 해줘" 를 못 치고 처음부터 다시 친다.
|
|
2234
|
+
*/
|
|
2235
|
+
if (ev.kept) say(` ${c.gray(옮긴말('run.errKept'))}`);
|
|
1992
2236
|
break;
|
|
1993
2237
|
|
|
1994
2238
|
/*
|
|
@@ -2018,11 +2262,7 @@ export async function chatLoop(opts = {}) {
|
|
|
2018
2262
|
* 제보가 정확히 그 모양이었다 — 빠진 것 자체보다, 빠졌다는 말을
|
|
2019
2263
|
* 안 한 것이 문제다.
|
|
2020
2264
|
*/
|
|
2021
|
-
|
|
2022
|
-
say('');
|
|
2023
|
-
say(` ${c.yellow('!')} ${c.white(`시킨 것 중 ${ev.빠진.length}가지가 안 다뤄졌습니다`)}`);
|
|
2024
|
-
for (const x of ev.빠진) say(` ${c.gray(`${x.번호}. ${clip(x.글, 68)}`)}`);
|
|
2025
|
-
}
|
|
2265
|
+
빠진것보이기(ev.빠진);
|
|
2026
2266
|
break;
|
|
2027
2267
|
}
|
|
2028
2268
|
}
|
|
@@ -2060,11 +2300,15 @@ export async function chatLoop(opts = {}) {
|
|
|
2060
2300
|
turn = null;
|
|
2061
2301
|
interrupted = false; // 중단은 '끝내기' 의사가 아니다. 종료 카운트를 되돌린다.
|
|
2062
2302
|
flush(); // 오류로 끝났어도 여기까지는 남긴다
|
|
2303
|
+
// 대화가 파일에 안 적히고 있으면 여기서 **한 번** 말한다 (agent/store.js).
|
|
2304
|
+
// 여태는 아무 말이 없어서, 사람이 알아차리는 자리가 다음 날 --resume 이었다.
|
|
2305
|
+
const 저장샘 = ctx.갈래?.현재store?.()?.처음못쓴것?.();
|
|
2306
|
+
if (저장샘) say(` ${mark.warn} ${c.yellow(옮긴말('store.notSaving', { n: 저장샘.수, 까닭: 저장샘.까닭 }))}`);
|
|
2063
2307
|
|
|
2064
2308
|
// ── 꼬리말 — 이번 턴만의 숫자 ────────────────────────────────────────
|
|
2065
2309
|
const secs = ((Date.now() - started) / 1000).toFixed(1);
|
|
2066
|
-
const bits = [
|
|
2067
|
-
if (tools) bits.push(
|
|
2310
|
+
const bits = [옮긴말('unit.sec', { n: secs })];
|
|
2311
|
+
if (tools) bits.push(`${옮긴말('scr.tools')} ${옮긴말('unit.calls', { n: tools })}`);
|
|
2068
2312
|
const dIn = session.usage.in - before.in;
|
|
2069
2313
|
const dOut = session.usage.out - before.out;
|
|
2070
2314
|
if (dIn || dOut) bits.push(`↑${dIn.toLocaleString()} ↓${dOut.toLocaleString()}`);
|
|
@@ -2101,14 +2345,29 @@ export async function chatLoop(opts = {}) {
|
|
|
2101
2345
|
if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
|
|
2102
2346
|
창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
|
|
2103
2347
|
const 답 = String(await ask(
|
|
2104
|
-
|
|
2348
|
+
`${옮긴말('plan.ask')} ${c.gray(옮긴말('plan.hint'))}`,
|
|
2105
2349
|
{ def: 'y' },
|
|
2106
2350
|
)).trim();
|
|
2107
|
-
|
|
2351
|
+
/*
|
|
2352
|
+
* ── 골라진 것 + 하고 싶은 말 ────────────────────────────────────
|
|
2353
|
+
*
|
|
2354
|
+
* 여태는 셋 중 하나였다. ⏎ 면 진행, n 이면 그만, 그 밖의 글은 통째로
|
|
2355
|
+
* 「이걸 고쳐서 다시 내라」. 그래서 「그대로 하되 검사도 넣어줘」 라고
|
|
2356
|
+
* 치면 **계획을 다시 짜러 갔다** — 사람은 진행하라고 한 것인데.
|
|
2357
|
+
* 반대로 「3단계는 빼자」 도 그냥 글 한 줄로 갔다. 어느 단계 얘긴지
|
|
2358
|
+
* 모델이 다시 세어 봐야 한다.
|
|
2359
|
+
*
|
|
2360
|
+
* 이제 넷이다. 가르는 규칙은 ui/pick.js 의 계획답풀기() 에 있다.
|
|
2361
|
+
* ⏎ · y → 그대로 진행
|
|
2362
|
+
* y 검사도 넣어줘 → 진행하되 그 말을 얹어서
|
|
2363
|
+
* 3 이건 빼자 → 3단계에 대고 하는 말로 붙여 다시 짜라
|
|
2364
|
+
* 그 밖의 글 → 여태 그대로 (다시 짜라)
|
|
2365
|
+
*/
|
|
2366
|
+
const 계획답 = 계획답풀기(답, 할일.length);
|
|
2108
2367
|
|
|
2109
|
-
if (
|
|
2110
|
-
say(` ${c.gray('
|
|
2111
|
-
} else if (
|
|
2368
|
+
if (계획답.갈래 === '그만') {
|
|
2369
|
+
say(` ${c.gray(옮긴말('plan.cancelled'))}`);
|
|
2370
|
+
} else if (계획답.갈래 === '진행') {
|
|
2112
2371
|
/*
|
|
2113
2372
|
* 승인한 계획을 **글자 그대로 실어 보낸다.**
|
|
2114
2373
|
*
|
|
@@ -2128,16 +2387,33 @@ export async function chatLoop(opts = {}) {
|
|
|
2128
2387
|
: '바로 위에 네가 적은 계획을 사람이 승인했다.'
|
|
2129
2388
|
+ ' 그 계획을 다시 적지 말고 지금부터 그대로 해라.')
|
|
2130
2389
|
+ ' 계획을 다시 적지 마라. **지난 대화를 뒤지지 마라** — 필요한 것은'
|
|
2131
|
-
+ ' 이 대화 안에 다 있다. 다 끝나면 무엇을 만들었는지만 짧게 알려라.'
|
|
2390
|
+
+ ' 이 대화 안에 다 있다. 다 끝나면 무엇을 만들었는지만 짧게 알려라.'
|
|
2391
|
+
/*
|
|
2392
|
+
* 승인하며 덧붙인 말은 **맨 뒤에** 붙인다.
|
|
2393
|
+
*
|
|
2394
|
+
* 계획 다음에 오는 말이라 「계획은 그대로, 다만 이것」 으로 읽힌다.
|
|
2395
|
+
* 앞에 붙이면 계획보다 먼저 읽혀서, 작은 모델은 그 한마디만
|
|
2396
|
+
* 하고 계획은 안 한다.
|
|
2397
|
+
*/
|
|
2398
|
+
+ (계획답.덧말 ? `\n\n사람이 승인하며 덧붙인 말: ${계획답.덧말}` : '');
|
|
2132
2399
|
이어갈모드 = 'code';
|
|
2133
2400
|
say('');
|
|
2134
|
-
say(` ${c.hgreen('▶')} ${c.gray(
|
|
2401
|
+
say(` ${c.hgreen('▶')} ${c.gray(계획답.덧말
|
|
2402
|
+
? 옮긴말('plan.goingWith', { 말: clip(계획답.덧말, 50) })
|
|
2403
|
+
: 옮긴말('plan.going'))}`);
|
|
2135
2404
|
} else {
|
|
2136
2405
|
// 'e' 를 따로 두지 않는다 — 고칠 점을 바로 치는 것이 한 걸음 짧다.
|
|
2137
|
-
|
|
2406
|
+
// 단계 번호를 댔으면 그 단계에 대고 하는 말로 적어 준다. 모델이
|
|
2407
|
+
// 어느 단계 얘긴지 다시 세어 볼 까닭이 없다.
|
|
2408
|
+
const 고칠말 = 계획답.단계
|
|
2409
|
+
? `${계획답.단계}단계에 대해: ${계획답.덧말}`
|
|
2410
|
+
: 계획답.덧말;
|
|
2411
|
+
이어갈것 = `계획에서 이걸 고쳐서 **계획만** 다시 내라 (아직 실행하지 마라): ${고칠말}`;
|
|
2138
2412
|
이어갈모드 = 'plan';
|
|
2139
2413
|
say('');
|
|
2140
|
-
say(` ${c.hcyan('☰')} ${c.gray(
|
|
2414
|
+
say(` ${c.hcyan('☰')} ${c.gray(계획답.단계
|
|
2415
|
+
? 옮긴말('plan.replanStep', { 단계: 계획답.단계 })
|
|
2416
|
+
: 옮긴말('plan.replan'))}`);
|
|
2141
2417
|
}
|
|
2142
2418
|
} else if (끊긴할일) {
|
|
2143
2419
|
/*
|
|
@@ -2212,7 +2488,12 @@ export async function chatLoop(opts = {}) {
|
|
|
2212
2488
|
// 끝맺음은 화면을 접기 **전에** 그린다. close() 가 상자를 걷어내므로,
|
|
2213
2489
|
// 그 뒤에 찍으면 걷어낸 자리에 뜬금없이 한 줄이 남는다.
|
|
2214
2490
|
say('');
|
|
2215
|
-
say(` ${c.gray('
|
|
2491
|
+
say(` ${c.gray(옮긴말('run.bye'))} ${c.gray(옮긴말('run.byeStats', {
|
|
2492
|
+
n: session.usage.calls,
|
|
2493
|
+
초: (session.usage.ms / 1000).toFixed(1),
|
|
2494
|
+
입력: session.usage.in.toLocaleString(),
|
|
2495
|
+
출력: session.usage.out.toLocaleString(),
|
|
2496
|
+
}))}`);
|
|
2216
2497
|
say('');
|
|
2217
2498
|
화면.close();
|
|
2218
2499
|
return 0;
|
|
@@ -2227,6 +2508,8 @@ export async function chatLoop(opts = {}) {
|
|
|
2227
2508
|
*/
|
|
2228
2509
|
function stageTag(ev, level) {
|
|
2229
2510
|
if (!ev || level === '쉬움') return '';
|
|
2230
|
-
|
|
2511
|
+
// 아는 단계만 표를 거친다. 모르는 것에 말() 을 태우면 못 찾은 열쇠가
|
|
2512
|
+
// 그대로 화면에 찍혀 `stage.xyz 생각 중…` 이 된다.
|
|
2513
|
+
const label = STAGES[ev.stage] ? 옮긴말(`stage.${ev.stage}`) : ev.stage;
|
|
2231
2514
|
return `${label}·${ev.level}`;
|
|
2232
2515
|
}
|