deel-local-cli 1.0.0 → 1.0.1
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 +43 -43
- package/README.md +45 -43
- package/bin/deel.js +4 -4
- package/package.json +1 -1
- package/src/repl.js +101 -29
- package/src/ui/inputbox.js +184 -0
- package/src/ui/screen.js +73 -28
- package/src/ui/wrap.js +68 -0
- package/src/ui/tui.js +0 -419
package/README.en.md
CHANGED
|
@@ -394,51 +394,51 @@ Two things matter here:
|
|
|
394
394
|
- **Beginners do not get fewer safeguards.** Undo, workspace scope and dangerous-command
|
|
395
395
|
blocking are identical. A beginner needs the undo more, not less.
|
|
396
396
|
|
|
397
|
-
###
|
|
398
|
-
|
|
399
|
-
Launched in a terminal,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
```
|
|
404
|
-
┌ 대화 ─────────────────────────────────────────────┬ 바뀐 파일 ───────────────┐
|
|
405
|
-
│ ❊ Grep(console.log) │ src/runner.js +3-1 │
|
|
406
|
-
│ └ 3 files · 11 hits │ src/index.js +1-1 │
|
|
407
|
-
│ │ src/ui/log.js +12-0 │
|
|
408
|
-
│ ◈ Edit(src/runner.js) ├ 할 일 ───────────────────│
|
|
409
|
-
│ └ 1 spot +3-1 │ ☑ find log calls │
|
|
410
|
-
│ - 12 console.log('시작', 이름) │ ☑ unify runner.js │
|
|
411
|
-
│ + 12 logger.info({ 단계: '시작', 이름 }) │ ▶ unify index.js │
|
|
412
|
-
│ │ ☐ update docs │
|
|
413
|
-
│ ▌ Unified log calls to the logger format. │ │
|
|
414
|
-
│ │ │
|
|
415
|
-
│ ── 4.2s · 3 tools · ↑3,900 ↓180 │ │
|
|
416
|
-
└───────────────────────────────────────────────────┴──────────────────────────┘
|
|
417
|
-
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱ 22% ▏ ◎ 종합 · ◇ medium · auto ▏ ↑3.8k ↓180
|
|
418
|
-
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
419
|
-
│ ❯ │
|
|
420
|
-
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
397
|
+
### The input box
|
|
398
|
+
|
|
399
|
+
Launched in a terminal, **the conversation scrolls normally and an input box is pinned at
|
|
400
|
+
the bottom.** Only the box is erased and redrawn — nothing above it is touched.
|
|
401
|
+
|
|
421
402
|
```
|
|
403
|
+
❊ Grep(console.log)
|
|
404
|
+
└ 3 files · 11 hits
|
|
405
|
+
◈ Edit(src/runner.js)
|
|
406
|
+
└ 1 spot +3-1
|
|
407
|
+
- 12 console.log('시작', 이름)
|
|
408
|
+
+ 12 logger.info({ 단계: '시작', 이름 })
|
|
422
409
|
|
|
423
|
-
|
|
424
|
-
`TERM=dumb`, or a window under 60×16 all fall back to the **scrolling view** without asking —
|
|
425
|
-
`deel … | tee log.txt` must not become a pile of escape codes. Passing `--tui` does not
|
|
426
|
-
override a pipe.
|
|
410
|
+
▌ Unified log calls to the logger format. One change in runner.js.
|
|
427
411
|
|
|
428
|
-
|
|
429
|
-
|---|---|---|
|
|
430
|
-
| When | Launched in a terminal | Pipes, CI, small windows, `--no-tui` |
|
|
431
|
-
| Files changed / todos | Always visible on the right (≥96 cols) | Printed inline as they happen |
|
|
432
|
-
| Earlier output | Wrapped and paged inside the frame | Your terminal's own scrollback |
|
|
433
|
-
| On exit | **Replays the conversation into scrollback** | Already there |
|
|
434
|
-
| For keeping a log | Not this one | This one |
|
|
412
|
+
── 4.2s · 3 tools · ↑3,900 ↓180
|
|
435
413
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
414
|
+
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱ 22% ▏ ◎ 종합 · ◇ medium · auto
|
|
415
|
+
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
416
|
+
│ ❯ also shrink the aggregate helpers │
|
|
417
|
+
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Terminal scrollback, selection and `Ctrl+F` all keep working, because the conversation is
|
|
421
|
+
never trapped inside a pane of ours. Long input grows the box to as many lines as it needs.
|
|
439
422
|
|
|
440
|
-
|
|
441
|
-
|
|
423
|
+
**It switches itself off where it would do harm.** Piped or redirected output, `CI` set,
|
|
424
|
+
`TERM=dumb`, or a window under 40 columns: no box, no asking. `deel … | tee log.txt` must
|
|
425
|
+
not become a pile of escape codes. Passing `--tui` does not override a pipe, and `--no-tui`
|
|
426
|
+
turns it off at any time.
|
|
427
|
+
|
|
428
|
+
Line editing stays entirely with Node's readline — Korean IME composition, paste, history,
|
|
429
|
+
Ctrl+A/E, backspace. We only *draw* the string readline is holding. Hand-rolling a line
|
|
430
|
+
editor is how you break IME input first.
|
|
431
|
+
|
|
432
|
+
> **A path taken and abandoned** — the first version borrowed the whole terminal (an
|
|
433
|
+
> alternate screen, like vim) and split it into conversation, changed-files and todo panes.
|
|
434
|
+
> It looked the part, and **every slash command went dead.** Six modules including
|
|
435
|
+
> `commands.js` write straight to the terminal rather than through the screen object, and a
|
|
436
|
+
> full repaint erased their output the instant it appeared. Not "the command didn't run" —
|
|
437
|
+
> "you can't see that it ran", which is worse. Fixing it would mean threading every one of
|
|
438
|
+
> those six through the screen object, plus every one added later, with a silent return of
|
|
439
|
+
> the same symptom if one is missed. So the design went the other way: let the conversation
|
|
440
|
+
> flow, manage only the box. ([`test/box.test.js`](test/box.test.js) spawns a child that
|
|
441
|
+
> pretends to be a terminal, so this one cannot ship again.)
|
|
442
442
|
|
|
443
443
|
---
|
|
444
444
|
|
|
@@ -1195,7 +1195,7 @@ deel --effort <profile> even / save (default) / deep
|
|
|
1195
1195
|
deel --offline Nothing leaves this machine
|
|
1196
1196
|
deel --continue Resume the most recent conversation
|
|
1197
1197
|
deel --resume <id> Resume a specific one
|
|
1198
|
-
deel --no-tui Turn the
|
|
1198
|
+
deel --no-tui Turn the input box off; plain scrolling view (see below)
|
|
1199
1199
|
```
|
|
1200
1200
|
|
|
1201
1201
|
### Project rules
|
|
@@ -1226,7 +1226,7 @@ If the working folder has `DEEL.md`, `CLAUDE.md` or `AGENTS.md`, it is loaded as
|
|
|
1226
1226
|
## Development
|
|
1227
1227
|
|
|
1228
1228
|
```bash
|
|
1229
|
-
npm test Full suite (1,
|
|
1229
|
+
npm test Full suite (1,787 checks)
|
|
1230
1230
|
npm run coverage Which lines the tests actually execute
|
|
1231
1231
|
npm run verify Import + network checks only
|
|
1232
1232
|
npm run bench Edit success rate
|
|
@@ -1280,7 +1280,7 @@ Zero dependencies rules out c8 and nyc, so this reads Node's own
|
|
|
1280
1280
|
`NODE_V8_COVERAGE` instead — nothing new to get through an import review. It picks up
|
|
1281
1281
|
child processes too, so the `cli` suite that spawns `deel` counts like everything else.
|
|
1282
1282
|
|
|
1283
|
-
Currently **92% overall** (6,
|
|
1283
|
+
Currently **92% overall** (6,911 of 7,496 lines). Three files are deliberately left short.
|
|
1284
1284
|
|
|
1285
1285
|
| File | Now | Why it stops there |
|
|
1286
1286
|
|---|---|---|
|
package/README.md
CHANGED
|
@@ -401,50 +401,51 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
|
|
|
401
401
|
- **초보라고 승인을 덜 받지 않습니다.** 되돌리기·작업 범위·위험 명령 차단은 두 수준이 같습니다.
|
|
402
402
|
초보일수록 되돌릴 수 있어야 합니다.
|
|
403
403
|
|
|
404
|
-
###
|
|
405
|
-
|
|
406
|
-
터미널에서 그냥 켜면
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
└───────────────────────────────────────────────────┴──────────────────────────┘
|
|
423
|
-
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱ 22% ▏ ◎ 종합 · ◇ medium · auto ▏ ↑3.8k ↓180
|
|
404
|
+
### 입력칸
|
|
405
|
+
|
|
406
|
+
터미널에서 그냥 켜면 **대화는 위로 흘러가고, 맨 아래에 입력 상자가 붙습니다.**
|
|
407
|
+
상자만 우리가 지우고 다시 그립니다 — 위쪽 대화는 손대지 않습니다.
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
❊ Grep(console.log)
|
|
411
|
+
└ 3개 파일 · 11건
|
|
412
|
+
◈ Edit(src/runner.js)
|
|
413
|
+
└ 1군데 +3-1
|
|
414
|
+
- 12 console.log('시작', 이름)
|
|
415
|
+
+ 12 logger.info({ 단계: '시작', 이름 })
|
|
416
|
+
|
|
417
|
+
▌ 로그 호출을 logger 형식으로 통일했습니다. runner.js 한 군데입니다.
|
|
418
|
+
|
|
419
|
+
── 4.2초 · 도구 3회 · ↑3,900 ↓180
|
|
420
|
+
|
|
421
|
+
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱ 22% ▏ ◎ 종합 · ◇ medium · auto
|
|
424
422
|
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
425
|
-
│ ❯
|
|
423
|
+
│ ❯ 집계 함수도 줄여줘 │
|
|
426
424
|
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
427
425
|
```
|
|
428
426
|
|
|
427
|
+
터미널 스크롤·복사·`Ctrl+F` 찾기가 **그대로 됩니다.** 대화를 우리 칸에 가둬 두지
|
|
428
|
+
않기 때문입니다. 긴 글을 치면 상자가 알아서 여러 줄로 늘어납니다.
|
|
429
|
+
|
|
429
430
|
**저절로 꺼지는 자리가 있습니다.** 파이프·리다이렉트로 넘길 때, `CI` 가 켜져 있을 때,
|
|
430
|
-
`TERM=dumb` 일 때, 창이
|
|
431
|
+
`TERM=dumb` 일 때, 창이 40칸보다 좁을 때는 묻지 않고 상자를 안 그립니다.
|
|
431
432
|
`deel … | tee 기록.txt` 가 제어문자 덩어리가 되면 안 되기 때문입니다.
|
|
432
|
-
`--tui` 를 줘도 파이프면 안 켭니다.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
433
|
+
`--tui` 를 줘도 파이프면 안 켭니다. `--no-tui` 로 언제든 끌 수 있습니다.
|
|
434
|
+
|
|
435
|
+
줄 편집은 전부 Node 의 readline 이 그대로 맡습니다 — 한글 조합, 붙여넣기,
|
|
436
|
+
위아래 이력, Ctrl+A/E, 백스페이스. 우리는 readline 이 들고 있는 글을 상자 안에
|
|
437
|
+
**그리기만** 합니다. 직접 키를 받아 줄 편집을 짜기 시작하면 한글 입력기부터 깨집니다.
|
|
438
|
+
|
|
439
|
+
> **한 번 틀렸던 길** — 처음에는 터미널을 통째로 빌려(vim 처럼 딴 화면) 대화 칸·
|
|
440
|
+
> 파일 칸·할 일 칸을 나눠 그렸습니다. 보기에는 그럴듯했는데 **슬래시 명령이 전부
|
|
441
|
+
> 먹통**이 됐습니다. `commands.js` 를 비롯한 여섯 모듈이 화면 객체를 안 거치고
|
|
442
|
+
> 터미널에 바로 쓰는데, 매번 화면을 통째로 다시 그리니 그 글이 찍히자마자 덮여
|
|
443
|
+
> 사라졌던 것입니다. 명령이 안 도는 게 아니라 **결과가 안 보이는** 것이라 더
|
|
444
|
+
> 나빴습니다. 고치려면 터미널에 쓰는 자리를 전부 화면 객체로 꿰야 하는데, 지금
|
|
445
|
+
> 여섯 곳이고 앞으로 늘 것이며, 하나라도 빠뜨리면 같은 증상이 조용히 돌아옵니다.
|
|
446
|
+
> 그래서 반대로 갔습니다 — 대화는 그냥 흘려보내고 상자만 관리합니다.
|
|
447
|
+
> ([`test/box.test.js`](test/box.test.js) 가 터미널인 척하는 자식을 띄워
|
|
448
|
+
> 이 결함이 다시 안 나가는지 봅니다.)
|
|
448
449
|
|
|
449
450
|
---
|
|
450
451
|
|
|
@@ -1216,7 +1217,7 @@ deel --effort <배분> even / save(기본) / deep
|
|
|
1216
1217
|
deel --offline 이 컴퓨터 밖으로 아무것도 안 보냄
|
|
1217
1218
|
deel --continue 가장 최근 대화 이어하기
|
|
1218
1219
|
deel --resume <id> 골라서 이어하기
|
|
1219
|
-
deel --no-tui
|
|
1220
|
+
deel --no-tui 입력 상자를 끄고 줄 화면으로 (아래 참고)
|
|
1220
1221
|
```
|
|
1221
1222
|
|
|
1222
1223
|
### 프로젝트 규칙
|
|
@@ -1247,7 +1248,7 @@ deel --no-tui 전체화면을 끄고 줄 화면으로 (아래 참고)
|
|
|
1247
1248
|
## 개발
|
|
1248
1249
|
|
|
1249
1250
|
```bash
|
|
1250
|
-
npm test 전체 검증 (1,
|
|
1251
|
+
npm test 전체 검증 (1,787항목)
|
|
1251
1252
|
npm run coverage 검사가 소스의 어디를 밟았는지
|
|
1252
1253
|
npm run verify 반입·통신 검증만
|
|
1253
1254
|
npm run bench 편집 성공률 측정
|
|
@@ -1314,7 +1315,7 @@ node test/coverage.mjs --json 기계가 읽을 형태로
|
|
|
1314
1315
|
`NODE_V8_COVERAGE` 를 읽습니다 — 새로 반입 심사할 것이 하나도 안 늡니다.
|
|
1315
1316
|
자식 프로세스까지 잡히므로 `deel` 을 띄워 보는 `cli` 검사도 그대로 집계됩니다.
|
|
1316
1317
|
|
|
1317
|
-
지금 **전체 92%** (7,
|
|
1318
|
+
지금 **전체 92%** (7,496줄 중 6,911줄). 일부러 못 채운 곳이 셋 있습니다.
|
|
1318
1319
|
|
|
1319
1320
|
| 파일 | 지금 | 왜 못 채우나 |
|
|
1320
1321
|
|---|---|---|
|
|
@@ -1328,8 +1329,9 @@ node test/coverage.mjs --json 기계가 읽을 형태로
|
|
|
1328
1329
|
bin/deel.js 진입점
|
|
1329
1330
|
src/
|
|
1330
1331
|
ui/ 색·한글 폭·상자·상태줄·입력
|
|
1331
|
-
ui/screen.js 화면 고르기 (줄 화면 /
|
|
1332
|
-
ui/
|
|
1332
|
+
ui/screen.js 화면 고르기 (줄 화면 / 상자 화면)
|
|
1333
|
+
ui/inputbox.js 맨 아래 입력 상자 — 그리기·지우기·커서 자리
|
|
1334
|
+
ui/wrap.js 색을 지키며 폭에 맞춰 접기
|
|
1333
1335
|
agent/loop.js 에이전트 루프
|
|
1334
1336
|
agent/session.js 대화 상태 + 컨텍스트 셈
|
|
1335
1337
|
agent/effort.js 단계별 추론 강도 배분
|
package/bin/deel.js
CHANGED
|
@@ -102,7 +102,7 @@ function help() {
|
|
|
102
102
|
say(` ${c.gray('--max-tokens <길이>')} 한 번에 받을 답 길이 상한 (32k). 큰 파일이 잘리면 올린다 — /out 과 같은 값`);
|
|
103
103
|
say(` ${c.gray('--think <수준>')} off / low / medium(기본) / high / max`);
|
|
104
104
|
say(` ${c.gray('--effort <배분>')} even(균일) / save(절약, 기본) / deep(깊게)`);
|
|
105
|
-
say(` ${c.gray('--no-tui')}
|
|
105
|
+
say(` ${c.gray('--no-tui')} 입력 상자 없이 줄 화면으로 (파이프·기록·좁은 터미널)`);
|
|
106
106
|
say(` ${c.gray('--offline')} 이 컴퓨터 밖으로는 아무것도 안 보냄 (자물쇠)`);
|
|
107
107
|
say(` ${c.gray('--continue')} 이 폴더에서 가장 최근 대화 이어하기`);
|
|
108
108
|
say(` ${c.gray('--resume <id>')} 골라서 이어하기 (deel sessions 로 id 확인)`);
|
|
@@ -173,9 +173,9 @@ async function main() {
|
|
|
173
173
|
maxTokens: flags['max-tokens'] ? parseSize(String(flags['max-tokens'])) : undefined,
|
|
174
174
|
think: flags.think ? String(flags.think) : undefined,
|
|
175
175
|
effort: flags.effort ? String(flags.effort) : undefined,
|
|
176
|
-
//
|
|
177
|
-
// --no-tui 줄 화면으로 (파이프·기록·좁은
|
|
178
|
-
// --tui 터미널이면 무조건
|
|
176
|
+
// 입력 상자를 쓸지. 안 주면 null — 그러면 화면 쪽이 상황을 보고 정한다.
|
|
177
|
+
// --no-tui 입력 상자 없이 줄 화면으로 (파이프·기록·좁은 터미널)
|
|
178
|
+
// --tui 터미널이면 무조건 입력 상자를 켠다
|
|
179
179
|
tui: flags['no-tui'] === true ? false : (flags.tui === true ? true : null),
|
|
180
180
|
offline: flags.offline === true || flags.offline === 'true',
|
|
181
181
|
continue: flags.continue === true || flags.c === true,
|
package/package.json
CHANGED
package/src/repl.js
CHANGED
|
@@ -69,8 +69,8 @@ const 답표시 = c.hcyan('▌');
|
|
|
69
69
|
export async function chatLoop(opts = {}) {
|
|
70
70
|
const cfg = load();
|
|
71
71
|
const prof = activeProfile(cfg);
|
|
72
|
-
// 연결이 없으면 화면을 세우기 전에 끝난다.
|
|
73
|
-
//
|
|
72
|
+
// 연결이 없으면 화면을 세우기 전에 끝난다. 세운 뒤에 나가면 상자를
|
|
73
|
+
// 그렸다 지우는 제어문자가 이 안내 사이에 끼어 화면이 지저분해진다.
|
|
74
74
|
if (!prof) {
|
|
75
75
|
바로쓰기('');
|
|
76
76
|
바로쓰기(` ${mark.warn} 저장된 연결이 없습니다. ${c.cyan('deel setup')} 을 먼저 실행하세요.`);
|
|
@@ -169,7 +169,33 @@ export async function chatLoop(opts = {}) {
|
|
|
169
169
|
// 지난 대화에서 정해 둔 것을 들고 시작한다.
|
|
170
170
|
session.memory = 기억토막(root);
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
/*
|
|
173
|
+
* 입력 상자를 쓸 때는 readline 이 스스로 되비추지 못하게 한다.
|
|
174
|
+
*
|
|
175
|
+
* readline 은 자기가 아는 커서 자리를 기준으로 지우고 다시 그린다. 그런데
|
|
176
|
+
* 그 자리는 우리가 그린 상자 테두리 안이 아니라 줄 맨 앞이다. 그대로 두면
|
|
177
|
+
* 백스페이스가 테두리를 갉아먹고, 긴 글이 접힐 때 상자가 무너진다.
|
|
178
|
+
*
|
|
179
|
+
* 그래서 되비추는 일만 뺏는다. 어디까지나 **되비추기만** 이다 — 한글 조합,
|
|
180
|
+
* 붙여넣기, 위아래 이력, Ctrl+A/E, 백스페이스는 전부 readline 이 그대로
|
|
181
|
+
* 맡는다. 우리는 readline 이 들고 있는 글(rl.line)을 상자 안에 그릴 뿐이다.
|
|
182
|
+
* 줄 편집을 직접 짜기 시작하면 한글 입력기부터 깨진다.
|
|
183
|
+
*/
|
|
184
|
+
const 상자쓰나 = 화면.kind === 'box';
|
|
185
|
+
// 사람이 지금 입력을 기다리는 중인가. 도구가 도는 동안 키를 눌러도
|
|
186
|
+
// 상자를 다시 그리면 안 된다 — 그 자리는 이미 대화가 흘러가고 있다.
|
|
187
|
+
let 입력기다림 = false;
|
|
188
|
+
// 되묻는 중이면 그 앞머리. 상자 대신 한 줄로 되비춘다.
|
|
189
|
+
let 묻는중 = null;
|
|
190
|
+
// 이번 틱에 다시 그리기로 이미 잡아 뒀나 (붙여넣기로 키가 쏟아질 때)
|
|
191
|
+
let 그릴예정 = false;
|
|
192
|
+
const 먹통 = { write() { return true; }, end() {}, on() {}, once() {}, emit() {}, removeListener() {} };
|
|
193
|
+
const rl = createInterface({
|
|
194
|
+
input: process.stdin,
|
|
195
|
+
output: 상자쓰나 ? 먹통 : process.stdout,
|
|
196
|
+
terminal: 상자쓰나 ? true : undefined,
|
|
197
|
+
historySize: 200,
|
|
198
|
+
});
|
|
173
199
|
|
|
174
200
|
// 입력을 큐로 받는다. rl.question 을 겹쳐 쓰면 파이프로 넣을 때 닫혀 버린다.
|
|
175
201
|
const queue = [];
|
|
@@ -179,6 +205,17 @@ export async function chatLoop(opts = {}) {
|
|
|
179
205
|
|
|
180
206
|
rl.on('line', (l) => {
|
|
181
207
|
if (echo) say(c.gray(l));
|
|
208
|
+
if (상자쓰나) {
|
|
209
|
+
if (묻는중 !== null) {
|
|
210
|
+
// 되묻는 자리: 답을 그 줄에 남긴 채 줄만 넘긴다.
|
|
211
|
+
process.stdout.write(`\r\x1b[2K${묻는중}${c.white(l)}\n`);
|
|
212
|
+
} else {
|
|
213
|
+
// 상자를 걷어내고, 사람이 보낸 글을 대화에 남긴다. 안 남기면 스크롤을
|
|
214
|
+
// 올렸을 때 답만 있고 무엇을 물었는지가 없다.
|
|
215
|
+
화면.입력지움();
|
|
216
|
+
if (l.trim()) say(` ${c.hcyan('❯')} ${c.white(l)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
182
219
|
if (waiter) { const w = waiter; waiter = null; w(l); }
|
|
183
220
|
else queue.push(l);
|
|
184
221
|
});
|
|
@@ -194,13 +231,47 @@ export async function chatLoop(opts = {}) {
|
|
|
194
231
|
if (process.stdin.isTTY) {
|
|
195
232
|
emitKeypressEvents(process.stdin, rl);
|
|
196
233
|
process.stdin.on('keypress', (_ch, key) => {
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
if (key && key.name === 'tab' && key.shift) {
|
|
235
|
+
session.work = nextWork(session.work);
|
|
236
|
+
const w = getWork(session.work);
|
|
237
|
+
화면.입력지움();
|
|
238
|
+
say(` ${c.hcyan(w.glyph)} ${c.bold(w.name)} ${c.gray('(' + w.en + ')')} ${c.gray(w.hint)}`
|
|
239
|
+
+ (canWrite(session.work) ? '' : ` ${c.green('· 파일을 못 바꿉니다')}`));
|
|
240
|
+
prompt();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
/*
|
|
244
|
+
* 친 것을 상자 안에 그린다.
|
|
245
|
+
*
|
|
246
|
+
* readline 이 이 키를 처리하고 rl.line 을 고친 **뒤에** 그려야 하는데,
|
|
247
|
+
* keypress 는 그 전에 온다. 그래서 한 틱 미룬다. 안 미루면 늘 한 글자
|
|
248
|
+
* 뒤처진 글이 보인다 — 치는 사람 눈에는 마지막 글자가 안 찍히는 것으로
|
|
249
|
+
* 보이고, 그게 제일 못 미더운 화면이다.
|
|
250
|
+
*/
|
|
251
|
+
if (!상자쓰나) return;
|
|
252
|
+
if (key && key.name === 'return') return; // 줄이 끝나는 것은 'line' 이 맡는다
|
|
253
|
+
if (묻는중 !== null) {
|
|
254
|
+
const 앞 = 묻는중;
|
|
255
|
+
setImmediate(() => {
|
|
256
|
+
if (묻는중 === null) return;
|
|
257
|
+
process.stdout.write(`\r\x1b[2K${앞}${c.white(rl.line ?? '')}`);
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (!입력기다림) return;
|
|
262
|
+
/*
|
|
263
|
+
* 여러 키가 한꺼번에 들어와도 **한 번만** 그린다.
|
|
264
|
+
*
|
|
265
|
+
* 붙여넣기는 글자 수만큼 키가 쏟아진다. 스무 줄짜리를 붙이면 상자를
|
|
266
|
+
* 수백 번 다시 그리게 되고, 화면이 눈에 띄게 떨린다. 어차피 마지막
|
|
267
|
+
* 한 번이 지금 상태이므로, 이번 틱에 이미 잡아 뒀으면 그냥 넘긴다.
|
|
268
|
+
*/
|
|
269
|
+
if (그릴예정) return;
|
|
270
|
+
그릴예정 = true;
|
|
271
|
+
setImmediate(() => {
|
|
272
|
+
그릴예정 = false;
|
|
273
|
+
if (입력기다림) 화면.입력갱신(session, rl.line ?? '', rl.cursor ?? 0);
|
|
274
|
+
});
|
|
204
275
|
});
|
|
205
276
|
}
|
|
206
277
|
|
|
@@ -211,10 +282,20 @@ export async function chatLoop(opts = {}) {
|
|
|
211
282
|
};
|
|
212
283
|
|
|
213
284
|
const ask = async (label, o = {}) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
285
|
+
const 앞 = ` ${c.gray('›')} ${label} ${o.def ? c.gray(`[${o.def}] `) : ''}`;
|
|
286
|
+
화면.붙임(앞);
|
|
287
|
+
/*
|
|
288
|
+
* 되묻는 자리는 상자를 안 쓴다 — '실행할까요? (y/n)' 에 테두리를 두르면
|
|
289
|
+
* 대화의 흐름이 끊긴다. 대신 되비추는 일은 우리가 맡아야 한다.
|
|
290
|
+
* 상자 모드에서는 readline 의 되비추기를 꺼 놨기 때문이다. 안 해 주면
|
|
291
|
+
* y 를 쳐도 화면에 아무것도 안 나타난다 — 먹은 건지 안 먹은 건지 모른다.
|
|
292
|
+
*/
|
|
293
|
+
묻는중 = 상자쓰나 ? 앞 : null;
|
|
294
|
+
try {
|
|
295
|
+
const a = await nextLine();
|
|
296
|
+
if (a === null) return o.def ?? '';
|
|
297
|
+
return a.trim() || o.def || '';
|
|
298
|
+
} finally { 묻는중 = null; }
|
|
218
299
|
};
|
|
219
300
|
|
|
220
301
|
/**
|
|
@@ -357,7 +438,7 @@ export async function chatLoop(opts = {}) {
|
|
|
357
438
|
say(` ${c.gray('/help 명령 목록')} ${c.gray('/think 추론 강도')} ${c.gray('Ctrl+C 중단·끝내기')}`);
|
|
358
439
|
|
|
359
440
|
// 입력 자리. 어떻게 생겼는지는 화면 쪽이 정한다 —
|
|
360
|
-
// 줄화면은 상태줄을 깔고 그 아래 ❯ 를,
|
|
441
|
+
// 줄화면은 상태줄을 깔고 그 아래 ❯ 를, 상자화면은 테두리를 두른 칸을 그린다.
|
|
361
442
|
const prompt = () => 화면.입력자리(session);
|
|
362
443
|
|
|
363
444
|
// Ctrl+C 는 상황에 따라 뜻이 다르다.
|
|
@@ -380,7 +461,9 @@ export async function chatLoop(opts = {}) {
|
|
|
380
461
|
|
|
381
462
|
for (;;) {
|
|
382
463
|
prompt();
|
|
464
|
+
입력기다림 = true;
|
|
383
465
|
const line = await nextLine();
|
|
466
|
+
입력기다림 = false;
|
|
384
467
|
if (line === null) break; // 입력이 끝났다 (파이프 종료 / Ctrl+D)
|
|
385
468
|
interrupted = false;
|
|
386
469
|
const text = line.trim();
|
|
@@ -495,7 +578,7 @@ export async function chatLoop(opts = {}) {
|
|
|
495
578
|
* 같아서, 화면을 훑을 때 '모델이 뭐라고 했는지' 를 눈으로 못 찾았다.
|
|
496
579
|
* 도구 이름·결과·바뀐 자리가 줄줄이 지나간 끝에 답이 섞여 있었다.
|
|
497
580
|
*
|
|
498
|
-
* 세로줄 하나면 된다.
|
|
581
|
+
* 세로줄 하나면 된다. 칸을 나눠 그리는 화면으로 갈 이유가 없다 —
|
|
499
582
|
* 파이프로 넘기거나 기록으로 남길 때도 그대로 읽힌다.
|
|
500
583
|
*/
|
|
501
584
|
case 'content':
|
|
@@ -534,12 +617,6 @@ export async function chatLoop(opts = {}) {
|
|
|
534
617
|
const 글 = t.state === 'done' ? c.gray(t.text) : t.state === 'doing' ? c.white(t.text) : c.gray(t.text);
|
|
535
618
|
say(` ${표} ${clip(글, 74)}`);
|
|
536
619
|
}
|
|
537
|
-
// 전체화면이면 오른쪽 칸에도 세워 둔다. 흘러가 버리지 않게 —
|
|
538
|
-
// 긴 일에서 '어디까지 했나' 는 늘 보여야 하는 정보다.
|
|
539
|
-
화면.할일칸(ev.result.todos.map((t) => {
|
|
540
|
-
const 표 = t.state === 'done' ? c.green('☑') : t.state === 'doing' ? c.hyellow('▶') : c.gray('☐');
|
|
541
|
-
return `${표} ${t.state === 'doing' ? c.white(t.text) : c.gray(t.text)}`;
|
|
542
|
-
}));
|
|
543
620
|
} else {
|
|
544
621
|
say(` ${toolResultLine(ev.result, ev.ms ?? 0)}`);
|
|
545
622
|
// 파일을 고쳤으면 무엇이 바뀌었는지 바로 보여 준다.
|
|
@@ -552,11 +629,6 @@ export async function chatLoop(opts = {}) {
|
|
|
552
629
|
// 엉뚱한 자리를 가리킨다 — 목록에 ../../.. 가 찍힌다.
|
|
553
630
|
session.noteChange(ev.result.changed ?? ev.args?.file_path, ev.result.diff);
|
|
554
631
|
for (const l of renderDiff(ev.result.diff, { maxLines: DIFF_LINES[session.level] ?? 20 })) say(l);
|
|
555
|
-
// 오른쪽 칸에 '이번 대화에서 뭘 건드렸나' 를 세워 둔다.
|
|
556
|
-
화면.파일칸([...session.changes].map(([p, d]) => {
|
|
557
|
-
const 이름 = ctx?.scope ? ctx.scope.show(p) : p;
|
|
558
|
-
return `${c.white(이름)} ${c.green('+' + d.added)}${c.red('-' + d.removed)}`;
|
|
559
|
-
}));
|
|
560
632
|
}
|
|
561
633
|
}
|
|
562
634
|
flush(); // 도구가 하나 끝날 때마다 적어 둔다
|
|
@@ -692,8 +764,8 @@ export async function chatLoop(opts = {}) {
|
|
|
692
764
|
// 띄운 남의 프로세스는 반드시 거둔다. 안 거두면 deel 을 껐는데도
|
|
693
765
|
// 그 서버가 계속 돌고 있게 된다 — 사람 눈에는 안 보이는 채로.
|
|
694
766
|
for (const s of mcp붙임.서버들) s.닫기();
|
|
695
|
-
// 끝맺음은 화면을 접기 **전에** 그린다.
|
|
696
|
-
//
|
|
767
|
+
// 끝맺음은 화면을 접기 **전에** 그린다. close() 가 상자를 걷어내므로,
|
|
768
|
+
// 그 뒤에 찍으면 걷어낸 자리에 뜬금없이 한 줄이 남는다.
|
|
697
769
|
say('');
|
|
698
770
|
say(` ${c.gray('끝냅니다.')} ${c.gray(`모델 호출 ${session.usage.calls}회 · 도구 시간 ${(session.usage.ms / 1000).toFixed(1)}초 · ↑${session.usage.in.toLocaleString()} ↓${session.usage.out.toLocaleString()}`)}`);
|
|
699
771
|
say('');
|