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.
- package/README.en.md +6 -3
- package/README.md +6 -3
- package/package.json +2 -2
- package/src/i18n/en.js +1 -0
- package/src/i18n/ko.js +1 -0
- package/src/repl.js +110 -5
- package/src/ui/inputbox.js +178 -5
- package/src/ui/motion.js +312 -5
- package/src/ui/office.js +431 -0
- package/src/ui/screen.js +319 -312
- package/src/ui/status.js +13 -2
- package/src/ui/wrap.js +47 -2
package/README.en.md
CHANGED
|
@@ -18,7 +18,7 @@ Zero dependencies · Node 20+ · Exactly one place your source can go
|
|
|
18
18
|
|
|
19
19
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
20
20
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
21
|
-
[](docs/en/develop.md)
|
|
22
22
|
|
|
23
23
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
24
24
|
[](package.json)
|
|
@@ -100,7 +100,7 @@ This page is the **summary**. Each section links to the detail behind it.
|
|
|
100
100
|
| [Speed and spend](docs/en/tuning.md) | Per-stage effort · the prefix cache · context length |
|
|
101
101
|
| [Safety and corporate review](docs/en/safety.md) | Undo · working scope · audit log · the review package |
|
|
102
102
|
| [Configuration](docs/en/config.md) · [Development](docs/en/develop.md) | Env vars · run flags · running the tests · folder layout |
|
|
103
|
-
| [Release notes](docs/en/releases.md) | [1.4.3](docs/en/releases.md#143) · [1.4.2](docs/en/releases.md#142) · [1.4.1](docs/en/releases.md#141) · [1.4.0](docs/en/releases.md#140) · [1.3.0](docs/en/releases.md#130) · [1.2.0](docs/en/releases.md#120) |
|
|
103
|
+
| [Release notes](docs/en/releases.md) | [1.5.2](docs/en/releases.md#152) · [1.5.1](docs/en/releases.md#151) · [1.5.0](docs/en/releases.md#150) · [1.4.3](docs/en/releases.md#143) · [1.4.2](docs/en/releases.md#142) · [1.4.1](docs/en/releases.md#141) · [1.4.0](docs/en/releases.md#140) · [1.3.0](docs/en/releases.md#130) · [1.2.0](docs/en/releases.md#120) |
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
@@ -980,7 +980,10 @@ so one run tells you everything.
|
|
|
980
980
|
|
|
981
981
|
| Version | What changed |
|
|
982
982
|
|---|---|
|
|
983
|
-
| **[1.
|
|
983
|
+
| **[1.5.2](docs/en/releases.md#152)** | Everything else the adversarial reviews turned up — where the screen was lying |
|
|
984
|
+
| [1.5.1](docs/en/releases.md#151) | Two of the things 1.5.0 added shipped broken. This fixes them |
|
|
985
|
+
| [1.5.0](docs/en/releases.md#150) | Line breaks exist now, the screen got fun, and it came out lighter than before |
|
|
986
|
+
| [1.4.3](docs/en/releases.md#143) | The README explains what's different, and the review report gets its missing line |
|
|
984
987
|
| [1.4.2](docs/en/releases.md#142) | 1.4.1 shipped before its own security fixes — this corrects that |
|
|
985
988
|
| [1.4.1](docs/en/releases.md#141) | No new features, only what was actually found and fixed — Windows abort, ReDoS, XSS |
|
|
986
989
|
| [1.4.0](docs/en/releases.md#140) | deel gets a face, speaks English, and sees meaning — eleven places |
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
|
|
20
20
|
[](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
|
|
21
|
-
[](docs/ko/develop.md)
|
|
22
22
|
|
|
23
23
|
[](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
|
|
24
24
|
[](package.json)
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
| [속도와 씀씀이](docs/ko/tuning.md) | 단계별 추론 강도 · 프리픽스 캐시 · 컨텍스트 길이 |
|
|
102
102
|
| [안전망과 사내 반입](docs/ko/safety.md) | 되돌리기 · 작업 범위 · 감사기록 · 심사 서류 |
|
|
103
103
|
| [설정](docs/ko/config.md) · [개발](docs/ko/develop.md) | 환경변수 · 실행 옵션 · 검사 돌리기 · 폴더 구조 |
|
|
104
|
-
| [릴리스 노트](docs/ko/releases.md) | [1.4.3](docs/ko/releases.md#143) · [1.4.2](docs/ko/releases.md#142) · [1.4.1](docs/ko/releases.md#141) · [1.4.0](docs/ko/releases.md#140) · [1.3.0](docs/ko/releases.md#130) · [1.2.0](docs/ko/releases.md#120) |
|
|
104
|
+
| [릴리스 노트](docs/ko/releases.md) | [1.5.2](docs/ko/releases.md#152) · [1.5.1](docs/ko/releases.md#151) · [1.5.0](docs/ko/releases.md#150) · [1.4.3](docs/ko/releases.md#143) · [1.4.2](docs/ko/releases.md#142) · [1.4.1](docs/ko/releases.md#141) · [1.4.0](docs/ko/releases.md#140) · [1.3.0](docs/ko/releases.md#130) · [1.2.0](docs/ko/releases.md#120) |
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
@@ -971,7 +971,10 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
|
|
|
971
971
|
|
|
972
972
|
| 판 | 무엇이 바뀌었나 |
|
|
973
973
|
|---|---|
|
|
974
|
-
| **[1.
|
|
974
|
+
| **[1.5.2](docs/ko/releases.md#152)** | 적대적 리뷰가 남긴 나머지를 전부 정리했다 — 화면이 거짓말하던 자리들 |
|
|
975
|
+
| [1.5.1](docs/ko/releases.md#151) | 1.5.0 이 넣은 것 두 개가 망가진 채 나갔다. 그것을 고친다 |
|
|
976
|
+
| [1.5.0](docs/ko/releases.md#150) | 줄바꿈이 생겼고, 화면이 재미있어졌고, 그러면서 오히려 가벼워졌다 |
|
|
977
|
+
| [1.4.3](docs/ko/releases.md#143) | README 가 왜 다른지 말하고, 심사서의 빠진 한 줄을 채운다 |
|
|
975
978
|
| [1.4.2](docs/ko/releases.md#142) | 1.4.1 이 보안 수정 전에 나갔다 — 그걸 바로잡는 판 |
|
|
976
979
|
| [1.4.1](docs/ko/releases.md#141) | 새 기능 없이, 실제로 찾은 것만 고친다 — 윈도우 abort·ReDoS·XSS |
|
|
977
980
|
| [1.4.0](docs/ko/releases.md#140) | deel 이 제 얼굴을 갖고, 영어로도 말하고, 뜻까지 본다 — 열한 자리 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deel-local-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI — 외부 의존성 0개 / Zero-dependency coding agent CLI for local LLMs and private gateways",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"verify": "node test/no-bundle.test.js && node test/network.test.js && node test/web.test.js",
|
|
52
52
|
"bench": "node test/edit-bench.js",
|
|
53
53
|
"demo": "node test/demo.js",
|
|
54
|
-
"check": "node --check bin/deel.js && node --check src/repl.js && node --check src/oneshot.js && node --check src/commands.js && node --check src/agent/loop.js && node --check src/agent/session.js && node --check src/agent/effort.js && node --check src/agent/modes.js && node --check src/agent/route.js && node --check src/agent/models.js && node --check src/agent/grade.js && node --check src/agent/mention.js && node --check src/agent/project.js && node --check src/tools/encoding.js && node --check src/tools/excel-com.js && node --check src/tools/excel.js && node --check src/tools/xlsx.js && node --check src/tools/docs.js && node --check src/agent/compact.js && node --check src/agent/store.js && node --check src/agent/sessionui.js && node --check src/backend/adapter.js && node --check src/backend/ctxsize.js && node --check src/backend/probe.js && node --check src/backend/detect.js && node --check src/backend/http.js && node --check src/backend/scan.js && node --check src/backend/scanui.js && node --check src/tools/index.js && node --check src/tools/desc.en.js && node --check src/tools/lsp.js && node --check src/lsp/rpc.js && node --check src/lsp/servers.js && node --check src/lsp/client.js && node --check src/lsp/diag.js && node --check src/tools/fsutil.js && node --check src/tools/edit-match.js && node --check src/tools/webfetch.js && node --check src/tools/todo.js && node --check src/tools/jobs.js && node --check src/skills/discover.js && node --check src/plugins/manage.js && node --check src/pack/zip.js && node --check src/pack/tar.js && node --check src/pack/selfpack.js && node --check src/safety/guard.js && node --check src/safety/undo.js && node --check src/safety/audit.js && node --check src/safety/network.js && node --check src/setup.js && node --check src/report.js && node --check src/config.js && node --check src/ui/diff.js && node --check src/ui/ansi.js && node --check src/ui/status.js && node --check src/ui/level.js && node --check src/ui/prompt.js && node --check src/ui/spinner.js && node --check src/ui/motion.js && node --check src/ui/notify.js && node --check src/ui/intro.js && node --check src/ui/banner.js && node --check src/ui/export.js && node --check src/i18n/index.js && node --check src/i18n/ko.js && node --check src/i18n/en.js && node --check src/preview/serve.js && node --check src/ui/working.js && node --check src/ui/inputbox.js && node --check src/ui/md.js && node --check src/agent/threads.js && node --check src/agent/evolve.js && node --check src/agent/pins.js && node --check src/agent/card.js && node --check src/agent/preset.js && node --check src/agent/evidence.js && node --check src/acp/jsonrpc.js && node --check src/acp/map.js && node --check src/acp/serve.js && node --check src/pack/sbom.js && node --check src/safety/secrets.js && echo OK",
|
|
54
|
+
"check": "node --check bin/deel.js && node --check src/repl.js && node --check src/oneshot.js && node --check src/commands.js && node --check src/agent/loop.js && node --check src/agent/session.js && node --check src/agent/effort.js && node --check src/agent/modes.js && node --check src/agent/route.js && node --check src/agent/models.js && node --check src/agent/grade.js && node --check src/agent/mention.js && node --check src/agent/project.js && node --check src/tools/encoding.js && node --check src/tools/excel-com.js && node --check src/tools/excel.js && node --check src/tools/xlsx.js && node --check src/tools/docs.js && node --check src/agent/compact.js && node --check src/agent/store.js && node --check src/agent/sessionui.js && node --check src/backend/adapter.js && node --check src/backend/ctxsize.js && node --check src/backend/probe.js && node --check src/backend/detect.js && node --check src/backend/http.js && node --check src/backend/scan.js && node --check src/backend/scanui.js && node --check src/tools/index.js && node --check src/tools/desc.en.js && node --check src/tools/lsp.js && node --check src/lsp/rpc.js && node --check src/lsp/servers.js && node --check src/lsp/client.js && node --check src/lsp/diag.js && node --check src/tools/fsutil.js && node --check src/tools/edit-match.js && node --check src/tools/webfetch.js && node --check src/tools/todo.js && node --check src/tools/jobs.js && node --check src/skills/discover.js && node --check src/plugins/manage.js && node --check src/pack/zip.js && node --check src/pack/tar.js && node --check src/pack/selfpack.js && node --check src/safety/guard.js && node --check src/safety/undo.js && node --check src/safety/audit.js && node --check src/safety/network.js && node --check src/setup.js && node --check src/report.js && node --check src/config.js && node --check src/ui/diff.js && node --check src/ui/ansi.js && node --check src/ui/status.js && node --check src/ui/level.js && node --check src/ui/prompt.js && node --check src/ui/spinner.js && node --check src/ui/motion.js && node --check src/ui/office.js && node --check src/ui/notify.js && node --check src/ui/intro.js && node --check src/ui/banner.js && node --check src/ui/export.js && node --check src/i18n/index.js && node --check src/i18n/ko.js && node --check src/i18n/en.js && node --check src/preview/serve.js && node --check src/ui/working.js && node --check src/ui/inputbox.js && node --check src/ui/wrap.js && node --check src/ui/md.js && node --check src/agent/threads.js && node --check src/agent/evolve.js && node --check src/agent/pins.js && node --check src/agent/card.js && node --check src/agent/preset.js && node --check src/agent/evidence.js && node --check src/acp/jsonrpc.js && node --check src/acp/map.js && node --check src/acp/serve.js && node --check src/pack/sbom.js && node --check src/safety/secrets.js && echo OK",
|
|
55
55
|
"coverage": "node test/coverage.mjs",
|
|
56
56
|
"prepublishOnly": "npm run check && npm test"
|
|
57
57
|
},
|
package/src/i18n/en.js
CHANGED
|
@@ -129,6 +129,7 @@ export const en = {
|
|
|
129
129
|
'head.shiftTab': 'Shift+Tab',
|
|
130
130
|
'head.shiftTabHint': ' changes it · ',
|
|
131
131
|
'head.tabHint': 'Tab completes the / command you are typing',
|
|
132
|
+
'head.newlineHint': 'New line: Alt+Enter (if your terminal sends it), or end a line with space + ` then Enter',
|
|
132
133
|
'head.thisPC': 'found',
|
|
133
134
|
'head.skills': 'skills {n}',
|
|
134
135
|
'head.commands': 'commands {n}',
|
package/src/i18n/ko.js
CHANGED
|
@@ -116,6 +116,7 @@ export const ko = {
|
|
|
116
116
|
'head.shiftTab': 'Shift+Tab',
|
|
117
117
|
'head.shiftTabHint': '으로 바꿉니다 · ',
|
|
118
118
|
'head.tabHint': 'Tab 은 치던 / 명령을 채웁니다',
|
|
119
|
+
'head.newlineHint': '줄바꿈: Alt+Enter(터미널이 지원하면) 또는 줄 끝에 빈칸+` 를 붙이고 Enter',
|
|
119
120
|
'head.thisPC': '이 PC',
|
|
120
121
|
'head.skills': '스킬 {n}',
|
|
121
122
|
'head.commands': '명령 {n}',
|
package/src/repl.js
CHANGED
|
@@ -121,6 +121,23 @@ const DIFF_LINES = { 쉬움: 14, 개발자: 40 };
|
|
|
121
121
|
// 모델의 말 앞에 세우는 세로줄. 도구 줄과 눈으로 갈리게 하는 유일한 표시다.
|
|
122
122
|
const 답표시 = c.hcyan('▌');
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* 줄 끝 백틱이 "이어쓰기" 표시인가.
|
|
126
|
+
*
|
|
127
|
+
* 예전에는 백틱 하나로 끝나기만 하면 표시로 봤다. 그런데 코딩 도구에서
|
|
128
|
+
* "read `config.json`" · "echo `date`" 처럼 **인라인 코드로 끝나는 줄**은
|
|
129
|
+
* 아주 흔하다. 그 줄들이 전부 안 보내지고, 닫는 백틱까지 뜯긴 채 이어쓰기
|
|
130
|
+
* 모드에 갇혔다 — 사람 눈에는 Enter 가 그냥 안 먹는 것으로 보인다.
|
|
131
|
+
*
|
|
132
|
+
* 그래서 조건을 둘로 좁혔다. 백틱 앞이 **빈칸**이고(또는 백틱만 있는 줄이고),
|
|
133
|
+
* 그 줄에 백틱이 **그것 하나뿐**일 때만 표시로 본다. 인라인 코드는 백틱이
|
|
134
|
+
* 둘이라 안 걸리고, ``` 울타리는 앞이 백틱이라 안 걸린다.
|
|
135
|
+
*
|
|
136
|
+
* 검사에서 표를 만들어 재려고 밖으로 뺐다.
|
|
137
|
+
*/
|
|
138
|
+
export const 이어쓰기표시 = (l) =>
|
|
139
|
+
/(^|\s)`$/.test(l) && (String(l).match(/`/g) ?? []).length === 1;
|
|
140
|
+
|
|
124
141
|
export async function chatLoop(opts = {}) {
|
|
125
142
|
const cfg = load();
|
|
126
143
|
const prof = activeProfile(cfg);
|
|
@@ -322,17 +339,54 @@ export async function chatLoop(opts = {}) {
|
|
|
322
339
|
let closed = false;
|
|
323
340
|
const echo = !process.stdin.isTTY; // 파이프·기록용일 때는 입력을 되비춘다
|
|
324
341
|
|
|
342
|
+
/*
|
|
343
|
+
* 줄 끝에 ` (백틱) 을 붙이면 "아직 안 끝났다" 는 뜻이다 — 여러 줄을 한
|
|
344
|
+
* 메시지로 묶어 보낼 때 쓴다. 확정된 줄은 여기 쌓아 두다가, ` 없이 끝나는
|
|
345
|
+
* 줄이 오면 그때 \n 으로 이어붙여 한 메시지로 내보낸다.
|
|
346
|
+
*
|
|
347
|
+
* 실제 개행을 상자(rl.line) 안에 넣어서는 **안 된다**. 화면은 멀쩡히
|
|
348
|
+
* 그려지지만, Enter 로 보낼 때 readline 의 history 가 그 줄을 \n 에서
|
|
349
|
+
* 쪼개 거꾸로 쌓고 \r 로 다시 붙여 돌려준다. "안녕\n반가워" 를 보내면
|
|
350
|
+
* 'line' 이벤트에 오는 값은 "반가워\r안녕" 이다 — 사람이 쓴 순서가
|
|
351
|
+
* 뒤집혀서 모델에게 간다. historySize 를 0 으로 두면 안 그러니 history 가
|
|
352
|
+
* 하는 짓이 맞다. 그래서 개행은 rl.line 밖, 여기서만 다룬다.
|
|
353
|
+
*
|
|
354
|
+
* 백슬래시(\) 가 아니라 백틱을 고른 것은 일부러다 — 이 프로그램은 윈도우
|
|
355
|
+
* 경로(`C:\Users\...`)를 늘 다루는데, 그 끝은 흔히 백슬래시로 끝난다.
|
|
356
|
+
* 백슬래시를 표로 쓰면 평범한 경로를 붙여넣기만 해도 계속 이어 치는 것으로
|
|
357
|
+
* 잘못 읽힌다. 백틱은 그럴 일이 없다.
|
|
358
|
+
*/
|
|
359
|
+
let 이어쓰기줄들 = null;
|
|
360
|
+
|
|
361
|
+
/** 줄을 쌓아 두고 "다음 줄" 이라고 알린다. 백틱 표시와 Alt+Enter 가 같이 쓴다. */
|
|
362
|
+
const 줄쌓기 = (줄) => {
|
|
363
|
+
const 첫줄 = 이어쓰기줄들 === null;
|
|
364
|
+
(이어쓰기줄들 ??= []).push(줄);
|
|
365
|
+
화면.입력지움();
|
|
366
|
+
say(`${첫줄 ? ` ${c.hcyan('❯')} ` : ' '}${c.white(줄)}`);
|
|
367
|
+
say(` ${c.gray('… 다음 줄. 그냥 Enter 로 보냅니다 · Ctrl+C 로 취소')}`);
|
|
368
|
+
};
|
|
369
|
+
|
|
325
370
|
rl.on('line', (l) => {
|
|
326
371
|
if (echo) say(c.gray(l));
|
|
372
|
+
let 보낼것 = l;
|
|
327
373
|
if (상자쓰나) {
|
|
328
374
|
if (묻는중 !== null) {
|
|
329
375
|
// 되묻는 자리: 답을 그 줄에 남긴 채 줄만 넘긴다.
|
|
330
376
|
process.stdout.write(`\r\x1b[2K${묻는중}${c.white(l)}\n`);
|
|
331
377
|
} else if (입력기다림) {
|
|
378
|
+
// 표시 자체와 그 앞 빈칸은 사람이 친 글이 아니라 문법이다. 같이 뗀다.
|
|
379
|
+
if (이어쓰기표시(l)) {
|
|
380
|
+
줄쌓기(l.slice(0, -1).replace(/\s+$/, ''));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
332
383
|
// 상자를 걷어내고, 사람이 보낸 글을 대화에 남긴다. 안 남기면 스크롤을
|
|
333
384
|
// 올렸을 때 답만 있고 무엇을 물었는지가 없다.
|
|
385
|
+
const 이었나 = 이어쓰기줄들 !== null;
|
|
386
|
+
보낼것 = 이었나 ? [...이어쓰기줄들, l].join('\n') : l;
|
|
387
|
+
이어쓰기줄들 = null;
|
|
334
388
|
화면.입력지움();
|
|
335
|
-
if (l.trim()) say(` ${c.hcyan('❯')} ${c.white(l)}`);
|
|
389
|
+
if (l.trim() || 이었나) say(`${이었나 ? ' ' : ` ${c.hcyan('❯')} `}${c.white(l)}`);
|
|
336
390
|
} else if (l.trim()) {
|
|
337
391
|
/*
|
|
338
392
|
* 일하는 도중에 미리 쳐 둔 것.
|
|
@@ -344,8 +398,8 @@ export async function chatLoop(opts = {}) {
|
|
|
344
398
|
화면.대기갱신('', queue.length + 1);
|
|
345
399
|
}
|
|
346
400
|
}
|
|
347
|
-
if (waiter) { const w = waiter; waiter = null; w(
|
|
348
|
-
else queue.push(
|
|
401
|
+
if (waiter) { const w = waiter; waiter = null; w(보낼것); }
|
|
402
|
+
else queue.push(보낼것);
|
|
349
403
|
});
|
|
350
404
|
rl.on('close', () => {
|
|
351
405
|
closed = true;
|
|
@@ -432,7 +486,36 @@ export async function chatLoop(opts = {}) {
|
|
|
432
486
|
* 보이고, 그게 제일 못 미더운 화면이다.
|
|
433
487
|
*/
|
|
434
488
|
if (!상자쓰나) return;
|
|
435
|
-
if (key && key.name === 'return')
|
|
489
|
+
if (key && key.name === 'return') {
|
|
490
|
+
/*
|
|
491
|
+
* Alt+Enter(맥은 Option+Enter) 만 여기서 잡을 수 있다 — 나머지는 못 잡는다.
|
|
492
|
+
*
|
|
493
|
+
* 평범한 Enter 도, 대부분 터미널에서의 Shift+Enter·Ctrl+Enter 도 여기
|
|
494
|
+
* 닿기 전에 이미 끝나 있다. readline 도 우리와 같은 keypress 를 듣고
|
|
495
|
+
* 있는데(이 리스너보다 먼저 등록됐다), 터미널이 그 조합들을 평범한
|
|
496
|
+
* Enter 와 **똑같은 바이트**(\r, 드물게 \n)로 보내는 한 readline 이
|
|
497
|
+
* 우리보다 먼저 줄을 끝내 버린다 — 그 뒤에 우리가 뭘 해도 늦다.
|
|
498
|
+
* (readline 코드를 흉내 내서 직접 확인했다: \r·\n 은 keypress 가
|
|
499
|
+
* 이 리스너에 닿기도 전에 'line' 이벤트부터 쏜다.)
|
|
500
|
+
*
|
|
501
|
+
* ESC 로 시작하는 조합(Alt/Option+Enter)은 다르다 — readline 이 ESC
|
|
502
|
+
* 뒤에 더 올 것을 기다리는 동안 우리 keypress 가 먼저 온다. 그래서
|
|
503
|
+
* key.meta 로 여기서 골라낼 수 있다. rl.write('\n') 은 못 쓴다 —
|
|
504
|
+
* 그것도 내부적으로 줄을 끝내는 처리를 그대로 타 버린다(직접 확인).
|
|
505
|
+
*
|
|
506
|
+
* 커서 앞은 확정해서 쌓고, 뒤는 상자에 남긴다. rl.line 에 \n 을
|
|
507
|
+
* 넣어 이어붙이지 **않는다** — 그러면 화면은 맞는데 보낼 때
|
|
508
|
+
* history 가 줄 순서를 뒤집는다(이어쓰기줄들 선언부 참고).
|
|
509
|
+
*/
|
|
510
|
+
if (key.meta && 입력기다림 && 묻는중 === null) {
|
|
511
|
+
const 뒤 = (rl.line ?? '').slice(rl.cursor ?? 0);
|
|
512
|
+
줄쌓기((rl.line ?? '').slice(0, rl.cursor ?? 0));
|
|
513
|
+
rl.line = 뒤;
|
|
514
|
+
rl.cursor = 0;
|
|
515
|
+
} else {
|
|
516
|
+
return; // 줄이 끝나는 것은 'line' 이 맡는다
|
|
517
|
+
}
|
|
518
|
+
}
|
|
436
519
|
if (묻는중 !== null) {
|
|
437
520
|
const 앞 = 묻는중;
|
|
438
521
|
setImmediate(() => {
|
|
@@ -488,6 +571,7 @@ export async function chatLoop(opts = {}) {
|
|
|
488
571
|
* y 를 쳐도 화면에 아무것도 안 나타난다 — 먹은 건지 안 먹은 건지 모른다.
|
|
489
572
|
*/
|
|
490
573
|
묻는중 = 상자쓰나 ? 앞 : null;
|
|
574
|
+
이어쓰기줄들 = null; // 되묻는 사이에 걸쳐 있던 미완성 이어쓰기는 버린다
|
|
491
575
|
try {
|
|
492
576
|
const a = await nextLine();
|
|
493
577
|
if (a === null) return o.def ?? '';
|
|
@@ -679,7 +763,7 @@ export async function chatLoop(opts = {}) {
|
|
|
679
763
|
}
|
|
680
764
|
|
|
681
765
|
// ── 머리말 ────────────────────────────────────────────────────────────
|
|
682
|
-
화면.머리말(headerLines(session, found));
|
|
766
|
+
화면.머리말(headerLines(session, found, 상자쓰나));
|
|
683
767
|
const warn = [];
|
|
684
768
|
// 홈 폴더에서 켠 경우. 작업 범위가 집 전체가 된다.
|
|
685
769
|
//
|
|
@@ -739,6 +823,17 @@ export async function chatLoop(opts = {}) {
|
|
|
739
823
|
let interrupted = false;
|
|
740
824
|
let turn = null; // 지금 도는 턴의 AbortController
|
|
741
825
|
rl.on('SIGINT', () => {
|
|
826
|
+
/*
|
|
827
|
+
* 쌓아 둔 줄이 있으면 먼저 버린다. 이게 없으면 이어쓰기를 **취소할 길이
|
|
828
|
+
* 없다** — 표를 하나 붙여 놓고 마음이 바뀌어도, 다음에 치는 것이 무엇이든
|
|
829
|
+
* 앞줄에 붙어서 나간다. `/help` 같은 슬래시 명령까지 앞줄에 붙는 순간
|
|
830
|
+
* 더는 '/' 로 시작하지 않아 명령으로 안 읽히고 모델에게 넘어간다.
|
|
831
|
+
*/
|
|
832
|
+
if (이어쓰기줄들 !== null) {
|
|
833
|
+
이어쓰기줄들 = null;
|
|
834
|
+
say('');
|
|
835
|
+
say(` ${c.gray('이어쓰던 것을 버렸습니다.')}`);
|
|
836
|
+
}
|
|
742
837
|
if (turn && !turn.signal.aborted) {
|
|
743
838
|
turn.abort();
|
|
744
839
|
return; // 화면 정리는 루프 쪽 'aborted' 이벤트가 한다
|
|
@@ -759,6 +854,8 @@ export async function chatLoop(opts = {}) {
|
|
|
759
854
|
* 코드가 안 하는 것을 화면이 약속하고 있었던 셈이다.
|
|
760
855
|
*/
|
|
761
856
|
let 이어갈것 = null; // 이어서 보낼 말 (null 이면 사람 입력을 기다린다)
|
|
857
|
+
// 지금 도는 하위 작업 수. 사무실이 자리를 이만큼 채운다.
|
|
858
|
+
let 도는하위 = 0;
|
|
762
859
|
let 이어갈모드 = null; // 그때 쓸 작업 모드. 다시 고르지 않는다.
|
|
763
860
|
|
|
764
861
|
/*
|
|
@@ -1031,6 +1128,9 @@ export async function chatLoop(opts = {}) {
|
|
|
1031
1128
|
// 같이 돈 것은 이름을 다시 적어 준다. 안 그러면 어느 결과인지 모른다.
|
|
1032
1129
|
if (ev.parallel || ev.showLabel) say(` ${toolLabel(ev.name, ev.args)}`);
|
|
1033
1130
|
if (ev.name === 'TodoWrite' && ev.result?.todos) {
|
|
1131
|
+
// 사무실 화이트보드도 이걸 본다. 할 일은 세션이 아니라 턴 문맥에
|
|
1132
|
+
// 있어서 상자가 스스로 못 읽는다 — 아는 자리에서 넣어 준다.
|
|
1133
|
+
화면.할일갱신(ev.result.todos);
|
|
1034
1134
|
for (const t of ev.result.todos) {
|
|
1035
1135
|
const 표 = t.state === 'done' ? c.green('☑') : t.state === 'doing' ? c.hyellow('▶') : c.gray('☐');
|
|
1036
1136
|
const 글 = t.state === 'done' ? c.gray(t.text) : t.state === 'doing' ? c.white(t.text) : c.gray(t.text);
|
|
@@ -1129,6 +1229,9 @@ export async function chatLoop(opts = {}) {
|
|
|
1129
1229
|
clearThinking();
|
|
1130
1230
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1131
1231
|
화면.일바꿈('하위', clip(ev.목적, 24));
|
|
1232
|
+
// 사무실은 도는 하위 작업 수만큼 자리를 채운다.
|
|
1233
|
+
도는하위 += 1;
|
|
1234
|
+
화면.하위갱신(도는하위);
|
|
1132
1235
|
say('');
|
|
1133
1236
|
say(` ${c.hmagenta('⌥')} ${c.bold('하위 작업')} ${c.white(clip(ev.목적, 60))}`
|
|
1134
1237
|
+ ` ${c.gray(`· ${보일이름(ev.모드)} · 최대 ${ev.steps}걸음`)}`);
|
|
@@ -1150,6 +1253,8 @@ export async function chatLoop(opts = {}) {
|
|
|
1150
1253
|
case 'task_done': {
|
|
1151
1254
|
clearThinking();
|
|
1152
1255
|
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
1256
|
+
도는하위 = Math.max(0, 도는하위 - 1);
|
|
1257
|
+
화면.하위갱신(도는하위);
|
|
1153
1258
|
const 끝 = ev.끝 ?? {};
|
|
1154
1259
|
const 잘됨 = 끝.type === 'done';
|
|
1155
1260
|
const 왜 = { done: '끝냈습니다', limit: '걸음 수를 다 써서 멈췄습니다 — 다 못 했습니다',
|
package/src/ui/inputbox.js
CHANGED
|
@@ -37,6 +37,9 @@ import { c, width, cursor, clip } from './ansi.js';
|
|
|
37
37
|
import { isLocalHost } from '../safety/network.js';
|
|
38
38
|
import { statusLine, contextWarning } from './status.js';
|
|
39
39
|
import { 접어쓰기 } from './wrap.js';
|
|
40
|
+
import {
|
|
41
|
+
켜나 as 사무실켜나, 사무실줄들, 상태만들기 as 사무실상태,
|
|
42
|
+
} from './office.js';
|
|
40
43
|
import { 문구고르기, 걸린시간, 느긋해질때, 문구주기 } from './working.js';
|
|
41
44
|
import { 그림고르기 } from './motion.js';
|
|
42
45
|
import { 최대추천 } from './complete.js';
|
|
@@ -56,6 +59,14 @@ const 줄끝지움 = '\x1b[K';
|
|
|
56
59
|
const 위로 = (n) => (n > 0 ? `\x1b[${n}A` : '');
|
|
57
60
|
// 돌아가는 표시를 한 칸 넘기는 주기. 사람 눈에 부드럽고, 다시 그리는 값은 싸다.
|
|
58
61
|
const 돌림주기 = 90;
|
|
62
|
+
/*
|
|
63
|
+
* 견주기를 접고 통째로 덮어써서 화면을 되살리는 주기(ms).
|
|
64
|
+
*
|
|
65
|
+
* 판 수가 아니라 **시계**로 센다. 일이 도는 동안에는 90ms 마다 그리니 판
|
|
66
|
+
* 수로 세도 같지만, 놀고 있을 때는 키를 칠 때만 그린다 — 그때 판으로 세면
|
|
67
|
+
* '45타 뒤' 가 되고, 가만히 두면 깨진 화면이 영영 안 낫는다.
|
|
68
|
+
*/
|
|
69
|
+
const 자가치유주기 = 4000;
|
|
59
70
|
|
|
60
71
|
// 상자 안쪽 왼쪽에 붙는 것: ' ' + '│' + ' ' + '❯' + ' ' → 5칸
|
|
61
72
|
const 앞머리 = 5;
|
|
@@ -76,7 +87,7 @@ export const 안쪽최대 = 8;
|
|
|
76
87
|
* @returns {{줄들: string[], 커서: {위: number, 열: number}}}
|
|
77
88
|
* 커서.위 = 마지막 줄에서 몇 줄 위인가, 커서.열 = 1부터 세는 칸
|
|
78
89
|
*/
|
|
79
|
-
export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '', 일감 = null, 추천 = [], 바깥 = false } = {}) {
|
|
90
|
+
export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '', 일감 = null, 추천 = [], 바깥 = false, 사무실 = null } = {}) {
|
|
80
91
|
const 칸 = Math.max(20, 폭);
|
|
81
92
|
/*
|
|
82
93
|
* ── 테두리가 곧 경계선이다 ───────────────────────────────────────────
|
|
@@ -96,6 +107,16 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
96
107
|
const 안쪽 = Math.max(4, 칸 - 앞머리 - 2); // 오른쪽 ' │' 두 칸
|
|
97
108
|
const 줄들 = [];
|
|
98
109
|
|
|
110
|
+
/*
|
|
111
|
+
* 사무실은 **상태줄 위**에 얹힌다.
|
|
112
|
+
*
|
|
113
|
+
* 여기 넣는 것이 중요하다. 상자가 이미 '아래 몇 줄을 붙박아 두고 그 자리에서
|
|
114
|
+
* 다시 그리기' 를 하고 있으므로, 사무실은 그 붙박이 자리가 몇 줄 길어지는
|
|
115
|
+
* 것일 뿐이다. 새 화면 방식을 안 들이는 것 — 예전 전체화면(tui.js)이 대화
|
|
116
|
+
* 흐름과 다투다 죽었다.
|
|
117
|
+
*/
|
|
118
|
+
if (사무실?.length) 줄들.push(...사무실);
|
|
119
|
+
|
|
99
120
|
if (상태) 줄들.push(상태);
|
|
100
121
|
if (경고) 줄들.push(` ${경고}`);
|
|
101
122
|
|
|
@@ -240,6 +261,22 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
240
261
|
export class InputBox {
|
|
241
262
|
constructor() {
|
|
242
263
|
this.그린줄 = 0; // 지금 화면에 붙어 있는 상자의 줄 수
|
|
264
|
+
|
|
265
|
+
/*
|
|
266
|
+
* 마지막으로 그릴 때의 터미널 크기.
|
|
267
|
+
*
|
|
268
|
+
* 폭이 바뀌는 순간 터미널은 **이미** 제 손으로 줄을 다시 접는다. 그런데
|
|
269
|
+
* 그린줄·커서위·앞줄들 은 옛 폭에서 센 값이라, 그 뒤로 커서를 올리는 수가
|
|
270
|
+
* 전부 틀어진다 — 틀어진 자리에 다음 판을 덮어쓰니 대화가 지워진다.
|
|
271
|
+
* 줄 수가 우연히 달라질 때까지 아무도 이걸 안 고쳐 줬다.
|
|
272
|
+
*
|
|
273
|
+
* 'resize' 이벤트를 듣는 길도 있는데 안 쓴다. 상자마다 리스너를 하나씩
|
|
274
|
+
* 걸면 걷을 자리가 필요하고(안 걷으면 EventEmitter 경고가 뜬다), 이벤트가
|
|
275
|
+
* 안 오는 환경도 있다. 어차피 크기가 틀어져서 손해를 보는 순간은 **다음에
|
|
276
|
+
* 그릴 때**이므로, 그 자리에서 견주면 그것으로 충분하다.
|
|
277
|
+
*/
|
|
278
|
+
this.그린폭 = null;
|
|
279
|
+
this.그린높이 = null;
|
|
243
280
|
/*
|
|
244
281
|
* 커서를 상자 맨 아랫줄에서 몇 줄 위에 놓아 뒀나.
|
|
245
282
|
*
|
|
@@ -278,6 +315,56 @@ export class InputBox {
|
|
|
278
315
|
* 그래서 규칙을 하나로 못 박는다 — **줄 중간에서는 아무것도 안 그린다.**
|
|
279
316
|
*/
|
|
280
317
|
this.줄중간 = false;
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* 지난번에 화면에 찍어 둔 줄들. **안 바뀐 줄을 안 보내려고** 들고 있다.
|
|
321
|
+
*
|
|
322
|
+
* 상자는 90ms 마다 다시 그려지는데, 그때마다 열두 줄을 통째로 보내면
|
|
323
|
+
* 대부분이 지난번과 똑같은 글자다. 재 보니 한 판에 실제로 바뀌는 것은
|
|
324
|
+
* 한 줄뿐이었다 — 나머지 열한 줄은 같은 것을 다시 보내는 셈이다.
|
|
325
|
+
* 줄마다 견줘서 바뀐 것만 보내면 터미널로 나가는 양이 91% 줄어든다.
|
|
326
|
+
*
|
|
327
|
+
* null 이면 견줄 것이 없다는 뜻이다(막 지웠거나 줄 수가 달라졌거나).
|
|
328
|
+
*/
|
|
329
|
+
this.앞줄들 = null;
|
|
330
|
+
/*
|
|
331
|
+
* 몇 판째 견주기로만 그렸나.
|
|
332
|
+
*
|
|
333
|
+
* 견주기는 '화면이 내가 아는 그대로다' 를 믿는다. 그런데 이 저장소에는
|
|
334
|
+
* 화면 객체를 안 거치고 stdout 에 바로 쓰는 자리가 몇 군데 있다(그 때문에
|
|
335
|
+
* 예전 전체화면이 죽었다). 그런 글이 상자를 덮으면 견주기는 그 줄을
|
|
336
|
+
* '안 바뀌었다' 며 안 고치고, 깨진 화면이 그대로 남는다.
|
|
337
|
+
*
|
|
338
|
+
* 통째로 그리던 예전 방식은 매번 덮어써서 저절로 나았다. 그 자가 치유를
|
|
339
|
+
* 잃지 않으려고, 가끔 한 번은 통째로 그린다.
|
|
340
|
+
*
|
|
341
|
+
* 처음에는 '판 45번마다' 로 셌다. 90ms 마다 도는 동안에는 그것이 4초였지만,
|
|
342
|
+
* **일이 안 도는 동안에는 그리기가 키를 칠 때만 일어난다.** 그래서 그때는
|
|
343
|
+
* 4초가 아니라 '45타' 가 됐다 — 놀고 있을 때 화면이 깨지면 마흔다섯 번을
|
|
344
|
+
* 더 쳐야 나았고, 가만히 두면 영영 안 나았다. 시계로 세야 맞다.
|
|
345
|
+
*/
|
|
346
|
+
this.마지막통째로 = 0;
|
|
347
|
+
|
|
348
|
+
/*
|
|
349
|
+
* 사무실 화이트보드에 붙일 할 일.
|
|
350
|
+
*
|
|
351
|
+
* 할 일은 세션이 아니라 턴 문맥(ctx.todos)에 있어서 상자가 스스로 못
|
|
352
|
+
* 읽는다. 그래서 아는 쪽에서 넣어 준다(할일갱신). 안 넣어 주면 빈 보드로
|
|
353
|
+
* 둔다 — **모르는 것을 그럴듯하게 채우지 않는다.**
|
|
354
|
+
*/
|
|
355
|
+
this.할일들 = [];
|
|
356
|
+
// 지금 도는 하위 작업 수 (사무실 자리 수). 모르면 0 이다.
|
|
357
|
+
this.도는하위 = 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 할 일이 바뀌었다. 사무실 화이트보드가 이걸 본다. */
|
|
361
|
+
할일갱신(할일들) {
|
|
362
|
+
this.할일들 = Array.isArray(할일들) ? 할일들 : [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** 도는 하위 작업 수. 사무실이 자리를 이만큼 채운다. */
|
|
366
|
+
하위갱신(n) {
|
|
367
|
+
this.도는하위 = Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
|
|
281
368
|
}
|
|
282
369
|
|
|
283
370
|
/** 대화 글이 줄 중간까지 쓰였다. 줄이 끝날 때까지 상자를 안 세운다. */
|
|
@@ -374,6 +461,8 @@ export class InputBox {
|
|
|
374
461
|
process.stdout.write(out);
|
|
375
462
|
this.그린줄 = 0;
|
|
376
463
|
this.커서위 = 0;
|
|
464
|
+
// 지운 자리를 '지난번에 찍어 둔 것' 으로 믿으면 안 된다.
|
|
465
|
+
this.앞줄들 = null;
|
|
377
466
|
}
|
|
378
467
|
|
|
379
468
|
/**
|
|
@@ -398,12 +487,53 @@ export class InputBox {
|
|
|
398
487
|
if (this.줄중간) {
|
|
399
488
|
process.stdout.write('\n');
|
|
400
489
|
this.줄중간 = false;
|
|
490
|
+
// 화면이 한 줄 밀렸다 — 지난번 줄들은 이제 견줄 것이 못 된다.
|
|
491
|
+
this.앞줄들 = null;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/*
|
|
495
|
+
* 사무실은 켠 사람에게만, 그리고 화면이 넉넉할 때만 그린다.
|
|
496
|
+
* 켜나() 가 그 판단을 다 한다 — 여기서는 켜졌으면 줄을 받아 얹을 뿐이다.
|
|
497
|
+
*
|
|
498
|
+
* 칸수를 **한 번만** 읽어서 둘 다에 쓴다. 따로 읽으면 켜나() 가 본 폭과
|
|
499
|
+
* 실제로 그린 폭이 다를 수 있고, 사무실이 터미널보다 한 칸이라도 넓어지는
|
|
500
|
+
* 순간 줄이 접힌다 — 상자는 접힌 줄을 못 세서 그만큼 어긋난 자리에 다음
|
|
501
|
+
* 판을 덮어쓴다. 화면이 통째로 지워지는 길이 거기서 열린다.
|
|
502
|
+
*
|
|
503
|
+
* 상자는 칸수를 몰라도 그려야 하므로 80 으로 버티지만, 사무실은 모르면
|
|
504
|
+
* 아예 안 켠다. 모르는 채로 켜는 쪽이 훨씬 큰 손해다.
|
|
505
|
+
*/
|
|
506
|
+
const 칸수 = process.stdout.columns;
|
|
507
|
+
const 폭 = 칸수 ?? 80;
|
|
508
|
+
|
|
509
|
+
/*
|
|
510
|
+
* 지난번에 그린 크기와 다르면 들고 있던 것을 다 버린다.
|
|
511
|
+
*
|
|
512
|
+
* 터미널이 이미 제 손으로 줄을 다시 접어 놨으므로, 옛 폭에서 센 값으로
|
|
513
|
+
* 커서를 올리면 엉뚱한 줄에 덮어쓴다 — 대화가 지워지는 길이 그것이다.
|
|
514
|
+
*
|
|
515
|
+
* 그린줄을 0 으로 두면 위로 안 올라가고 지금 자리에서 새로 그린다. 창을
|
|
516
|
+
* 줄인 직후 한 번은 낡은 상자가 위에 남을 수 있는데, 그건 스크롤로 지나가는
|
|
517
|
+
* 자국일 뿐이다. **어디 있는지 모르는 줄을 짐작해서 덮어쓰는 것보다,
|
|
518
|
+
* 안 건드리고 새로 그리는 편이 낫다.**
|
|
519
|
+
*/
|
|
520
|
+
const 높이 = process.stdout.rows ?? null;
|
|
521
|
+
if (this.그린폭 !== 폭 || this.그린높이 !== 높이) {
|
|
522
|
+
this.앞줄들 = null;
|
|
523
|
+
this.그린줄 = 0;
|
|
524
|
+
this.커서위 = 0;
|
|
525
|
+
this.그린폭 = 폭;
|
|
526
|
+
this.그린높이 = 높이;
|
|
401
527
|
}
|
|
528
|
+
const 방 = 사무실켜나(process.stdout.rows, 칸수)
|
|
529
|
+
? 사무실줄들(사무실상태(this.session, this.일감, this.틱, this.할일들, this.도는하위), 폭)
|
|
530
|
+
: null;
|
|
402
531
|
|
|
403
|
-
const
|
|
532
|
+
const 짜기 = (방줄들) => 프레임({
|
|
533
|
+
사무실: 방줄들,
|
|
404
534
|
글,
|
|
405
535
|
커서: 커서자리,
|
|
406
|
-
|
|
536
|
+
폭,
|
|
407
537
|
상태: this.session ? statusLine(this.session) : '',
|
|
408
538
|
경고: this.session ? (contextWarning(this.session) ?? '') : '',
|
|
409
539
|
곁말: this.곁말,
|
|
@@ -414,6 +544,24 @@ export class InputBox {
|
|
|
414
544
|
바깥: this.session ? 바깥으로가나(this.session) : false,
|
|
415
545
|
});
|
|
416
546
|
|
|
547
|
+
/*
|
|
548
|
+
* ── 다 그려 놓고 나서, 화면에 안 들어가면 사무실을 뺀다 ───────────────
|
|
549
|
+
*
|
|
550
|
+
* 켜나() 의 28줄은 '사무실 12 + 상자 5' 로 잡은 값인데, 상자는 그것보다
|
|
551
|
+
* 훨씬 커질 수 있다. 긴 글이 여덟 줄로 접히고 추천 목록 여섯 줄에 '그 밖에
|
|
552
|
+
* N개 더' 와 컨텍스트 경고까지 붙으면 서른두 줄이다. 그러면 붙박이 자리가
|
|
553
|
+
* 화면보다 길어져서, 터미널이 위로 밀어 올린 만큼 커서 셈이 통째로 어긋난다.
|
|
554
|
+
*
|
|
555
|
+
* 관문 숫자를 34쯤으로 올리는 길도 있지만, 그러면 평소에 멀쩡히 쓰던
|
|
556
|
+
* 사람들까지 사무실을 잃는다. 최악의 경우는 드물게 잠깐 오는 것이므로,
|
|
557
|
+
* **그 판만** 사무실을 빼는 편이 낫다. 사무실은 매 판 다시 정하니까
|
|
558
|
+
* 자리가 나면 저절로 돌아온다.
|
|
559
|
+
*/
|
|
560
|
+
let 짠것 = 짜기(방);
|
|
561
|
+
const 화면줄수 = process.stdout.rows ?? 0;
|
|
562
|
+
if (방 && 화면줄수 > 0 && 짠것.줄들.length + 1 > 화면줄수) 짠것 = 짜기(null);
|
|
563
|
+
const { 줄들, 커서: 자리 } = 짠것;
|
|
564
|
+
|
|
417
565
|
/*
|
|
418
566
|
* ── 지우지 않고 덮어쓴다. 그리고 한 번에 내보낸다 ────────────────────
|
|
419
567
|
*
|
|
@@ -437,9 +585,33 @@ export class InputBox {
|
|
|
437
585
|
const 윗줄까지 = 이전줄 ? Math.max(0, 이전줄 - 1 - this.커서위) : 0;
|
|
438
586
|
const 남은줄 = Math.max(0, 이전줄 - 줄들.length);
|
|
439
587
|
|
|
588
|
+
/*
|
|
589
|
+
* 바뀐 줄만 보낸다.
|
|
590
|
+
*
|
|
591
|
+
* 줄 수가 그대로일 때만 견준다 — 줄 수가 달라졌으면 지울 줄까지 생겨서
|
|
592
|
+
* 셈이 얽힌다. 그때는 그냥 통째로 그린다. 어차피 드물다(글이 접히거나
|
|
593
|
+
* 추천 목록이 뜨고 지는 순간뿐이고, 그건 애니메이션이 아니다).
|
|
594
|
+
*
|
|
595
|
+
* 안 보낸 줄에서도 '\n' 은 보내야 한다 — 커서를 다음 줄로 내려야 하기
|
|
596
|
+
* 때문이다. 글자 없이 줄만 넘어간다.
|
|
597
|
+
*/
|
|
598
|
+
const 지금때 = Date.now();
|
|
599
|
+
const 통째로 = this.앞줄들 === null
|
|
600
|
+
|| this.앞줄들.length !== 줄들.length
|
|
601
|
+
|| 남은줄 > 0
|
|
602
|
+
|| 지금때 - this.마지막통째로 >= 자가치유주기;
|
|
603
|
+
|
|
440
604
|
let 나갈것 = cursor.숨김 + '\r' + 위로(윗줄까지);
|
|
441
|
-
|
|
442
|
-
|
|
605
|
+
if (통째로) {
|
|
606
|
+
나갈것 += 줄들.map((l) => l + 줄끝지움).join('\n') + '\n';
|
|
607
|
+
for (let i = 0; i < 남은줄; i++) 나갈것 += 줄끝지움 + '\n';
|
|
608
|
+
this.마지막통째로 = 지금때;
|
|
609
|
+
} else {
|
|
610
|
+
for (const [i, l] of 줄들.entries()) {
|
|
611
|
+
if (l !== this.앞줄들[i]) 나갈것 += l + 줄끝지움;
|
|
612
|
+
나갈것 += '\n';
|
|
613
|
+
}
|
|
614
|
+
}
|
|
443
615
|
/*
|
|
444
616
|
* 방금 찍은 마지막 줄 **다음** 줄에 커서가 있다(남은 줄을 지웠으면 그만큼
|
|
445
617
|
* 더 아래). 거기서 되짚어 올라간다.
|
|
@@ -456,5 +628,6 @@ export class InputBox {
|
|
|
456
628
|
|
|
457
629
|
this.그린줄 = 줄들.length;
|
|
458
630
|
this.커서위 = 자리.위;
|
|
631
|
+
this.앞줄들 = 줄들;
|
|
459
632
|
}
|
|
460
633
|
}
|