deel-local-cli 1.0.1 → 1.0.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 +139 -4
- package/README.md +141 -4
- package/package.json +1 -1
- package/src/agent/modes.js +1 -1
- package/src/commands.js +29 -4
- package/src/repl.js +111 -11
- package/src/ui/approve.js +66 -0
- package/src/ui/complete.js +89 -0
- package/src/ui/inputbox.js +129 -4
- package/src/ui/screen.js +58 -8
- package/src/ui/status.js +29 -6
- package/src/ui/working.js +118 -0
package/README.en.md
CHANGED
|
@@ -20,10 +20,12 @@ Zero dependencies · Node 20+ · Exactly one place your source can go
|
|
|
20
20
|
│ Sends to this machine 127.0.0.1:11434 ← nowhere else │
|
|
21
21
|
│ Link streaming · tools · reasoning control │
|
|
22
22
|
│ Folder C:\work\myproject │
|
|
23
|
+
│ Approval ⏵⏵ 자동 승인 — nothing is asked; /undo is the net │
|
|
24
|
+
│ Shift+Tab to change · Tab completes a / command │
|
|
23
25
|
│ This PC 337 skills · 127 commands · 42 plugins │
|
|
24
26
|
╰──────────────────────────────────────────────────────────────╯
|
|
25
27
|
|
|
26
|
-
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱▱▱ 22% 28k/128k ▏ ◎ 종합 · ◇ medium·절약 ·
|
|
28
|
+
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱▱▱ 22% 28k/128k ▏ ◎ 종합 · ◇ medium·절약 · ⏵⏵ 자동
|
|
27
29
|
❯ unify the logging style
|
|
28
30
|
|
|
29
31
|
❊ Grep(console.log)
|
|
@@ -245,6 +247,18 @@ Discovered plugin commands are invoked as `/<plugin>:<name>`, with `$ARGUMENTS`
|
|
|
245
247
|
server or loaded a different model, `/scan save` then `/model` switches over without losing
|
|
246
248
|
the conversation.
|
|
247
249
|
|
|
250
|
+
**Without typing**
|
|
251
|
+
|
|
252
|
+
| Key | What it does |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `Tab` | Completes the `/` command you are typing. Candidates appear under the box as you type |
|
|
255
|
+
| `Shift+Tab` | Approval policy (`⏵⏵ auto` → `⏵ risky only` → `⏸ everything`) |
|
|
256
|
+
| `Ctrl+O` | Work mode (`종합` → `코드` → `계획` → …) |
|
|
257
|
+
| `↑` `↓` | Input history |
|
|
258
|
+
| `Ctrl+C` | Stops the answer in progress; twice on an empty line quits |
|
|
259
|
+
|
|
260
|
+
Korean IME composition, paste, `Ctrl+A/E` and backspace all keep working.
|
|
261
|
+
|
|
248
262
|
### Attaching a file with `@`
|
|
249
263
|
|
|
250
264
|
Write `@` followed by a path and that file is sent along with your message.
|
|
@@ -411,7 +425,7 @@ the bottom.** Only the box is erased and redrawn — nothing above it is touched
|
|
|
411
425
|
|
|
412
426
|
── 4.2s · 3 tools · ↑3,900 ↓180
|
|
413
427
|
|
|
414
|
-
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱ 22% ▏ ◎ 종합 · ◇ medium ·
|
|
428
|
+
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱ 22% ▏ ◎ 종합 · ◇ medium · ⏵⏵ 자동
|
|
415
429
|
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
416
430
|
│ ❯ also shrink the aggregate helpers │
|
|
417
431
|
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
@@ -442,6 +456,127 @@ editor is how you break IME input first.
|
|
|
442
456
|
|
|
443
457
|
---
|
|
444
458
|
|
|
459
|
+
### You don't have to type the whole command
|
|
460
|
+
|
|
461
|
+
There are over thirty commands. The only person who has them memorised is the one who
|
|
462
|
+
wrote them, and even he gets as far as `/mem…` and stops to wonder whether it was `memory`
|
|
463
|
+
or `memo`. If the only recourse at that point is typing `/help` and scanning thirty lines,
|
|
464
|
+
that isn't a command, it's a quiz.
|
|
465
|
+
|
|
466
|
+
So they show up **while you type.**
|
|
467
|
+
|
|
468
|
+
```
|
|
469
|
+
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
470
|
+
│ ❯ /mo │
|
|
471
|
+
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
472
|
+
› /model [이름|list|models] 연결·모델 바꾸기 (이름 일부 · list · models)
|
|
473
|
+
/mode <모드> 승인 정책 — 얼마나 물어보나
|
|
474
|
+
/memory [지우기 <번호>|…] 대화가 끝나도 남는 기억 — 보기·지우기
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
`Tab` fills it in: all the way if only one matches, otherwise **only as far as they all
|
|
478
|
+
agree** (`/mo` + Tab → `/mode`). It doesn't pick one for you, because deleting the wrong
|
|
479
|
+
guess costs more than typing the rest. Commands that take an argument get a trailing space
|
|
480
|
+
so you can keep going.
|
|
481
|
+
|
|
482
|
+
Prefix matches come first and **substring matches follow** — typos cluster in the first
|
|
483
|
+
character, so `/emo` still surfaces `/memory`. The moment you type a space (`/mode auto`)
|
|
484
|
+
the list folds away: the command is already decided.
|
|
485
|
+
|
|
486
|
+
The list sits **below** the box. Putting it inside would push what you are typing upward,
|
|
487
|
+
so you could no longer see it.
|
|
488
|
+
|
|
489
|
+
> No up/down selection. That would have to take over **input history** (up arrow), which
|
|
490
|
+
> is used far more often. What is needed here isn't picking, it's recognising — and once
|
|
491
|
+
> you recognise it, one `Tab` is enough.
|
|
492
|
+
>
|
|
493
|
+
> Commands hidden at the `쉬움` level still surface when you type their prefix. Hidden is
|
|
494
|
+
> not the same as unavailable — someone who knows `/recall` should not be told it doesn't
|
|
495
|
+
> exist because of a display setting.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
### The box stays while it works
|
|
500
|
+
|
|
501
|
+
Local models are slow. A single step can take tens of seconds, and if the bottom of the
|
|
502
|
+
screen goes blank for that long, **people assume it hung and hit Ctrl+C** — losing work that
|
|
503
|
+
was nearly done. So the border stays and only the contents change.
|
|
504
|
+
|
|
505
|
+
```
|
|
506
|
+
◧ Read(집계.py)
|
|
507
|
+
└ 6 lines
|
|
508
|
+
|
|
509
|
+
▏myproject · qwen2.5-coder:7b ▏ ▰▱▱▱▱▱▱▱▱▱ 2% ▏ ◎ 종합 · ◇ medium · ⏵⏵ 자동 ▏ ↑3.8k ↓180
|
|
510
|
+
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
511
|
+
│ ⠹ 파일 들여다보는 중… 12초 · 생각 1,240자 · Ctrl+C 중단 │
|
|
512
|
+
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
The phrase tracks **what is actually happening**. This is not decoration: a message that
|
|
516
|
+
cycles at random stops being read after the second time, and from then on it is worth no
|
|
517
|
+
more than a blank screen. One turn reads like this:
|
|
518
|
+
|
|
519
|
+
```
|
|
520
|
+
머리 굴리는 중 → 파일 들여다보는 중 → 코드 짜는 중 → 답 쓰는 중
|
|
521
|
+
(turning it over) (looking at files) (writing code) (writing the answer)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
| Activity | Phrases |
|
|
525
|
+
|---|---|
|
|
526
|
+
| Thinking | 머리 굴리는 중 · 어떻게 할지 궁리하는 중 · 수 읽는 중 · 따져 보는 중 |
|
|
527
|
+
| `Read` `Grep` `Glob` | 파일 들여다보는 중 · 코드 훑는 중 · 어디 있나 뒤지는 중 · 단서 찾는 중 |
|
|
528
|
+
| `Write` `Edit` `Append` | 코드 짜는 중 · 고쳐 넣는 중 · 손보는 중 · 한 줄씩 옮기는 중 |
|
|
529
|
+
| `Bash` | 명령 돌리는 중 · 터미널 두드리는 중 · 결과 기다리는 중 |
|
|
530
|
+
| `WebFetch` | 문서 찾아보는 중 · 읽어 오는 중 |
|
|
531
|
+
| Answering | 답 쓰는 중 · 정리해서 말하는 중 |
|
|
532
|
+
| **Past 45 seconds** | 아직 하는 중 · 조금만 더 · 생각보다 오래 걸리는 중 |
|
|
533
|
+
|
|
534
|
+
Within a category the phrase advances every 4 seconds — text frozen for 30 seconds reads as
|
|
535
|
+
hung too. On the right: **elapsed time**, and while the model is reasoning, **how many
|
|
536
|
+
characters of thinking have arrived**. One number that genuinely increases is what turns
|
|
537
|
+
"still alive" from a claim into a fact.
|
|
538
|
+
|
|
539
|
+
### What gets asked, and what just happens
|
|
540
|
+
|
|
541
|
+
Whether your files change **with or without being asked** is the one thing that has to be
|
|
542
|
+
readable at a glance. It sits on the right of the status line at all times.
|
|
543
|
+
|
|
544
|
+
| Indicator | Command | What it asks about |
|
|
545
|
+
|---|---|---|
|
|
546
|
+
| `⏵⏵ 자동 승인` (auto) | `/mode auto` | Nothing is asked. `/undo` is the safety net |
|
|
547
|
+
| `⏵ 위험만 확인` (risky only) | `/mode confirm` | Only irreversible commands. Files change unasked |
|
|
548
|
+
| `⏸ 모두 확인` (everything) | `/mode strict` | Every file change and every command is confirmed first |
|
|
549
|
+
|
|
550
|
+
`/mode` on its own lists all three and marks the current one with ●. The startup header
|
|
551
|
+
spells it out in a sentence, so the glyph is enough from then on.
|
|
552
|
+
|
|
553
|
+
**`Shift+Tab` cycles it without typing.** Each press moves one step and leaves a line
|
|
554
|
+
saying where it went. Whatever you were typing stays put.
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
⏵ 위험만 확인 되돌릴 수 없는 명령만 물어봅니다. 파일은 안 묻고 고칩니다
|
|
558
|
+
자동 승인 → 위험만 확인 · Shift+Tab 으로 계속 바꿉니다
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
The cycle runs **loose → strict** (auto → risky only → everything → auto). A mistaken
|
|
562
|
+
press only makes it ask more; it never drops you into "changes files unasked" in one hit.
|
|
563
|
+
|
|
564
|
+
> That key used to cycle the work mode (`종합`, `코드`, …). The swap is about **who
|
|
565
|
+
> reaches for it more often.** Work mode follows your request on its own, while approval
|
|
566
|
+
> policy is what you want to change mid-task when a particular job deserves a look.
|
|
567
|
+
> Work mode moved to `Ctrl+O`; `/work` still does the same thing.
|
|
568
|
+
|
|
569
|
+
> It used to be the bare word `auto`. Next to `종합` and `medium·절약` it looked like just
|
|
570
|
+
> another mode, and nothing on screen said that one of them meant **files change without
|
|
571
|
+
> asking.**
|
|
572
|
+
>
|
|
573
|
+
> That also changed what gets truncated when space runs out. A corporate gateway model name
|
|
574
|
+
> like `databricks-gpt-5-6-luna` eats twenty-three columns, and that alone was pushing the
|
|
575
|
+
> approval indicator off the line entirely. Now **the model name shortens first** — you
|
|
576
|
+
> already know what you are running; whether your files change unasked is what you need now.
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
445
580
|
## Tools
|
|
446
581
|
|
|
447
582
|
Names and arguments match Claude Code, so skills written for that convention work unchanged.
|
|
@@ -1226,7 +1361,7 @@ If the working folder has `DEEL.md`, `CLAUDE.md` or `AGENTS.md`, it is loaded as
|
|
|
1226
1361
|
## Development
|
|
1227
1362
|
|
|
1228
1363
|
```bash
|
|
1229
|
-
npm test Full suite (1,
|
|
1364
|
+
npm test Full suite (1,832 checks)
|
|
1230
1365
|
npm run coverage Which lines the tests actually execute
|
|
1231
1366
|
npm run verify Import + network checks only
|
|
1232
1367
|
npm run bench Edit success rate
|
|
@@ -1280,7 +1415,7 @@ Zero dependencies rules out c8 and nyc, so this reads Node's own
|
|
|
1280
1415
|
`NODE_V8_COVERAGE` instead — nothing new to get through an import review. It picks up
|
|
1281
1416
|
child processes too, so the `cli` suite that spawns `deel` counts like everything else.
|
|
1282
1417
|
|
|
1283
|
-
Currently **92% overall** (
|
|
1418
|
+
Currently **92% overall** (7,056 of 7,646 lines). Three files are deliberately left short.
|
|
1284
1419
|
|
|
1285
1420
|
| File | Now | Why it stops there |
|
|
1286
1421
|
|---|---|---|
|
package/README.md
CHANGED
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
│ 보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다 │
|
|
21
21
|
│ 연결 스트리밍 · 도구 · 추론 조절 │
|
|
22
22
|
│ 폴더 C:\work\myproject │
|
|
23
|
+
│ 승인 ⏵⏵ 자동 승인 — 안 묻고 고칩니다. 되돌리기(/undo)가 안전망입니다 │
|
|
24
|
+
│ Shift+Tab 으로 바꿉니다 · Tab 은 치던 / 명령을 채웁니다 │
|
|
23
25
|
│ 이 PC 스킬 337 · 명령 127 · 플러그인 42 │
|
|
24
26
|
╰──────────────────────────────────────────────────────────────╯
|
|
25
27
|
/help 명령 목록 /think 추론 강도 Ctrl+C 중단·끝내기
|
|
26
28
|
|
|
27
|
-
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱▱▱ 22% 28k/128k ▏ ◎ 종합 · ◇ medium·절약 ·
|
|
29
|
+
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱▱▱ 22% 28k/128k ▏ ◎ 종합 · ◇ medium·절약 · ⏵⏵ 자동
|
|
28
30
|
❯ 로그 형식 통일해줘
|
|
29
31
|
|
|
30
32
|
❊ Grep(console.log)
|
|
@@ -251,6 +253,18 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
|
|
|
251
253
|
`/scan` 과 `/sessions` 는 나가지 않고도 씁니다. 로컬 서버를 새로 켰거나 모델을
|
|
252
254
|
바꿔 올렸을 때 `/scan save` → `/model` 두 번이면 대화를 이어둔 채로 갈아탑니다.
|
|
253
255
|
|
|
256
|
+
**치지 않고 쓰는 것**
|
|
257
|
+
|
|
258
|
+
| 키 | 무엇을 |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `Tab` | 치던 `/` 명령을 채웁니다. 치는 도중에 후보가 상자 아래에 뜹니다 |
|
|
261
|
+
| `Shift+Tab` | 승인 방식 (`⏵⏵ 자동` → `⏵ 위험만` → `⏸ 모두`) |
|
|
262
|
+
| `Ctrl+O` | 작업 모드 (`종합` → `코드` → `계획` → …) |
|
|
263
|
+
| `↑` `↓` | 지난 입력 이력 |
|
|
264
|
+
| `Ctrl+C` | 답하는 중이면 그 답만 끊고, 빈 줄에서 두 번이면 끝냅니다 |
|
|
265
|
+
|
|
266
|
+
한글 조합·붙여넣기·`Ctrl+A/E`·백스페이스는 전부 그대로 됩니다.
|
|
267
|
+
|
|
254
268
|
### `@파일` 로 바로 붙이기
|
|
255
269
|
|
|
256
270
|
말 속에 `@` 뒤로 경로를 쓰면 그 파일을 말과 함께 바로 보냅니다.
|
|
@@ -418,7 +432,7 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
|
|
|
418
432
|
|
|
419
433
|
── 4.2초 · 도구 3회 · ↑3,900 ↓180
|
|
420
434
|
|
|
421
|
-
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱ 22% ▏ ◎ 종합 · ◇ medium ·
|
|
435
|
+
▏myproject · qwen2.5-coder:7b ▏ ▰▰▱▱▱▱▱▱ 22% ▏ ◎ 종합 · ◇ medium · ⏵⏵ 자동
|
|
422
436
|
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
423
437
|
│ ❯ 집계 함수도 줄여줘 │
|
|
424
438
|
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
@@ -449,6 +463,127 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
|
|
|
449
463
|
|
|
450
464
|
---
|
|
451
465
|
|
|
466
|
+
### 명령은 다 안 쳐도 됩니다
|
|
467
|
+
|
|
468
|
+
명령이 서른 개가 넘습니다. 다 외우고 있는 사람은 만든 사람뿐이고, 그마저도
|
|
469
|
+
`/mem…` 까지 치고 "이게 memory 였나 memo 였나" 하고 멈춥니다. 그때 할 수 있는
|
|
470
|
+
것이 `/help` 를 쳐서 서른 줄을 받아 눈으로 훑는 것뿐이면, 그건 명령이 아니라
|
|
471
|
+
시험입니다.
|
|
472
|
+
|
|
473
|
+
그래서 **치는 도중에 보여 줍니다.**
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
477
|
+
│ ❯ /mo │
|
|
478
|
+
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
479
|
+
› /model [이름|list|models] 연결·모델 바꾸기 (이름 일부 · list · models)
|
|
480
|
+
/mode <모드> 승인 정책 — 얼마나 물어보나
|
|
481
|
+
/memory [지우기 <번호>|…] 대화가 끝나도 남는 기억 — 보기·지우기
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
`Tab` 을 누르면 채워집니다. 하나만 맞으면 끝까지, 여럿이면 **다 같이 가진
|
|
485
|
+
앞부분까지만** 채웁니다(`/mo` + Tab → `/mode`). 하나를 골라 박아 넣지 않는
|
|
486
|
+
것은, 사람이 원한 것이 아닐 때 지우는 수고가 더 들기 때문입니다. 인자를 받는
|
|
487
|
+
명령이면 빈칸까지 붙여 바로 이어 칠 수 있게 합니다.
|
|
488
|
+
|
|
489
|
+
앞에서 맞는 것을 먼저 보여 주고, **가운데 맞는 것도 뒤에 붙입니다** — 오타는
|
|
490
|
+
앞글자에서 제일 많이 나므로, `/emo` 라고 잘못 쳐도 `/memory` 가 나옵니다.
|
|
491
|
+
빈칸을 치는 순간(`/mode auto`) 목록은 접힙니다. 명령이 이미 정해졌기 때문입니다.
|
|
492
|
+
|
|
493
|
+
목록은 **상자 아래**에 붙습니다. 안에 넣으면 치던 글이 밀려 올라가서 무엇을
|
|
494
|
+
치고 있었는지가 안 보입니다.
|
|
495
|
+
|
|
496
|
+
> 위아래 화살표로 고르게 하지 않습니다. 그러면 **지난 입력 이력**(위 화살표)을
|
|
497
|
+
> 뺏어야 하는데, 그건 훨씬 자주 쓰는 기능입니다. 여기서 필요한 것은 고르는
|
|
498
|
+
> 것이 아니라 **알아보는 것**이고, 알아보고 나면 Tab 한 번이면 됩니다.
|
|
499
|
+
>
|
|
500
|
+
> `쉬움` 수준에서 감춰 둔 명령도 앞글자를 치면 나옵니다. 감춘 것은 '못 쓰는
|
|
501
|
+
> 것' 이 아닙니다 — `/recall` 을 아는 사람이 수준 설정 때문에 "그런 명령
|
|
502
|
+
> 없다" 는 화면을 보면 안 됩니다.
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### 일하는 중에는 상자가 그대로 있습니다
|
|
507
|
+
|
|
508
|
+
로컬 모델은 느립니다. 한 걸음에 수십 초가 걸리는데 그 동안 화면 아래가 텅 비면
|
|
509
|
+
**멈춘 줄 알고 Ctrl+C 를 누르게 됩니다** — 다 되어 가던 일이 그렇게 날아갑니다.
|
|
510
|
+
그래서 테두리는 그대로 두고 안엣것만 바꿉니다.
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
◧ Read(집계.py)
|
|
514
|
+
└ 6줄
|
|
515
|
+
|
|
516
|
+
▏myproject · qwen2.5-coder:7b ▏ ▰▱▱▱▱▱▱▱▱▱ 2% ▏ ◎ 종합 · ◇ medium · ⏵⏵ 자동 ▏ ↑3.8k ↓180
|
|
517
|
+
╭─────────────────────────────────────────────────────────────────────────────╮
|
|
518
|
+
│ ⠹ 파일 들여다보는 중… 12초 · 생각 1,240자 · Ctrl+C 중단 │
|
|
519
|
+
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
문구는 **지금 진짜로 하는 일**을 따라갑니다. 재미로 넣은 것이 아닙니다 —
|
|
523
|
+
아무 말이나 돌려 대면 두 번째부터 아무도 안 읽고, 그때부터는 화면이 조용한 것과
|
|
524
|
+
같아집니다. 한 턴을 돌리면 이렇게 지나갑니다:
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
머리 굴리는 중 → 파일 들여다보는 중 → 코드 짜는 중 → 답 쓰는 중
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
| 하는 일 | 문구 |
|
|
531
|
+
|---|---|
|
|
532
|
+
| 생각 | 머리 굴리는 중 · 어떻게 할지 궁리하는 중 · 수 읽는 중 · 따져 보는 중 |
|
|
533
|
+
| `Read` `Grep` `Glob` | 파일 들여다보는 중 · 코드 훑는 중 · 어디 있나 뒤지는 중 · 단서 찾는 중 |
|
|
534
|
+
| `Write` `Edit` `Append` | 코드 짜는 중 · 고쳐 넣는 중 · 손보는 중 · 한 줄씩 옮기는 중 |
|
|
535
|
+
| `Bash` | 명령 돌리는 중 · 터미널 두드리는 중 · 결과 기다리는 중 |
|
|
536
|
+
| `WebFetch` | 문서 찾아보는 중 · 읽어 오는 중 |
|
|
537
|
+
| 답하는 중 | 답 쓰는 중 · 정리해서 말하는 중 |
|
|
538
|
+
| **45초가 넘으면** | 아직 하는 중 · 조금만 더 · 생각보다 오래 걸리는 중 |
|
|
539
|
+
|
|
540
|
+
같은 갈래 안에서도 4초마다 다른 문구로 넘어갑니다. 같은 글자가 30초 동안 그대로
|
|
541
|
+
있으면 그것도 멈춘 것처럼 보이기 때문입니다. 오른쪽에는 **걸린 시간**과, 모델이
|
|
542
|
+
추론 중이면 **지금까지 생각한 글자 수**가 같이 뜹니다 — 진짜로 늘어나는 숫자가
|
|
543
|
+
하나 있어야 '살아 있다' 가 표시가 아니라 사실이 됩니다.
|
|
544
|
+
|
|
545
|
+
### 무엇을 물어보고 무엇을 그냥 할지
|
|
546
|
+
|
|
547
|
+
내 파일이 **물어보고 바뀌는지, 안 물어보고 바뀌는지**는 흘깃 봐서 바로 알아야
|
|
548
|
+
하는 한 가지입니다. 상태줄 오른쪽에 늘 떠 있습니다.
|
|
549
|
+
|
|
550
|
+
| 표시 | 명령 | 무엇을 물어보나 |
|
|
551
|
+
|---|---|---|
|
|
552
|
+
| `⏵⏵ 자동 승인` | `/mode auto` | 안 묻고 고칩니다. 되돌리기(`/undo`)가 안전망입니다 |
|
|
553
|
+
| `⏵ 위험만 확인` | `/mode confirm` | 되돌릴 수 없는 명령만 물어봅니다. 파일은 안 묻고 고칩니다 |
|
|
554
|
+
| `⏸ 모두 확인` | `/mode strict` | 파일을 고치거나 명령을 돌리기 전에 매번 물어봅니다 |
|
|
555
|
+
|
|
556
|
+
`/mode` 만 치면 셋을 나란히 보여 주고 지금 것에 ● 를 찍습니다.
|
|
557
|
+
켤 때 머리말에도 사람 말로 한 줄 나옵니다 — 한 번 읽고 나면 그 다음부터는
|
|
558
|
+
글자만 봐도 압니다.
|
|
559
|
+
|
|
560
|
+
**`Shift+Tab` 으로 치지 않고 바꿉니다.** 누를 때마다 차례로 돌고, 무엇에서
|
|
561
|
+
무엇으로 갔는지 한 줄이 남습니다. 치던 글은 그대로 있습니다.
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
⏵ 위험만 확인 되돌릴 수 없는 명령만 물어봅니다. 파일은 안 묻고 고칩니다
|
|
565
|
+
자동 승인 → 위험만 확인 · Shift+Tab 으로 계속 바꿉니다
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
차례는 **느슨한 쪽 → 조이는 쪽**입니다(자동 → 위험만 → 모두 → 자동). 잘못
|
|
569
|
+
눌러도 더 물어보게 될 뿐, 한 번에 '안 묻고 고침' 으로 떨어지지 않습니다.
|
|
570
|
+
|
|
571
|
+
> 이 자리는 원래 작업 모드(`종합`·`코드`…)가 쓰고 있었습니다. 바꾼 이유는
|
|
572
|
+
> **누가 더 자주 손대느냐**입니다. 작업 모드는 요청을 보고 저절로 옮겨 가지만,
|
|
573
|
+
> 승인 방식은 "이번 건은 좀 봐야겠다" 싶을 때 일하는 도중에 바꾸고 싶어집니다.
|
|
574
|
+
> 작업 모드는 `Ctrl+O` 로 옮겼고 `/work` 도 그대로 됩니다.
|
|
575
|
+
|
|
576
|
+
> 전에는 `auto` 라는 영문 한 낱말이었습니다. 옆에 `종합`·`medium·절약` 이 나란히
|
|
577
|
+
> 있으니 셋 다 그냥 '모드' 로 보였고, 그중 하나가 **묻지 않고 파일을 고친다**는
|
|
578
|
+
> 뜻이라는 것은 화면 어디에도 없었습니다.
|
|
579
|
+
>
|
|
580
|
+
> 자리가 모자랄 때 무엇을 접는지도 그래서 바꿨습니다. 사내 게이트웨이 모델 이름은
|
|
581
|
+
> `databricks-gpt-5-6-luna` 처럼 스물세 칸을 그냥 먹는데, 그것 하나 때문에 승인
|
|
582
|
+
> 방식이 화면에서 통째로 사라지고 있었습니다. 지금은 **모델 이름을 먼저 줄입니다** —
|
|
583
|
+
> 무엇으로 도는지는 이미 알지만, 내 파일이 안 물어보고 바뀌는지는 지금 봐야 압니다.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
452
587
|
## 도구
|
|
453
588
|
|
|
454
589
|
이름과 인자를 Claude Code 와 같게 맞췄습니다. 그 관례로 쓰인 스킬·명령이 그대로 먹습니다.
|
|
@@ -1248,7 +1383,7 @@ deel --no-tui 입력 상자를 끄고 줄 화면으로 (아래 참고
|
|
|
1248
1383
|
## 개발
|
|
1249
1384
|
|
|
1250
1385
|
```bash
|
|
1251
|
-
npm test 전체 검증 (1,
|
|
1386
|
+
npm test 전체 검증 (1,832항목)
|
|
1252
1387
|
npm run coverage 검사가 소스의 어디를 밟았는지
|
|
1253
1388
|
npm run verify 반입·통신 검증만
|
|
1254
1389
|
npm run bench 편집 성공률 측정
|
|
@@ -1315,7 +1450,7 @@ node test/coverage.mjs --json 기계가 읽을 형태로
|
|
|
1315
1450
|
`NODE_V8_COVERAGE` 를 읽습니다 — 새로 반입 심사할 것이 하나도 안 늡니다.
|
|
1316
1451
|
자식 프로세스까지 잡히므로 `deel` 을 띄워 보는 `cli` 검사도 그대로 집계됩니다.
|
|
1317
1452
|
|
|
1318
|
-
지금 **전체 92%** (7,
|
|
1453
|
+
지금 **전체 92%** (7,646줄 중 7,056줄). 일부러 못 채운 곳이 셋 있습니다.
|
|
1319
1454
|
|
|
1320
1455
|
| 파일 | 지금 | 왜 못 채우나 |
|
|
1321
1456
|
|---|---|---|
|
|
@@ -1331,6 +1466,8 @@ src/
|
|
|
1331
1466
|
ui/ 색·한글 폭·상자·상태줄·입력
|
|
1332
1467
|
ui/screen.js 화면 고르기 (줄 화면 / 상자 화면)
|
|
1333
1468
|
ui/inputbox.js 맨 아래 입력 상자 — 그리기·지우기·커서 자리
|
|
1469
|
+
ui/working.js 일하는 중 문구 — 지금 하는 일을 따라간다
|
|
1470
|
+
ui/approve.js 승인 방식 표시 (자동/위험만/모두)
|
|
1334
1471
|
ui/wrap.js 색을 지키며 폭에 맞춰 접기
|
|
1335
1472
|
agent/loop.js 에이전트 루프
|
|
1336
1473
|
agent/session.js 대화 상태 + 컨텍스트 셈
|
package/package.json
CHANGED
package/src/agent/modes.js
CHANGED
|
@@ -236,7 +236,7 @@ export function get(id) {
|
|
|
236
236
|
return MODES[normalize(id) ?? DEFAULT];
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
/**
|
|
239
|
+
/** Ctrl+O 로 돌릴 때 다음 모드. (Shift+Tab 은 승인 방식이 가져갔다) */
|
|
240
240
|
export function next(id) {
|
|
241
241
|
const i = ORDER.indexOf(normalize(id) ?? DEFAULT);
|
|
242
242
|
return ORDER[(i + 1) % ORDER.length];
|
package/src/commands.js
CHANGED
|
@@ -199,15 +199,40 @@ export async function handle(line, session, ctx) {
|
|
|
199
199
|
return { handled: true };
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/*
|
|
203
|
+
* /mode — 무엇을 물어보고 무엇을 그냥 할지.
|
|
204
|
+
*
|
|
205
|
+
* 목록을 그릴 때 **지금 것을 표시로 찍는다.** 전에는 '지금 auto' 라고 한 줄
|
|
206
|
+
* 적고 아래에 셋을 나란히 늘어놨는데, 그러면 어느 것이 켜져 있는지 두 군데를
|
|
207
|
+
* 견줘 봐야 안다. 내 파일이 물어보고 바뀌는지 아닌지는 흘깃 봐서 알아야 한다.
|
|
208
|
+
*/
|
|
202
209
|
case 'mode': {
|
|
210
|
+
const { 승인, 차례: 승인차례, 표시: 승인표시 } = await import('./ui/approve.js');
|
|
203
211
|
if (!MODES[arg]) {
|
|
204
|
-
|
|
205
|
-
for (const
|
|
212
|
+
rule('승인 방식 — 무엇을 물어볼까', 70);
|
|
213
|
+
for (const k of 승인차례) {
|
|
214
|
+
const 지금 = k === session.mode;
|
|
215
|
+
const m = 승인[k];
|
|
216
|
+
say(` ${지금 ? c.hgreen('●') : c.gray('○')} ${승인표시(k)}${c.gray(pad('', Math.max(1, 14 - width(m.이름))))}`
|
|
217
|
+
+ `${c.gray(pad('/mode ' + k, 15))} ${지금 ? c.white(m.한줄) : c.gray(m.한줄)}`);
|
|
218
|
+
}
|
|
219
|
+
say('');
|
|
220
|
+
say(` ${c.gray('지금은')} ${승인표시(session.mode)} ${c.gray('입니다. 상태줄 오른쪽에도 늘 떠 있습니다.')}`);
|
|
221
|
+
say(` ${c.gray('치지 않고 바꾸려면')} ${c.cyan('Shift+Tab')} ${c.gray('— 누를 때마다 차례로 돕니다.')}`);
|
|
206
222
|
say('');
|
|
207
223
|
return { handled: true };
|
|
208
224
|
}
|
|
225
|
+
const 앞 = session.mode;
|
|
209
226
|
session.mode = arg;
|
|
210
|
-
|
|
227
|
+
const m = 승인[arg];
|
|
228
|
+
say('');
|
|
229
|
+
say(` ${mark.ok} ${승인표시(앞)} ${c.gray('→')} ${승인표시(arg)}`);
|
|
230
|
+
say(` ${c.gray(m.한줄)}`);
|
|
231
|
+
// 안 묻는 쪽으로 옮길 때만 안전망을 짚어 준다. 반대로 갈 때는 안 짚는다 —
|
|
232
|
+
// 조심하는 쪽으로 가는 사람에게 경고를 붙일 이유가 없다.
|
|
233
|
+
if (arg === 'auto' && 앞 !== 'auto') {
|
|
234
|
+
say(` ${c.gray('되돌리려면')} ${c.cyan('/undo')}${c.gray(', 무엇이 바뀌었는지는')} ${c.cyan('/diff')}`);
|
|
235
|
+
}
|
|
211
236
|
say('');
|
|
212
237
|
return { handled: true };
|
|
213
238
|
}
|
|
@@ -1386,7 +1411,7 @@ function showWork(session) {
|
|
|
1386
1411
|
say(` ${canWrite(k) ? c.gray('파일 바꿈') : c.green('읽기만')}${c.gray(' · 생각 ' + (w.think ?? '그대로') + '·' + w.effort + ' · 최대 ' + w.steps + '걸음')}`);
|
|
1387
1412
|
}
|
|
1388
1413
|
say('');
|
|
1389
|
-
say(` ${c.gray('바꾸려면')} ${c.cyan('/plan')} ${c.cyan('/code')} ${c.cyan('/debug')} ${c.gray('… 또는')} ${c.cyan('
|
|
1414
|
+
say(` ${c.gray('바꾸려면')} ${c.cyan('/plan')} ${c.cyan('/code')} ${c.cyan('/debug')} ${c.gray('… 또는')} ${c.cyan('Ctrl+O')} ${c.gray('로 차례로')}`);
|
|
1390
1415
|
say(` ${c.gray('이건 승인 정책(')}${c.cyan('/mode')}${c.gray(')과 다른 축입니다 — 무엇을 하느냐 / 얼마나 물어보냐.')}`);
|
|
1391
1416
|
say('');
|
|
1392
1417
|
}
|
package/src/repl.js
CHANGED
|
@@ -6,7 +6,7 @@ import { c, say as 바로쓰기, mark, clip } from './ui/ansi.js';
|
|
|
6
6
|
import { headerLines } from './ui/status.js';
|
|
7
7
|
import { 화면고르기 } from './ui/screen.js';
|
|
8
8
|
import { STAGES } from './agent/effort.js';
|
|
9
|
-
import { handle } from './commands.js';
|
|
9
|
+
import { handle, COMMANDS } from './commands.js';
|
|
10
10
|
import { next as nextWork, get as getWork, canWrite } from './agent/modes.js';
|
|
11
11
|
import { route } from './agent/route.js';
|
|
12
12
|
import { run } from './agent/loop.js';
|
|
@@ -19,12 +19,15 @@ import { discover } from './skills/discover.js';
|
|
|
19
19
|
import { allowEndpoint, setOffline, isOffline, isLocalHost } from './safety/network.js';
|
|
20
20
|
import { Store, latest, prune } from './agent/store.js';
|
|
21
21
|
import { askHidden } from './ui/prompt.js';
|
|
22
|
-
import { explain } from './ui/level.js';
|
|
22
|
+
import { explain, shows as levelShows } from './ui/level.js';
|
|
23
|
+
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
24
|
+
import { 추천, 채울글 } from './ui/complete.js';
|
|
23
25
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
24
26
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
25
27
|
import { expand as expandMentions } from './agent/mention.js';
|
|
26
28
|
import { 다붙이기 } from './backend/mcp.js';
|
|
27
29
|
import { 프롬프트토막 as 기억토막, 읽기 as 기억읽기 } from './agent/memory.js';
|
|
30
|
+
import { 갈래고르기 } from './ui/working.js';
|
|
28
31
|
|
|
29
32
|
// 도구마다 눈에 띄는 글자를 다르게 준다. 훑을 때 종류가 먼저 보인다.
|
|
30
33
|
const TOOL_GLYPH = {
|
|
@@ -195,6 +198,22 @@ export async function chatLoop(opts = {}) {
|
|
|
195
198
|
output: 상자쓰나 ? 먹통 : process.stdout,
|
|
196
199
|
terminal: 상자쓰나 ? true : undefined,
|
|
197
200
|
historySize: 200,
|
|
201
|
+
/*
|
|
202
|
+
* 빈 완성기. 아무것도 안 내놓지만 **달아 둬야** 한다.
|
|
203
|
+
*
|
|
204
|
+
* 네 가지를 실제로 눌러 보고 정했다.
|
|
205
|
+
*
|
|
206
|
+
* 완성기 없음 Tab → 줄에 리터럴 탭이 박힌다 (`/hel` + Tab → `/hel\t`).
|
|
207
|
+
* 그 글이 그대로 모델에게 간다. 검사 6개가 여기서 빨개진다.
|
|
208
|
+
* 진짜 완성기 readline 이 자기 방식으로 줄을 고쳐 버린다. 우리가 그린
|
|
209
|
+
* 상자와 어긋나고, 후보가 있는 줄에서 Shift+Tab 을 누르면
|
|
210
|
+
* 승인 방식만 바뀌어야 하는데 글까지 바뀐다.
|
|
211
|
+
* 빈 완성기 Tab 도 Shift+Tab 도 줄을 안 건드린다. ← 이것
|
|
212
|
+
*
|
|
213
|
+
* 그래서 readline 에게서는 '줄을 안 건드림' 만 받고, 무엇을 채울지는 우리가
|
|
214
|
+
* rl.write 로 직접 정한다. rl.write 는 공개 API 라 한글도 안 깨진다.
|
|
215
|
+
*/
|
|
216
|
+
completer: 상자쓰나 ? (line) => [[], line] : undefined,
|
|
198
217
|
});
|
|
199
218
|
|
|
200
219
|
// 입력을 큐로 받는다. rl.question 을 겹쳐 쓰면 파이프로 넣을 때 닫혀 버린다.
|
|
@@ -224,14 +243,55 @@ export async function chatLoop(opts = {}) {
|
|
|
224
243
|
if (waiter) { const w = waiter; waiter = null; w(null); }
|
|
225
244
|
});
|
|
226
245
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
246
|
+
/*
|
|
247
|
+
* 지금 치고 있는 글에 맞는 명령들.
|
|
248
|
+
*
|
|
249
|
+
* 수준에 따라 감춘 명령이 있다(쉬움에서는 자주 쓰는 것만 보인다). 그런데
|
|
250
|
+
* **감춘 것이 못 쓰는 것은 아니다** — 치면 그대로 돌아간다. 그래서 보이는
|
|
251
|
+
* 것 중에 맞는 게 없으면 감춘 것까지 뒤진다. `/recall` 을 아는 사람이
|
|
252
|
+
* 쉬움 수준이라는 이유로 "그런 명령 없다" 는 화면을 보면 안 된다.
|
|
253
|
+
*/
|
|
254
|
+
const 지금추천 = (글) => {
|
|
255
|
+
const 보이는것 = Object.keys(COMMANDS).filter((n) => n !== 'quit' && levelShows(session.level, n));
|
|
256
|
+
const 것 = 추천(글, COMMANDS, 보이는것);
|
|
257
|
+
if (것.length) return 것;
|
|
258
|
+
return 추천(글, COMMANDS, Object.keys(COMMANDS).filter((n) => n !== 'quit'));
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/*
|
|
262
|
+
* 키를 가로챈다 — 터미널일 때만.
|
|
263
|
+
*
|
|
264
|
+
* 파이프로 넣을 때 가로채면 입력이 깨진다. 검사와 데모가 그렇게 돌아간다.
|
|
265
|
+
*
|
|
266
|
+
* ── Shift+Tab 은 무엇을 돌려야 하나 ──────────────────────────────────
|
|
267
|
+
*
|
|
268
|
+
* 전에는 **작업 모드**(종합/코드/계획…)를 돌렸다. 바꾼다. Shift+Tab 은
|
|
269
|
+
* **승인 방식**을 돌린다 — 안 묻고 고칠지, 매번 물을지.
|
|
270
|
+
*
|
|
271
|
+
* 두 가지가 이 자리를 놓고 다퉜는데, 자주 눌러야 하는 쪽이 이겨야 한다.
|
|
272
|
+
* 작업 모드는 요청을 보고 저절로 옮겨 가므로 사람이 손댈 일이 드물다.
|
|
273
|
+
* 반면 승인 방식은 "이번 건 좀 봐야겠다" 싶을 때 **일하는 도중에** 바꾸고
|
|
274
|
+
* 싶어진다. 그리고 이건 안전 설정이라, 손이 기억하는 자리에 있어야 한다.
|
|
275
|
+
* 다른 도구(Claude Code)도 같은 키에 같은 것을 둔다.
|
|
276
|
+
*
|
|
277
|
+
* 작업 모드는 Ctrl+O 로 옮겼다. `/work` 도 그대로 된다.
|
|
278
|
+
*/
|
|
231
279
|
if (process.stdin.isTTY) {
|
|
232
280
|
emitKeypressEvents(process.stdin, rl);
|
|
233
281
|
process.stdin.on('keypress', (_ch, key) => {
|
|
282
|
+
// Shift+Tab — 승인 방식 (자동 → 위험만 → 모두)
|
|
234
283
|
if (key && key.name === 'tab' && key.shift) {
|
|
284
|
+
const 앞 = 승인고르기(session.mode);
|
|
285
|
+
session.mode = 승인다음(session.mode);
|
|
286
|
+
const 뒤 = 승인고르기(session.mode);
|
|
287
|
+
화면.입력지움();
|
|
288
|
+
say(` ${뒤.색(뒤.글자)} ${c.bold(뒤.색(뒤.이름))} ${c.gray(뒤.한줄)}`);
|
|
289
|
+
say(` ${c.gray(`${앞.이름} → ${뒤.이름} · Shift+Tab 으로 계속 바꿉니다`)}`);
|
|
290
|
+
prompt();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Ctrl+O — 작업 모드 (종합/코드/계획/설계/디버그/묻기/총괄)
|
|
294
|
+
if (key && key.ctrl && key.name === 'o') {
|
|
235
295
|
session.work = nextWork(session.work);
|
|
236
296
|
const w = getWork(session.work);
|
|
237
297
|
화면.입력지움();
|
|
@@ -240,6 +300,20 @@ export async function chatLoop(opts = {}) {
|
|
|
240
300
|
prompt();
|
|
241
301
|
return;
|
|
242
302
|
}
|
|
303
|
+
/*
|
|
304
|
+
* Tab — 치던 슬래시 명령을 채운다.
|
|
305
|
+
*
|
|
306
|
+
* 하나만 맞으면 끝까지, 여럿이면 다 같이 가진 앞부분까지. 목록에서
|
|
307
|
+
* 위아래로 고르게 하지 않는다 — 그러면 지난 입력 이력(위 화살표)을
|
|
308
|
+
* 뺏어야 하는데, 그건 훨씬 자주 쓰는 기능이다.
|
|
309
|
+
*/
|
|
310
|
+
if (key && key.name === 'tab' && !key.shift && 상자쓰나 && 입력기다림 && 묻는중 === null) {
|
|
311
|
+
const 채울 = 채울글(rl.line ?? '', 지금추천(rl.line ?? ''));
|
|
312
|
+
if (채울) rl.write(채울);
|
|
313
|
+
// 채울 게 없어도 그리기는 한다 — 후보 목록이 그대로 남아 있어야 한다.
|
|
314
|
+
화면.입력갱신(session, rl.line ?? '', rl.cursor ?? 0, 지금추천(rl.line ?? ''));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
243
317
|
/*
|
|
244
318
|
* 친 것을 상자 안에 그린다.
|
|
245
319
|
*
|
|
@@ -270,7 +344,7 @@ export async function chatLoop(opts = {}) {
|
|
|
270
344
|
그릴예정 = true;
|
|
271
345
|
setImmediate(() => {
|
|
272
346
|
그릴예정 = false;
|
|
273
|
-
if (입력기다림) 화면.입력갱신(session, rl.line ?? '', rl.cursor ?? 0);
|
|
347
|
+
if (입력기다림) 화면.입력갱신(session, rl.line ?? '', rl.cursor ?? 0, 지금추천(rl.line ?? ''));
|
|
274
348
|
});
|
|
275
349
|
});
|
|
276
350
|
}
|
|
@@ -437,9 +511,19 @@ export async function chatLoop(opts = {}) {
|
|
|
437
511
|
for (const w of warn) say(` ${mark.warn} ${c.gray(w)}`);
|
|
438
512
|
say(` ${c.gray('/help 명령 목록')} ${c.gray('/think 추론 강도')} ${c.gray('Ctrl+C 중단·끝내기')}`);
|
|
439
513
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
514
|
+
/*
|
|
515
|
+
* 입력 자리. 어떻게 생겼는지는 화면 쪽이 정한다 —
|
|
516
|
+
* 줄화면은 상태줄을 깔고 그 아래 ❯ 를, 상자화면은 테두리를 두른 칸을 그린다.
|
|
517
|
+
*
|
|
518
|
+
* 치던 글은 되살린다. Shift+Tab 이나 Ctrl+C 처럼 **입력 도중에** 한 줄을
|
|
519
|
+
* 끼워 넣고 다시 그리는 자리가 있는데, 그때 빈 칸을 그리면 치던 글이
|
|
520
|
+
* 사라진 것처럼 보인다. 실제로는 readline 이 그대로 들고 있어서 Enter 를
|
|
521
|
+
* 치면 멀쩡히 보내진다 — 화면만 거짓말을 하는 셈이라 더 나쁘다.
|
|
522
|
+
*/
|
|
523
|
+
const prompt = () => {
|
|
524
|
+
const 글 = 상자쓰나 && 입력기다림 ? (rl.line ?? '') : '';
|
|
525
|
+
화면.입력자리(session, 글, 글 ? (rl.cursor ?? 0) : 0, 글 ? 지금추천(글) : []);
|
|
526
|
+
};
|
|
443
527
|
|
|
444
528
|
// Ctrl+C 는 상황에 따라 뜻이 다르다.
|
|
445
529
|
// 모델이 답하는 중 → 그 답을 끊는다 (프로그램은 살아 있다)
|
|
@@ -537,6 +621,14 @@ export async function chatLoop(opts = {}) {
|
|
|
537
621
|
const 꼬리표 = (ev) => { const t = stageTag(ev, session.level); return t ? t + ' ' : ''; };
|
|
538
622
|
|
|
539
623
|
turn = new AbortController();
|
|
624
|
+
/*
|
|
625
|
+
* 상자를 '일하는 중' 으로 바꾼다.
|
|
626
|
+
*
|
|
627
|
+
* 로컬 모델은 느리다 — 한 걸음에 수십 초가 걸린다. 그 동안 화면 아래가
|
|
628
|
+
* 텅 비어 있으면 사람은 멈춘 줄 알고 Ctrl+C 를 누른다. 다 되어 가던 일이
|
|
629
|
+
* 그렇게 날아간다. 테두리를 그대로 두고 안엣것만 바꾸는 이유다.
|
|
630
|
+
*/
|
|
631
|
+
화면.일시작(session, '생각');
|
|
540
632
|
try {
|
|
541
633
|
for await (const ev of run(session, ctx, 보낼글, { signal: turn.signal })) {
|
|
542
634
|
switch (ev.type) {
|
|
@@ -583,13 +675,16 @@ export async function chatLoop(opts = {}) {
|
|
|
583
675
|
*/
|
|
584
676
|
case 'content':
|
|
585
677
|
clearThinking();
|
|
586
|
-
if (!streamed) { streamed = true; 화면.붙임(` ${답표시} `); }
|
|
678
|
+
if (!streamed) { streamed = true; 화면.일바꿈('답'); 화면.붙임(` ${답표시} `); }
|
|
587
679
|
화면.붙임(ev.text.replace(/\n/g, `\n ${답표시} `));
|
|
588
680
|
break;
|
|
589
681
|
|
|
590
682
|
case 'tool_start':
|
|
591
683
|
clearThinking();
|
|
592
684
|
if (streamed) { say(''); streamed = false; }
|
|
685
|
+
// 문구를 지금 하는 일에 맞춘다. 아무 말이나 돌려 대면 두 번째부터
|
|
686
|
+
// 아무도 안 읽고, 그때부터는 화면이 조용한 것과 같아진다.
|
|
687
|
+
화면.일바꿈(갈래고르기(ev.name));
|
|
593
688
|
say('');
|
|
594
689
|
say(` ${toolLabel(ev.name, ev.args)}`);
|
|
595
690
|
break;
|
|
@@ -598,6 +693,7 @@ export async function chatLoop(opts = {}) {
|
|
|
598
693
|
case 'tools_start':
|
|
599
694
|
clearThinking();
|
|
600
695
|
if (streamed) { say(''); streamed = false; }
|
|
696
|
+
화면.일바꿈(갈래고르기(ev.names?.[0]));
|
|
601
697
|
say('');
|
|
602
698
|
say(` ${c.gray(`${ev.count}개를 함께 돌립니다`)} ${c.gray('·')} ${c.gray(ev.names.join(' '))}`);
|
|
603
699
|
break;
|
|
@@ -648,6 +744,7 @@ export async function chatLoop(opts = {}) {
|
|
|
648
744
|
* (Ctrl+C 로 멈출 수도 있다 — 그건 compact.js 쪽에서 받는다.)
|
|
649
745
|
*/
|
|
650
746
|
case 'compacting':
|
|
747
|
+
화면.일바꿈('접기');
|
|
651
748
|
clearThinking();
|
|
652
749
|
화면.돌리기('컨텍스트가 찼습니다 — 앞선 대화를 요약해 접는 중…');
|
|
653
750
|
접는중 = true;
|
|
@@ -745,6 +842,9 @@ export async function chatLoop(opts = {}) {
|
|
|
745
842
|
오류보이기(err.message);
|
|
746
843
|
}
|
|
747
844
|
접기멈춤();
|
|
845
|
+
// 어떻게 끝났든 일하는 표시는 반드시 걷는다. 오류로 빠져나온 길에서
|
|
846
|
+
// 안 걷으면 돌아가는 표시가 화면에 붙박이로 남고, 시계도 계속 돈다.
|
|
847
|
+
화면.일끝();
|
|
748
848
|
turn = null;
|
|
749
849
|
interrupted = false; // 중단은 '끝내기' 의사가 아니다. 종료 카운트를 되돌린다.
|
|
750
850
|
flush(); // 오류로 끝났어도 여기까지는 남긴다
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 승인 방식을 사람 말로.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 따로 두나 ────────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 상태줄에 `auto` 라고만 떠 있었다. 그 옆에는 `종합` 과 `medium·절약` 이 나란히
|
|
7
|
+
* 있어서, 셋 다 그냥 '모드' 처럼 보였다. 그중 하나가 **묻지 않고 파일을 고친다**
|
|
8
|
+
* 는 뜻이라는 것은 화면 어디에도 없었다.
|
|
9
|
+
*
|
|
10
|
+
* 이건 꾸미기가 아니라 안전 표시다. 지금 이 순간 내 파일이 물어보고 바뀌는지
|
|
11
|
+
* 안 물어보고 바뀌는지는, 화면을 흘깃 봐서 **바로** 알아야 하는 한 가지다.
|
|
12
|
+
*
|
|
13
|
+
* 그래서 세 가지를 준다 — 눈에 띄는 글자, 사람 말 이름, 무엇을 묻는지 한 줄.
|
|
14
|
+
*/
|
|
15
|
+
import { c } from './ansi.js';
|
|
16
|
+
|
|
17
|
+
export const 승인 = {
|
|
18
|
+
auto: {
|
|
19
|
+
글자: '⏵⏵',
|
|
20
|
+
이름: '자동 승인',
|
|
21
|
+
짧은이름: '자동',
|
|
22
|
+
한줄: '안 묻고 고칩니다. 되돌리기(/undo)가 안전망입니다',
|
|
23
|
+
색: c.hyellow,
|
|
24
|
+
},
|
|
25
|
+
confirm: {
|
|
26
|
+
글자: '⏵',
|
|
27
|
+
이름: '위험만 확인',
|
|
28
|
+
짧은이름: '위험만',
|
|
29
|
+
한줄: '되돌릴 수 없는 명령만 물어봅니다. 파일은 안 묻고 고칩니다',
|
|
30
|
+
색: c.white,
|
|
31
|
+
},
|
|
32
|
+
strict: {
|
|
33
|
+
글자: '⏸',
|
|
34
|
+
이름: '모두 확인',
|
|
35
|
+
짧은이름: '모두',
|
|
36
|
+
한줄: '파일을 고치거나 명령을 돌리기 전에 매번 물어봅니다',
|
|
37
|
+
색: c.hgreen,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const 차례 = ['auto', 'confirm', 'strict'];
|
|
42
|
+
export const 기본 = 'auto';
|
|
43
|
+
|
|
44
|
+
export function 고르기(이름) {
|
|
45
|
+
return 승인[이름] ?? 승인[기본];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 다음 승인 방식. Shift+Tab 이 이걸로 돈다.
|
|
50
|
+
*
|
|
51
|
+
* 자동 → 위험만 → 모두 → 자동. **느슨한 쪽에서 조이는 쪽으로** 도는 차례다 —
|
|
52
|
+
* 한 번 누를 때마다 더 많이 물어보게 되니, 잘못 눌러도 위험해지지 않는다.
|
|
53
|
+
* 반대로 돌면 Shift+Tab 한 번에 '안 묻고 고침' 으로 떨어진다.
|
|
54
|
+
*/
|
|
55
|
+
export function 다음(이름) {
|
|
56
|
+
// 모르는 값이면 화면에 떠 있는 것(고르기가 내주는 기본)을 기준으로 삼는다.
|
|
57
|
+
// 안 그러면 보이는 것과 한 칸 어긋나서, 눌러도 안 바뀐 것처럼 보인다.
|
|
58
|
+
const i = 차례.indexOf(승인[이름] ? 이름 : 기본);
|
|
59
|
+
return 차례[(i + 1) % 차례.length];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 상태줄에 넣을 조각. 짧게=true 면 좁은 창용. */
|
|
63
|
+
export function 표시(이름, { 짧게 = false } = {}) {
|
|
64
|
+
const m = 고르기(이름);
|
|
65
|
+
return `${m.색(m.글자)} ${m.색(짧게 ? m.짧은이름 : m.이름)}`;
|
|
66
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 슬래시 명령 자동완성.
|
|
3
|
+
*
|
|
4
|
+
* ── 무엇을 푸는가 ───────────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 명령이 서른 개가 넘는다. 다 외우고 있는 사람은 만든 사람뿐이고, 그마저도
|
|
7
|
+
* `/mem…` 까지 치고 "이게 memory 였나 memo 였나" 하고 멈춘다. 그때 할 수 있는
|
|
8
|
+
* 것이 `/help` 를 쳐서 서른 줄을 받아 눈으로 훑는 것뿐이면, 그건 명령이 아니라
|
|
9
|
+
* 시험이다.
|
|
10
|
+
*
|
|
11
|
+
* 그래서 치는 도중에 보여 준다. 고르는 것이 아니라 **알아보는 것**이 목적이라
|
|
12
|
+
* 위아래 화살표로 고르게 하지 않는다 — 그러면 지난 입력 이력(위 화살표)을
|
|
13
|
+
* 뺏어야 하고, 그건 훨씬 자주 쓰는 기능이다. Tab 한 번이면 채워진다.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// 화면에 한 번에 보여 줄 최대 개수. 더 많으면 목록이 화면을 밀어낸다.
|
|
17
|
+
export const 최대추천 = 6;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 지금 치고 있는 글에 맞는 명령들.
|
|
21
|
+
*
|
|
22
|
+
* 슬래시로 시작하고 아직 빈칸을 안 친 동안에만 본다. `/mode auto` 처럼 인자를
|
|
23
|
+
* 치기 시작하면 명령은 이미 정해진 것이라 더 보여 줄 것이 없다.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} 글 사람이 지금까지 친 것
|
|
26
|
+
* @param {object} 명령표 { 이름: { desc, arg } }
|
|
27
|
+
* @param {string[]} 보일것 이 수준에서 보여 줄 이름들 (없으면 전부)
|
|
28
|
+
*/
|
|
29
|
+
export function 추천(글, 명령표 = {}, 보일것 = null) {
|
|
30
|
+
const s = String(글 ?? '');
|
|
31
|
+
const m = /^\/([^\s]*)$/.exec(s);
|
|
32
|
+
if (!m) return [];
|
|
33
|
+
|
|
34
|
+
const 친것 = m[1].toLowerCase();
|
|
35
|
+
const 이름들 = 보일것 ?? Object.keys(명령표);
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* 앞에서부터 맞는 것을 먼저, 가운데 맞는 것을 뒤에.
|
|
39
|
+
*
|
|
40
|
+
* `/mo` 를 치면 mode·model 이 먼저 오고 memory 는 안 온다. 그런데 `/emo` 처럼
|
|
41
|
+
* 앞을 틀리게 쳤을 때도 memory 가 나오면 고맙다 — 오타는 앞글자에서 제일 많이 난다.
|
|
42
|
+
*/
|
|
43
|
+
const 앞 = [];
|
|
44
|
+
const 안 = [];
|
|
45
|
+
for (const 이름 of 이름들) {
|
|
46
|
+
const 낮 = 이름.toLowerCase();
|
|
47
|
+
if (!친것) { 앞.push(이름); continue; }
|
|
48
|
+
if (낮.startsWith(친것)) 앞.push(이름);
|
|
49
|
+
else if (낮.includes(친것)) 안.push(이름);
|
|
50
|
+
}
|
|
51
|
+
return [...앞, ...안].map((이름) => ({
|
|
52
|
+
이름,
|
|
53
|
+
설명: 명령표[이름]?.desc ?? '',
|
|
54
|
+
인자: 명령표[이름]?.arg ?? '',
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Tab 을 눌렀을 때 **덧붙일 글자**. 없으면 빈 글자.
|
|
60
|
+
*
|
|
61
|
+
* 하나만 맞으면 끝까지 채우고 빈칸을 하나 붙인다(인자를 받는 명령이면 바로 이어
|
|
62
|
+
* 칠 수 있게). 여럿이면 **다 같이 가진 앞부분까지만** 채운다 — 하나를 골라
|
|
63
|
+
* 넣어 버리면 사람이 원한 것이 아닐 때 지우는 수고가 더 든다.
|
|
64
|
+
*/
|
|
65
|
+
export function 채울글(글, 후보들) {
|
|
66
|
+
const s = String(글 ?? '');
|
|
67
|
+
const m = /^\/([^\s]*)$/.exec(s);
|
|
68
|
+
if (!m || !후보들?.length) return '';
|
|
69
|
+
const 친것 = m[1];
|
|
70
|
+
|
|
71
|
+
// 앞에서부터 맞는 것만 채운다. 가운데 맞은 것을 채우면 친 글자가 사라진다.
|
|
72
|
+
const 맞는것 = 후보들.filter((x) => x.이름.toLowerCase().startsWith(친것.toLowerCase()));
|
|
73
|
+
if (!맞는것.length) return '';
|
|
74
|
+
|
|
75
|
+
if (맞는것.length === 1) {
|
|
76
|
+
const 이름 = 맞는것[0].이름;
|
|
77
|
+
return 이름.slice(친것.length) + (맞는것[0].인자 ? ' ' : '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 다 같이 가진 앞부분.
|
|
81
|
+
let 같은데까지 = 맞는것[0].이름;
|
|
82
|
+
for (const x of 맞는것.slice(1)) {
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < 같은데까지.length && i < x.이름.length
|
|
85
|
+
&& 같은데까지[i].toLowerCase() === x.이름[i].toLowerCase()) i++;
|
|
86
|
+
같은데까지 = 같은데까지.slice(0, i);
|
|
87
|
+
}
|
|
88
|
+
return 같은데까지.length > 친것.length ? 같은데까지.slice(친것.length) : '';
|
|
89
|
+
}
|
package/src/ui/inputbox.js
CHANGED
|
@@ -33,12 +33,16 @@
|
|
|
33
33
|
* 2) 그릴 것을 값으로 내놓는다(프레임). 검사는 파이프로 도니 화면을 못 본다 —
|
|
34
34
|
* 값으로 내놓아야 한 칸도 안 어긋나는지 잴 수 있다.
|
|
35
35
|
*/
|
|
36
|
-
import { c, width, cursor } from './ansi.js';
|
|
36
|
+
import { c, width, cursor, clip } from './ansi.js';
|
|
37
37
|
import { statusLine, contextWarning } from './status.js';
|
|
38
38
|
import { 접어쓰기 } from './wrap.js';
|
|
39
|
+
import { 문구고르기, 돌림틀, 걸린시간, 느긋해질때, 문구주기 } from './working.js';
|
|
40
|
+
import { 최대추천 } from './complete.js';
|
|
39
41
|
|
|
40
42
|
const 줄끝지움 = '\x1b[K';
|
|
41
43
|
const 위로 = (n) => (n > 0 ? `\x1b[${n}A` : '');
|
|
44
|
+
// 돌아가는 표시를 한 칸 넘기는 주기. 사람 눈에 부드럽고, 다시 그리는 값은 싸다.
|
|
45
|
+
const 돌림주기 = 90;
|
|
42
46
|
|
|
43
47
|
// 상자 안쪽 왼쪽에 붙는 것: ' ' + '│' + ' ' + '❯' + ' ' → 5칸
|
|
44
48
|
const 앞머리 = 5;
|
|
@@ -55,10 +59,11 @@ export const 안쪽최대 = 8;
|
|
|
55
59
|
* @param {string} o.상태 상태줄 (없으면 안 그린다)
|
|
56
60
|
* @param {string} o.경고 컨텍스트 경고 (없으면 안 그린다)
|
|
57
61
|
* @param {string} o.곁말 상자 아래 흐린 한 줄 (도움말 등)
|
|
62
|
+
* @param {object[]} o.추천 자동완성 후보 [{이름, 설명, 인자}]
|
|
58
63
|
* @returns {{줄들: string[], 커서: {위: number, 열: number}}}
|
|
59
64
|
* 커서.위 = 마지막 줄에서 몇 줄 위인가, 커서.열 = 1부터 세는 칸
|
|
60
65
|
*/
|
|
61
|
-
export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '' } = {}) {
|
|
66
|
+
export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상태 = '', 경고 = '', 곁말 = '', 일감 = null, 추천 = [] } = {}) {
|
|
62
67
|
const 칸 = Math.max(20, 폭);
|
|
63
68
|
const 안쪽 = Math.max(4, 칸 - 앞머리 - 2); // 오른쪽 ' │' 두 칸
|
|
64
69
|
const 줄들 = [];
|
|
@@ -69,6 +74,36 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
69
74
|
const 가로 = '─'.repeat(칸 - 3);
|
|
70
75
|
줄들.push(c.gray(` ╭${가로}╮`));
|
|
71
76
|
|
|
77
|
+
/*
|
|
78
|
+
* 일하는 중.
|
|
79
|
+
*
|
|
80
|
+
* 상자를 없애고 대신 한 줄을 흘려보내는 길도 있었다. 그런데 그러면 일할 때만
|
|
81
|
+
* 화면 아래가 텅 비어, 사람이 보기에 **프로그램이 다른 것으로 바뀐 것처럼**
|
|
82
|
+
* 보인다. 테두리를 그대로 두고 안엣것만 바꾼다 — 같은 자리에서 같은 상자가
|
|
83
|
+
* 계속 살아 있는 편이 '돌아가는 중' 이라는 신호로 훨씬 낫다.
|
|
84
|
+
*/
|
|
85
|
+
if (일감) {
|
|
86
|
+
/*
|
|
87
|
+
* 여기서 쓸 수 있는 칸은 평소보다 **두 칸 넓다.**
|
|
88
|
+
*
|
|
89
|
+
* 평소에는 왼쪽에 `❯ ` 가 붙지만(앞머리 5칸) 일하는 중에는 돌아가는 표시가
|
|
90
|
+
* 본문 안에 들어가므로 그 두 칸이 본문 몫이 된다. 이걸 안 더하면 테두리가
|
|
91
|
+
* 딱 두 칸 짧아진다 — 눈으로는 거의 안 보이는데 실제 터미널에서는 줄이
|
|
92
|
+
* 어긋나 보인다.
|
|
93
|
+
*/
|
|
94
|
+
const 일안쪽 = 안쪽 + 2;
|
|
95
|
+
const 왼쪽 = `${c.hcyan(일감.돌림 ?? '⠋')} ${c.white(일감.말 ?? '')}${c.gray('…')}`;
|
|
96
|
+
const 오른쪽 = c.gray(일감.곁 ?? '');
|
|
97
|
+
const 남 = 일안쪽 - width(왼쪽) - width(오른쪽);
|
|
98
|
+
// 자리가 모자라면 오른쪽(걸린 시간·중단 안내)을 버린다. 왼쪽이 본문이다.
|
|
99
|
+
const 안 = 남 >= 2 ? `${왼쪽}${' '.repeat(남)}${오른쪽}` : 왼쪽;
|
|
100
|
+
줄들.push(` ${c.gray('│')} ${안}${' '.repeat(Math.max(0, 일안쪽 - width(안)))} ${c.gray('│')}`);
|
|
101
|
+
줄들.push(c.gray(` ╰${가로}╯`));
|
|
102
|
+
if (곁말) 줄들.push(` ${c.gray(곁말)}`);
|
|
103
|
+
// 일하는 중에는 커서를 안 보여 준다 — 칠 자리가 아니다.
|
|
104
|
+
return { 줄들, 커서: { 위: (곁말 ? 1 : 0), 열: 1 } };
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
/*
|
|
73
108
|
* 긴 입력은 접는다. 자르면 사람이 친 글이 안 보이는데, 안 보이는 채로
|
|
74
109
|
* Enter 를 치게 되는 것이 제일 나쁘다 — 무엇을 보내는지 모르게 된다.
|
|
@@ -83,6 +118,41 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
83
118
|
줄들.push(` ${c.gray('│')} ${표} ${한줄}${' '.repeat(Math.max(0, 남))} ${c.gray('│')}`);
|
|
84
119
|
}
|
|
85
120
|
줄들.push(c.gray(` ╰${가로}╯`));
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* 자동완성 추천은 상자 **아래**에 놓는다.
|
|
124
|
+
*
|
|
125
|
+
* 안에 넣으면 치던 글이 밀려 올라가서, 무엇을 치고 있었는지가 안 보인다.
|
|
126
|
+
* 아래에 두면 목록이 몇 줄이든 치던 자리는 늘 같은 데 있다.
|
|
127
|
+
*/
|
|
128
|
+
let 추천줄수 = 0;
|
|
129
|
+
if (추천?.length) {
|
|
130
|
+
const 보일것 = 추천.slice(0, 최대추천);
|
|
131
|
+
// 이름은 상자 안 글자와 같은 자리(6칸째)에서 시작한다 — 눈이 안 흔들린다.
|
|
132
|
+
const 이름들 = 보일것.map((x) => `/${x.이름}${x.인자 ? ` ${x.인자}` : ''}`);
|
|
133
|
+
// 이름칸이 넓어지면 설명이 그만큼 잘린다. 제일 긴 이름 하나 때문에 설명이
|
|
134
|
+
// 다 사라지는 일이 없게 위쪽을 막아 둔다.
|
|
135
|
+
const 이름폭 = Math.min(Math.max(...이름들.map(width)), Math.max(12, Math.floor(칸 * 0.38)));
|
|
136
|
+
const 설명폭 = 칸 - 앞머리 - 이름폭 - 3;
|
|
137
|
+
|
|
138
|
+
for (const [i, x] of 보일것.entries()) {
|
|
139
|
+
// 첫째 것만 밝게. Tab 을 누르면 대개 이것으로 채워진다.
|
|
140
|
+
const 표 = i === 0 ? c.hcyan('›') : ' ';
|
|
141
|
+
const 칠 = i === 0 ? c.white : c.gray;
|
|
142
|
+
const 이름 = clip(이름들[i], 이름폭);
|
|
143
|
+
const 벌림 = ' '.repeat(Math.max(1, 이름폭 - width(이름) + 2));
|
|
144
|
+
// 좁은 터미널에서는 설명을 버린다. 이름이 본문이다.
|
|
145
|
+
const 설명 = 설명폭 >= 8 ? c.gray(clip(x.설명 ?? '', 설명폭)) : '';
|
|
146
|
+
줄들.push(` ${표} ${칠(이름)}${설명 ? 벌림 + 설명 : ''}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
추천줄수 = 보일것.length;
|
|
150
|
+
if (추천.length > 보일것.length) {
|
|
151
|
+
줄들.push(` ${c.gray(`그 밖에 ${추천.length - 보일것.length}개 더`)}`);
|
|
152
|
+
추천줄수 += 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
86
156
|
if (곁말) 줄들.push(` ${c.gray(곁말)}`);
|
|
87
157
|
|
|
88
158
|
/*
|
|
@@ -99,7 +169,8 @@ export function 프레임({ 글 = '', 커서: 커서자리 = null, 폭 = 80, 상
|
|
|
99
169
|
const 숨긴줄 = 안쪽줄.length - 보일줄.length;
|
|
100
170
|
const 커서줄 = Math.min(Math.max(0, 앞줄들.length - 1 - 숨긴줄), 보일줄.length - 1);
|
|
101
171
|
|
|
102
|
-
|
|
172
|
+
// 커서 아래에 몇 줄이 더 있나 — 닫는 테두리 + 추천 목록 + 곁말.
|
|
173
|
+
const 아래 = 1 + 추천줄수 + (곁말 ? 1 : 0);
|
|
103
174
|
const 위 = 아래 + (보일줄.length - 1 - 커서줄);
|
|
104
175
|
// 1부터 세는 칸. 앞머리(5칸) 다음이 6칸째다 — 거기가 첫 글자 자리다.
|
|
105
176
|
const 열 = Math.min(앞머리 + 1 + width(앞줄들.at(-1) ?? ''), 칸 - 1);
|
|
@@ -128,6 +199,56 @@ export class InputBox {
|
|
|
128
199
|
this.커서위 = 0;
|
|
129
200
|
this.session = null;
|
|
130
201
|
this.곁말 = '';
|
|
202
|
+
|
|
203
|
+
// 일하는 중 상태. null 이면 평소(입력 기다리는 중)다.
|
|
204
|
+
this.일감 = null;
|
|
205
|
+
this.박자 = null; // 돌아가는 표시를 돌리는 시계
|
|
206
|
+
this.틱 = 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 일하기 시작. 상자 안엣것이 입력칸에서 '돌아가는 중' 으로 바뀐다.
|
|
211
|
+
*
|
|
212
|
+
* 스스로 다시 그리는 시계를 단다. 모델이 30초를 말없이 생각할 때 우리 쪽에서
|
|
213
|
+
* 아무 일도 안 일어나면 표시가 얼어붙는데, 그건 멈춘 것과 화면상 구분이 안 된다.
|
|
214
|
+
*/
|
|
215
|
+
일시작(session, 갈래 = '기본') {
|
|
216
|
+
this.session = session ?? this.session;
|
|
217
|
+
this.일감 = { 갈래, 시작: Date.now(), 곁정보: '' };
|
|
218
|
+
this.틱 = 0;
|
|
219
|
+
this.일그리기();
|
|
220
|
+
if (this.박자) clearInterval(this.박자);
|
|
221
|
+
this.박자 = setInterval(() => { this.틱 += 1; this.일그리기(); }, 돌림주기);
|
|
222
|
+
// 이 시계 때문에 프로그램이 안 끝나면 안 된다. 끝맺음이 걸려 버린다.
|
|
223
|
+
if (typeof this.박자.unref === 'function') this.박자.unref();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** 하는 일이 바뀌었다 (파일 읽기 → 코드 고치기 처럼). */
|
|
227
|
+
일바꿈(갈래, 곁정보 = null) {
|
|
228
|
+
if (!this.일감) return;
|
|
229
|
+
// 갈래가 바뀌면 문구를 그 자리에서 갈아 준다. 4초를 기다리면 이미 다음
|
|
230
|
+
// 일로 넘어가 있어서, 화면이 늘 한 걸음 늦은 말을 하게 된다.
|
|
231
|
+
if (갈래 && 갈래 !== this.일감.갈래) { this.일감.갈래 = 갈래; this.틱 = 0; }
|
|
232
|
+
if (곁정보 !== null) this.일감.곁정보 = 곁정보;
|
|
233
|
+
this.일그리기();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
일끝() {
|
|
237
|
+
if (this.박자) { clearInterval(this.박자); this.박자 = null; }
|
|
238
|
+
this.일감 = null;
|
|
239
|
+
this.지우기();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
일그리기() {
|
|
243
|
+
if (!this.일감) return;
|
|
244
|
+
const 지난 = Date.now() - this.일감.시작;
|
|
245
|
+
const 갈래 = 지난 >= 느긋해질때 ? '느긋' : this.일감.갈래;
|
|
246
|
+
const 회차 = Math.floor((this.틱 * 돌림주기) / 문구주기);
|
|
247
|
+
this.그리기(this.session, '', 0, {
|
|
248
|
+
돌림: 돌림틀[this.틱 % 돌림틀.length],
|
|
249
|
+
말: 문구고르기(갈래, 회차),
|
|
250
|
+
곁: [걸린시간(지난), this.일감.곁정보, 'Ctrl+C 중단'].filter(Boolean).join(' · '),
|
|
251
|
+
});
|
|
131
252
|
}
|
|
132
253
|
|
|
133
254
|
/** 상자를 지운다. 위쪽 대화는 안 건드린다. */
|
|
@@ -151,8 +272,10 @@ export class InputBox {
|
|
|
151
272
|
* @param {object} session 상태줄을 그리는 데 쓴다
|
|
152
273
|
* @param {string} 글 readline 이 들고 있는 글
|
|
153
274
|
* @param {number} 커서 그 안에서의 커서 위치
|
|
275
|
+
* @param {object} 일감 일하는 중이면 그 상태
|
|
276
|
+
* @param {object[]} 추천 자동완성 후보
|
|
154
277
|
*/
|
|
155
|
-
그리기(session, 글 = '', 커서자리 = null) {
|
|
278
|
+
그리기(session, 글 = '', 커서자리 = null, 일감 = null, 추천 = []) {
|
|
156
279
|
this.session = session ?? this.session;
|
|
157
280
|
this.지우기();
|
|
158
281
|
|
|
@@ -163,6 +286,8 @@ export class InputBox {
|
|
|
163
286
|
상태: this.session ? statusLine(this.session) : '',
|
|
164
287
|
경고: this.session ? (contextWarning(this.session) ?? '') : '',
|
|
165
288
|
곁말: this.곁말,
|
|
289
|
+
일감,
|
|
290
|
+
추천,
|
|
166
291
|
});
|
|
167
292
|
|
|
168
293
|
cursor.hide();
|
package/src/ui/screen.js
CHANGED
|
@@ -107,7 +107,7 @@ export class LineScreen {
|
|
|
107
107
|
/**
|
|
108
108
|
* 사람이 치고 있던 입력 줄을 지운다.
|
|
109
109
|
*
|
|
110
|
-
* Shift+Tab 으로
|
|
110
|
+
* Shift+Tab 으로 승인 방식을 돌릴 때처럼, 입력을 기다리는 도중에 화면에
|
|
111
111
|
* 한 줄을 끼워 넣어야 하는 자리가 있다. 안 지우면 `❯ ` 뒤에 새 글이 붙는다.
|
|
112
112
|
* 상자화면에서는 상자를 통째로 걷어낸다(BoxScreen 이 갈아 끼운다).
|
|
113
113
|
*/
|
|
@@ -150,6 +150,17 @@ export class LineScreen {
|
|
|
150
150
|
/** 사람이 치는 중인 글이 바뀌었다. 줄화면은 readline 이 알아서 되비춘다. */
|
|
151
151
|
입력갱신() { }
|
|
152
152
|
|
|
153
|
+
/*
|
|
154
|
+
* 일하는 중 표시. 줄화면은 안 그린다.
|
|
155
|
+
*
|
|
156
|
+
* 줄화면은 흘러가는 화면이라 '한 자리에서 도는 표시' 를 둘 데가 없다.
|
|
157
|
+
* 대신 기다림()·생각() 이 그 자리를 이미 맡고 있다 — 여기서 또 그리면
|
|
158
|
+
* 같은 말이 두 번 나간다. 파이프·기록에서는 돌아가는 표시 자체가 잡음이다.
|
|
159
|
+
*/
|
|
160
|
+
일시작() { }
|
|
161
|
+
일바꿈() { }
|
|
162
|
+
일끝() { }
|
|
163
|
+
|
|
153
164
|
close() {
|
|
154
165
|
this.돌림멈춤();
|
|
155
166
|
this.임시지움();
|
|
@@ -170,20 +181,59 @@ export class BoxScreen extends LineScreen {
|
|
|
170
181
|
this.상자 = 상자;
|
|
171
182
|
}
|
|
172
183
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
/*
|
|
185
|
+
* 대화 한 줄이 나가기 전에 상자를 걷어낸다. 안 걷으면 새 글이 상자 위에
|
|
186
|
+
* 겹쳐 찍히고, 지울 때 대화까지 같이 지워진다.
|
|
187
|
+
*
|
|
188
|
+
* 일하는 중이면 찍은 **뒤에 다시 세운다.** 이게 없으면 첫 도구 결과가
|
|
189
|
+
* 찍히는 순간 상자가 사라지고, 그 뒤로는 일이 끝날 때까지 화면 아래가
|
|
190
|
+
* 텅 빈 채로 남는다 — 정확히 그게 "밋밋해 보인다" 는 상태다.
|
|
191
|
+
*/
|
|
192
|
+
줄(s = '') { this.상자.지우기(); super.줄(s); this.상자.일그리기(); }
|
|
193
|
+
|
|
194
|
+
/*
|
|
195
|
+
* 스트리밍으로 이어 붙이는 글은 다시 안 세운다.
|
|
196
|
+
*
|
|
197
|
+
* 토막마다 상자를 세웠다 걷으면, 글자 몇 개마다 상자가 깜빡이고 커서가
|
|
198
|
+
* 위아래로 튄다. 답이 흘러나오는 동안에는 그 자체가 살아 있다는 신호라
|
|
199
|
+
* 상자가 없어도 밋밋하지 않다. 답이 끝나면 repl.js 가 다시 세운다.
|
|
200
|
+
*/
|
|
176
201
|
붙임(s) { this.상자.지우기(); super.붙임(s); }
|
|
177
202
|
|
|
178
|
-
/**
|
|
179
|
-
|
|
203
|
+
/** 일하기 시작·바뀜·끝. 상자 안엣것만 바뀐다 (ui/working.js 의 문구). */
|
|
204
|
+
일시작(session, 갈래) { this.임시지움(); this.상자.일시작(session, 갈래); }
|
|
205
|
+
일바꿈(갈래, 곁정보) { this.상자.일바꿈(갈래, 곁정보); }
|
|
206
|
+
일끝() { this.상자.일끝(); }
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* 줄화면에서 '생각 중…' 을 한 줄로 흘리던 자리.
|
|
210
|
+
*
|
|
211
|
+
* 상자를 쓸 때는 그 말이 상자 안으로 들어가야 한다. 안 그러면 같은 말이
|
|
212
|
+
* 두 군데(흘러가는 줄 + 상자)에 동시에 뜬다.
|
|
213
|
+
*/
|
|
214
|
+
기다림() { }
|
|
215
|
+
생각(s) {
|
|
216
|
+
// '생각 중… 1,234자' 에서 숫자만 떼어 곁정보로 붙인다. 진짜로 늘어나는
|
|
217
|
+
// 숫자가 하나 있어야 '살아 있다' 가 표시가 아니라 사실이 된다.
|
|
218
|
+
const m = /([\d,]+)자/.exec(String(s ?? ''));
|
|
219
|
+
this.상자.일바꿈('생각', m ? `생각 ${m[1]}자` : null);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 입력 자리 = 상태줄 + 상자. 커서는 상자 안에 놓인다.
|
|
224
|
+
*
|
|
225
|
+
* 치던 글을 받는다. 입력 도중에 한 줄을 끼워 넣고 다시 그리는 자리
|
|
226
|
+
* (Shift+Tab·Ctrl+O·Ctrl+C)에서, 빈 칸을 그렸다가 곧바로 채우면 한 번
|
|
227
|
+
* 깜빡인다. 처음부터 채워서 그린다 — 한 번만 그리면 안 깜빡인다.
|
|
228
|
+
*/
|
|
229
|
+
입력자리(session, 글 = '', 커서 = 0, 추천 = []) {
|
|
180
230
|
this.임시지움();
|
|
181
231
|
say('');
|
|
182
|
-
this.상자.그리기(session,
|
|
232
|
+
this.상자.그리기(session, 글, 커서, null, 추천);
|
|
183
233
|
}
|
|
184
234
|
|
|
185
235
|
/** 사람이 치는 중인 글이 바뀌었다 (repl.js 의 keypress 가 부른다). */
|
|
186
|
-
입력갱신(session, 글,
|
|
236
|
+
입력갱신(session, 글, 커서, 추천 = []) { this.상자.그리기(session, 글, 커서, null, 추천); }
|
|
187
237
|
|
|
188
238
|
입력지움() { this.상자.지우기(); }
|
|
189
239
|
|
package/src/ui/status.js
CHANGED
|
@@ -6,6 +6,7 @@ import { c, gauge, width, clip, cols, mark } from './ansi.js';
|
|
|
6
6
|
import { PROFILES } from '../agent/effort.js';
|
|
7
7
|
import { get as workMode, canWrite } from '../agent/modes.js';
|
|
8
8
|
import { isLocalHost, isOffline } from '../safety/network.js';
|
|
9
|
+
import { 표시 as 승인표시, 고르기 as 승인고르기 } from './approve.js';
|
|
9
10
|
|
|
10
11
|
// 한 조각씩 따로 만든다. 좁은 화면에서는 뒤에서부터 떨군다.
|
|
11
12
|
// 순서 = 중요도 순. 앞쪽이 끝까지 살아남는다.
|
|
@@ -14,7 +15,20 @@ export const SEGMENTS = {
|
|
|
14
15
|
// 이 조각 하나가 상태줄을 통째로 넘겨 줄이 접힌다(모델 이름은 이미 자르고 있었다).
|
|
15
16
|
dir: { desc: '작업 폴더', make: (s) => c.gray(clip(base(s.root), 20)) },
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
/*
|
|
19
|
+
* 모델 이름.
|
|
20
|
+
*
|
|
21
|
+
* 좁을 때 더 줄인다. 사내 게이트웨이 이름은 `databricks-gpt-5-6-luna` 처럼
|
|
22
|
+
* 스물세 칸을 그냥 먹는데, 자리가 없을 때 이것 하나 때문에 **셋째 덩이가
|
|
23
|
+
* 통째로 떨어져 나간다** — 승인 방식이 화면에서 사라진다는 뜻이다.
|
|
24
|
+
* 무엇으로 돌고 있는지는 이미 알고 있다. 내 파일이 안 물어보고 바뀌는지는
|
|
25
|
+
* 지금 봐야 안다. 둘 중 하나를 접어야 하면 이쪽이다.
|
|
26
|
+
*/
|
|
27
|
+
model: {
|
|
28
|
+
desc: '모델 이름',
|
|
29
|
+
make: (s) => c.hcyan(clip(s.conn.model, 24)),
|
|
30
|
+
short: (s) => c.hcyan(clip(s.conn.model, 14)),
|
|
31
|
+
},
|
|
18
32
|
|
|
19
33
|
ctx: {
|
|
20
34
|
desc: '컨텍스트 사용량',
|
|
@@ -54,12 +68,17 @@ export const SEGMENTS = {
|
|
|
54
68
|
short: (s) => `${c.gray('◇')} ${c.white(s.think)}`,
|
|
55
69
|
},
|
|
56
70
|
|
|
71
|
+
/*
|
|
72
|
+
* 승인 방식 — 내 파일이 물어보고 바뀌는가, 안 물어보고 바뀌는가.
|
|
73
|
+
*
|
|
74
|
+
* 전에는 `auto` 라는 영문 한 낱말이었다. 옆에 `종합`·`medium·절약` 이 나란히
|
|
75
|
+
* 있으니 셋 다 그냥 '모드' 로 보였고, 그중 하나가 **묻지 않고 파일을 고친다**
|
|
76
|
+
* 는 뜻이라는 것은 화면 어디에도 없었다. 이건 꾸미기가 아니라 안전 표시다.
|
|
77
|
+
*/
|
|
57
78
|
mode: {
|
|
58
|
-
desc: '
|
|
59
|
-
make: (s) =>
|
|
60
|
-
|
|
61
|
-
return tone(s.mode);
|
|
62
|
-
},
|
|
79
|
+
desc: '승인 방식',
|
|
80
|
+
make: (s) => 승인표시(s.mode),
|
|
81
|
+
short: (s) => 승인표시(s.mode, { 짧게: true }),
|
|
63
82
|
},
|
|
64
83
|
|
|
65
84
|
tok: {
|
|
@@ -217,6 +236,10 @@ export function headerLines(session, found) {
|
|
|
217
236
|
`${c.gray('보냄')} ${목적지} ${c.gray('← 여기 말고는 어디로도 안 갑니다')}`,
|
|
218
237
|
`${c.gray('연결')} ${능력}`,
|
|
219
238
|
`${c.gray('폴더')} ${c.white(clip(session.root, 56))}`,
|
|
239
|
+
// 켤 때 한 번은 사람 말로 알려 준다. 상태줄의 `⏵⏵ 자동 승인` 이 무슨
|
|
240
|
+
// 뜻인지 여기서 한 번 읽고 나면 그 다음부터는 글자만 봐도 안다.
|
|
241
|
+
`${c.gray('승인')} ${승인표시(session.mode)} ${c.gray('— ' + 승인고르기(session.mode).한줄)}`,
|
|
242
|
+
`${c.gray(' Shift+Tab')} ${c.gray('으로 바꿉니다 · ')}${c.gray('Tab 은 치던 / 명령을 채웁니다')}`,
|
|
220
243
|
];
|
|
221
244
|
if (found.skills.length || found.commands.length) {
|
|
222
245
|
lines.push(`${c.gray('이 PC')} ${c.white(`스킬 ${found.skills.length}`)}${c.gray(' · ')}${c.white(`명령 ${found.commands.length}`)}${c.gray(' · ')}${c.white(`플러그인 ${found.plugins.length}`)}`);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 일하는 중에 띄울 문구.
|
|
3
|
+
*
|
|
4
|
+
* ── 왜 아무 말이나 안 쓰나 ──────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* 재미 때문에 넣는 것이 아니다. 로컬 모델은 느리다 — 7B 라도 한 걸음에 수십
|
|
7
|
+
* 초가 걸리고, 그 동안 화면이 조용하면 사람은 **멈춘 줄 안다.** 실제로 Ctrl+C
|
|
8
|
+
* 를 눌러 버린다. 그러면 다 되어 가던 일이 날아간다.
|
|
9
|
+
*
|
|
10
|
+
* 그래서 문구는 **지금 진짜로 하는 일**을 따라간다. 파일을 읽는 중이면 읽는다고
|
|
11
|
+
* 하고, 명령을 돌리는 중이면 돌린다고 한다. 아무 말이나 돌려 대면 두 번째부터
|
|
12
|
+
* 아무도 안 읽고, 그때부터는 화면이 조용한 것과 같아진다.
|
|
13
|
+
*
|
|
14
|
+
* 한 갈래 안에서 여러 개를 돌리는 것은 다른 이유다. 같은 글자가 30초 동안
|
|
15
|
+
* 그대로 있으면 그것도 멈춘 것처럼 보인다. 돌아가는 표시만으로는 부족하다 —
|
|
16
|
+
* 터미널 렌더링이 밀리면 그것도 안 움직이는 것처럼 보이기 때문이다.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// 갈래별 문구. 앞엣것부터 차례로 돌린다 — 아무거나 뽑으면 눈에 거슬린다.
|
|
20
|
+
export const 문구 = {
|
|
21
|
+
생각: [
|
|
22
|
+
'머리 굴리는 중',
|
|
23
|
+
'어떻게 할지 궁리하는 중',
|
|
24
|
+
'수 읽는 중',
|
|
25
|
+
'따져 보는 중',
|
|
26
|
+
'생각 정리하는 중',
|
|
27
|
+
],
|
|
28
|
+
읽기: [
|
|
29
|
+
'파일 들여다보는 중',
|
|
30
|
+
'코드 훑는 중',
|
|
31
|
+
'어디 있나 뒤지는 중',
|
|
32
|
+
'단서 찾는 중',
|
|
33
|
+
],
|
|
34
|
+
쓰기: [
|
|
35
|
+
'코드 짜는 중',
|
|
36
|
+
'고쳐 넣는 중',
|
|
37
|
+
'손보는 중',
|
|
38
|
+
'한 줄씩 옮기는 중',
|
|
39
|
+
],
|
|
40
|
+
명령: [
|
|
41
|
+
'명령 돌리는 중',
|
|
42
|
+
'터미널 두드리는 중',
|
|
43
|
+
'결과 기다리는 중',
|
|
44
|
+
],
|
|
45
|
+
웹: [
|
|
46
|
+
'문서 찾아보는 중',
|
|
47
|
+
'읽어 오는 중',
|
|
48
|
+
],
|
|
49
|
+
답: [
|
|
50
|
+
'답 쓰는 중',
|
|
51
|
+
'정리해서 말하는 중',
|
|
52
|
+
],
|
|
53
|
+
접기: [
|
|
54
|
+
'지난 대화 접는 중',
|
|
55
|
+
'자리 만드는 중',
|
|
56
|
+
],
|
|
57
|
+
// 무슨 일인지 아직 모를 때. 첫 걸음과 도구 사이의 짧은 틈이다.
|
|
58
|
+
기본: [
|
|
59
|
+
'열심히 일하는 중',
|
|
60
|
+
'바쁘게 움직이는 중',
|
|
61
|
+
'뭔가 하는 중',
|
|
62
|
+
],
|
|
63
|
+
/*
|
|
64
|
+
* 오래 걸릴 때.
|
|
65
|
+
*
|
|
66
|
+
* 45초가 넘어가면 갈래를 이걸로 바꾼다. '아직 하는 중' 이라고 말해 주는 것과
|
|
67
|
+
* 아까 하던 말을 계속 돌리는 것은 다르다 — 후자는 붙박이처럼 보인다.
|
|
68
|
+
* 여기서 중요한 것은 재치가 아니라 **아직 살아 있다는 신호**다.
|
|
69
|
+
*/
|
|
70
|
+
느긋: [
|
|
71
|
+
'아직 하는 중',
|
|
72
|
+
'조금만 더',
|
|
73
|
+
'생각보다 오래 걸리는 중',
|
|
74
|
+
'그래도 하는 중',
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// 도구 이름 → 갈래. 없는 이름은 기본으로 떨어진다.
|
|
79
|
+
const 도구갈래 = {
|
|
80
|
+
Read: '읽기', Glob: '읽기', Grep: '읽기', Skill: '읽기', Recall: '읽기',
|
|
81
|
+
Write: '쓰기', Edit: '쓰기', Append: '쓰기', Remember: '쓰기', TodoWrite: '쓰기',
|
|
82
|
+
Bash: '명령',
|
|
83
|
+
WebFetch: '웹',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** 도구 이름으로 갈래를 고른다. 밖에서 붙인 도구(mcp__…)는 무엇을 할지 모른다. */
|
|
87
|
+
export function 갈래고르기(도구이름) {
|
|
88
|
+
if (!도구이름) return '기본';
|
|
89
|
+
if (String(도구이름).startsWith('mcp__')) return '기본';
|
|
90
|
+
return 도구갈래[도구이름] ?? '기본';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 갈래와 회차로 문구 하나. 회차가 늘면 다음 것으로 넘어간다.
|
|
95
|
+
*
|
|
96
|
+
* 시각이 아니라 회차를 받는 이유: 이래야 검사가 잴 수 있다. 시각을 안에서
|
|
97
|
+
* 읽으면 '4초 뒤에 바뀌는가' 를 재려고 검사가 진짜로 4초를 기다려야 한다.
|
|
98
|
+
*/
|
|
99
|
+
export function 문구고르기(갈래, 회차 = 0) {
|
|
100
|
+
const 목록 = 문구[갈래] ?? 문구.기본;
|
|
101
|
+
const i = ((Math.trunc(회차) % 목록.length) + 목록.length) % 목록.length;
|
|
102
|
+
return 목록[i];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 돌아가는 표시. 점자 여덟 칸짜리는 폭이 한 칸이라 테두리를 안 밀어낸다.
|
|
106
|
+
export const 돌림틀 = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
107
|
+
|
|
108
|
+
/** 걸린 시간을 짧게. 1분이 넘으면 분까지 적는다 — 90초보다 1분 30초가 읽힌다. */
|
|
109
|
+
export function 걸린시간(ms) {
|
|
110
|
+
const 초 = Math.max(0, Math.floor(ms / 1000));
|
|
111
|
+
if (초 < 60) return `${초}초`;
|
|
112
|
+
return `${Math.floor(초 / 60)}분 ${String(초 % 60).padStart(2, '0')}초`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 이 시간이 넘으면 '느긋' 갈래로 바꾼다.
|
|
116
|
+
export const 느긋해질때 = 45000;
|
|
117
|
+
// 문구를 바꾸는 주기.
|
|
118
|
+
export const 문구주기 = 4000;
|