deel-local-cli 1.5.2 → 1.5.4

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 CHANGED
@@ -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.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) |
103
+ | [Release notes](docs/en/releases.md) | [1.5.4](docs/en/releases.md#154) · [1.5.3](docs/en/releases.md#153) · [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
 
@@ -388,6 +388,7 @@ Names follow Claude Code / Codex conventions.
388
388
  | `/auto` | Hand the wheel back — it picks the mode from what you type |
389
389
  | `/code` `/plan` `/architect` `/debug` `/ask` `/orchestrator` | Switch work mode directly (pins it) |
390
390
  | `/level [level]` | How much to show (`쉬움` simple · `개발자` developer) |
391
+ | `/motion [plain\|knight\|animal\|office\|off]` | What animates while it works — takes effect at once, and is saved |
391
392
  | `/undo [turns]` | Revert file changes |
392
393
  | `/diff [file]` | Files changed this session, and the changed lines |
393
394
  | `/preview [folder\|file\|off]` | Serve what you built, right here — a browser opens with it |
@@ -928,7 +929,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
928
929
  ## Development
929
930
 
930
931
  ```bash
931
- npm test Full suite (1,832 checks)
932
+ npm test Full suite (3,681 checks)
932
933
  npm run coverage Which lines the tests actually execute
933
934
  npm run verify Import + network checks only
934
935
  npm run bench Edit success rate
@@ -980,7 +981,9 @@ so one run tells you everything.
980
981
 
981
982
  | Version | What changed |
982
983
  |---|---|
983
- | **[1.5.2](docs/en/releases.md#152)** | Everything else the adversarial reviews turned upwhere the screen was lying |
984
+ | **[1.5.4](docs/en/releases.md#154)** | The office fills up and a day passes in it "only one person" was a bug, not a design |
985
+ | [1.5.3](docs/en/releases.md#153) | Three things a Mac user tripped over — newline hint, office stays put, one `/motion` |
986
+ | [1.5.2](docs/en/releases.md#152) | Everything else the adversarial reviews turned up — where the screen was lying |
984
987
  | [1.5.1](docs/en/releases.md#151) | Two of the things 1.5.0 added shipped broken. This fixes them |
985
988
  | [1.5.0](docs/en/releases.md#150) | Line breaks exist now, the screen got fun, and it came out lighter than before |
986
989
  | [1.4.3](docs/en/releases.md#143) | The README explains what's different, and the review report gets its missing line |
package/README.md CHANGED
@@ -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.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) |
104
+ | [릴리스 노트](docs/ko/releases.md) | [1.5.4](docs/ko/releases.md#154) · [1.5.3](docs/ko/releases.md#153) · [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
 
@@ -366,6 +366,7 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
366
366
  | `/auto` | 다시 맡기기 — 말을 보고 알맞은 모드로 저절로 옮겨 갑니다 |
367
367
  | `/code` `/plan` `/architect` `/debug` `/ask` `/orchestrator` | 작업 모드 바로 바꾸기 (그때부터 고정) |
368
368
  | `/level [수준]` | 화면에 무엇을 내놓을지 (`쉬움` · `개발자`) |
369
+ | `/motion [기본\|기사\|동물\|사무실\|끔]` | 일하는 동안 뭐가 도나 — 그 자리에서 바뀌고 설정에 남습니다 |
369
370
  | `/undo [턴수]` | 파일 변경 되돌리기 |
370
371
  | `/diff [파일]` | 이번 대화에서 바뀐 파일 · 바뀐 자리 보기 |
371
372
  | `/preview [폴더\|파일\|off]` | 만든 웹을 이 자리에서 띄워 보기 — 브라우저가 같이 열립니다 |
@@ -906,7 +907,7 @@ deel sbom --only sbom # SBOM 한 장만
906
907
  ## 개발
907
908
 
908
909
  ```bash
909
- npm test 전체 검증 (1,832항목)
910
+ npm test 전체 검증 (3,681항목)
910
911
  npm run coverage 검사가 소스의 어디를 밟았는지
911
912
  npm run verify 반입·통신 검증만
912
913
  npm run bench 편집 성공률 측정
@@ -971,7 +972,9 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
971
972
 
972
973
  | 판 | 무엇이 바뀌었나 |
973
974
  |---|---|
974
- | **[1.5.2](docs/ko/releases.md#152)** | 적대적 리뷰가 남긴 나머지를 전부 정리했다화면이 거짓말하던 자리들 |
975
+ | **[1.5.4](docs/ko/releases.md#154)** | 사무실에 사람이 차고, 방에 하루가 흐른다「한 명뿐」은 고정이 아니라 버그였다 |
976
+ | [1.5.3](docs/ko/releases.md#153) | 맥에서 걸린 세 가지 — 줄바꿈 안내 · 사무실이 안 사라짐 · `/motion` 하나로 |
977
+ | [1.5.2](docs/ko/releases.md#152) | 적대적 리뷰가 남긴 나머지를 전부 정리했다 — 화면이 거짓말하던 자리들 |
975
978
  | [1.5.1](docs/ko/releases.md#151) | 1.5.0 이 넣은 것 두 개가 망가진 채 나갔다. 그것을 고친다 |
976
979
  | [1.5.0](docs/ko/releases.md#150) | 줄바꿈이 생겼고, 화면이 재미있어졌고, 그러면서 오히려 가벼워졌다 |
977
980
  | [1.4.3](docs/ko/releases.md#143) | README 가 왜 다른지 말하고, 심사서의 빠진 한 줄을 채운다 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI — 외부 의존성 0개 / Zero-dependency coding agent CLI for local LLMs and private gateways",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/commands.js CHANGED
@@ -19,6 +19,27 @@ import { 보고서적기 } from './ui/export.js';
19
19
  import { loadCommand, discover } from './skills/discover.js';
20
20
  import { install, list, remove, pack } from './plugins/manage.js';
21
21
  import { spin } from './ui/spinner.js';
22
+ import { 그림고르기설정, 끔설정 } from './ui/motion.js';
23
+ import { 사무실설정, 최소높이, 최소폭 } from './ui/office.js';
24
+
25
+ /**
26
+ * 설정에 적힌 그림을 화면 쪽에 물린다.
27
+ *
28
+ * 켤 때(repl·oneshot)와 `/motion` 으로 바꿀 때가 **같은 길을 타야** 한다.
29
+ * 갈라 두면 명령으로 바꾼 것이 다음에 켤 때 안 살아나거나 그 반대가 된다.
30
+ *
31
+ * 사무실은 그림 하나가 아니라 화면 아래를 통째로 쓰는 것이라 따로 켠다.
32
+ * '사무실' 을 골랐으면 상자 안 그림은 조용한 돌림표로 돌아간다 — 둘 다
33
+ * 같은 것을 말하므로 나란히 두면 같은 소리를 두 번 하는 셈이다.
34
+ */
35
+ export function 적용하기(고른것) {
36
+ const 값 = String(고른것 ?? '기본');
37
+ 사무실설정(값 === '사무실');
38
+ 그림고르기설정(값 === '사무실' ? '기본' : 값);
39
+ // 환경변수(DEEL_NO_MOTION)는 안 건드린다 — 사람이 직접 넣어 둔 것을 명령이
40
+ // 지워 버리면, 껐다고 믿는 자리에서 그림이 다시 돈다. 설정용 값은 따로 둔다.
41
+ 끔설정(값 === '끔');
42
+ }
22
43
  import { PROFILES, LEVELS as THINK_LEVELS, normalizeProfile, table as effortTable } from './agent/effort.js';
23
44
  import { scanLocal, toProfiles } from './backend/scan.js';
24
45
  import { list as listSessions } from './agent/store.js';
@@ -89,6 +110,7 @@ const 명령들 = {
89
110
  memory: { arg: true },
90
111
  level: { arg: true },
91
112
  bell: { arg: true },
113
+ motion: { arg: true },
92
114
  consult: { arg: true },
93
115
  lang: { arg: true },
94
116
  lsp: { arg: true },
@@ -431,6 +453,83 @@ export async function handle(line, session, ctx) {
431
453
  say('');
432
454
  return { handled: true };
433
455
  }
456
+ /*
457
+ * 일하는 동안 뭐가 도나.
458
+ *
459
+ * 예전에는 DEEL_MOTION 과 DEEL_OFFICE 두 환경변수였다. 켜 보려면 터미널을
460
+ * 껐다 켜야 했고, 이름도 둘을 따로 외워야 했다 — 재미로 넣은 것을 켜는 데
461
+ * 그만한 품이 들면 아무도 안 켠다. 명령 하나로 합치고 그 자리에서 바뀐다.
462
+ */
463
+ case 'motion': {
464
+ const cfg = load();
465
+ const 고를것 = [
466
+ { 값: '기본', 별명: ['기본', 'default', 'plain', '돌림표'], 설명: 말('motion.plain') },
467
+ { 값: '기사', 별명: ['기사', 'knight'], 설명: 말('motion.knight') },
468
+ { 값: '동물', 별명: ['동물', 'animal', 'animals'], 설명: 말('motion.animal') },
469
+ { 값: '사무실', 별명: ['사무실', 'office'], 설명: 말('motion.office') },
470
+ { 값: '끔', 별명: ['끔', 'off', '꺼', '끄기', 'none'], 설명: 말('motion.off') },
471
+ ];
472
+ const 지금값 = cfg.motion ?? '기본';
473
+ const 값 = String(arg ?? '').trim().toLowerCase();
474
+
475
+ if (!값) {
476
+ say('');
477
+ say(` ${c.gray(말('motion.now'))} ${c.hcyan(지금값)}`);
478
+ say('');
479
+ for (const 것 of 고를것) {
480
+ const 표 = 것.값 === 지금값 ? c.hgreen('●') : c.gray('○');
481
+ // padEnd 가 아니라 pad — 한글은 한 글자가 두 칸이라
482
+ // 글자 수로 채우면 '사무실' 줄만 두 칸 밀린다.
483
+ say(` ${표} ${c.cyan(pad(`/motion ${것.값}`, 18))} ${c.gray(것.설명)}`);
484
+ }
485
+ say('');
486
+ // 사무실은 화면이 좁거나 낮으면 안 뜬다. 켜 놓고 안 보이면 고장으로
487
+ // 보이므로, 지금 이 터미널이 되는지를 여기서 미리 말해 준다.
488
+ const 줄 = process.stdout.rows ?? 0;
489
+ const 칸 = process.stdout.columns ?? 0;
490
+ if (줄 && 칸 && (줄 < 최소높이 || 칸 < 최소폭)) {
491
+ say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.tooSmall', { 줄, 칸, 최소줄: 최소높이, 최소칸: 최소폭 }))}`);
492
+ say('');
493
+ }
494
+ if (process.env.DEEL_MOTION || process.env.DEEL_OFFICE) {
495
+ say(` ${c.gray(말('motion.envWins'))}`);
496
+ say('');
497
+ }
498
+ return { handled: true };
499
+ }
500
+
501
+ const 고른것 = 고를것.find((것) => 것.별명.includes(값));
502
+ if (!고른것) {
503
+ say('');
504
+ say(` ${c.gray(말('motion.unknown', { 값: arg }))}`);
505
+ say(` ${c.gray(말('motion.pickOne'))} ${고를것.map((것) => c.cyan(것.값)).join(c.gray(' · '))}`);
506
+ say('');
507
+ return { handled: true };
508
+ }
509
+
510
+ cfg.motion = 고른것.값;
511
+ try { save(cfg); } catch { /* 못 남겨도 이번 세션에는 먹는다 */ }
512
+ 적용하기(cfg.motion);
513
+
514
+ say('');
515
+ say(` ${mark.ok} ${말('motion.set', { 것: 고른것.값 })} ${c.gray('— ' + 고른것.설명)}`);
516
+ // 사무실을 골랐는데 이 터미널이 작으면 아무 일도 안 일어난 것처럼 보인다.
517
+ // 켰다고 말해 놓고 안 보이면 그게 고장이다. 그래서 그 자리에서 말한다.
518
+ if (고른것.값 === '사무실') {
519
+ const 줄 = process.stdout.rows ?? 0;
520
+ const 칸 = process.stdout.columns ?? 0;
521
+ if (줄 && 칸 && (줄 < 최소높이 || 칸 < 최소폭)) {
522
+ say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.tooSmall', { 줄, 칸, 최소줄: 최소높이, 최소칸: 최소폭 }))}`);
523
+ }
524
+ }
525
+ if (process.env.DEEL_MOTION || process.env.DEEL_OFFICE) {
526
+ say(` ${c.yellow(mark.warn)} ${c.gray(말('motion.envWins'))}`);
527
+ }
528
+ say(` ${c.gray(말('motion.appliesNow'))}`);
529
+ say('');
530
+ return { handled: true };
531
+ }
532
+
434
533
  case 'exit':
435
534
  case 'quit': return { handled: true, exit: true };
436
535
 
package/src/i18n/en.js CHANGED
@@ -82,6 +82,8 @@ export const en = {
82
82
  'cmd.level.arg': '[level]',
83
83
  'cmd.bell.desc': 'bell and window title when a turn ends or deel needs you (on/off)',
84
84
  'cmd.bell.arg': '[on|off]',
85
+ 'cmd.motion.desc': 'what moves while deel works — knight, animals, office',
86
+ 'cmd.motion.arg': '[knight|animal|office|default|off]',
85
87
  'cmd.lang.desc': 'screen language — 한국어 / English',
86
88
  'cmd.lang.arg': '[ko|en]',
87
89
  'cmd.init.desc': 'create a DEEL.md rules file',
@@ -129,7 +131,7 @@ export const en = {
129
131
  'head.shiftTab': 'Shift+Tab',
130
132
  'head.shiftTabHint': ' changes it · ',
131
133
  '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',
134
+ 'head.newlineHint': 'New line: end a line with space + ` then Enter — works everywhere (Alt+Enter only if your terminal sends it)',
133
135
  'head.thisPC': 'found',
134
136
  'head.skills': 'skills {n}',
135
137
  'head.commands': 'commands {n}',
@@ -184,6 +186,19 @@ export const en = {
184
186
  'bell.justRang': 'That was one ring. If you heard nothing, check your terminal’s bell setting.',
185
187
  'bell.appliesNow': 'Applies to this session right away.',
186
188
 
189
+ 'motion.now': 'Now:',
190
+ 'motion.plain': 'a one-cell spinner. Quiet',
191
+ 'motion.knight': 'a knight swings a sword. Sometimes gets flattened',
192
+ 'motion.animal': 'pixel animals wander about',
193
+ 'motion.office': 'twelve rows of office above the prompt — the work as a room',
194
+ 'motion.off': 'no drawing at all (for screen-reader users)',
195
+ 'motion.set': 'Switched to {것}.',
196
+ 'motion.appliesNow': 'Visible from the next frame. Saved to your config too.',
197
+ 'motion.unknown': '"{값}" is not one of the drawings.',
198
+ 'motion.pickOne': 'Pick one of:',
199
+ 'motion.envWins': 'DEEL_MOTION / DEEL_OFFICE is set, and the environment wins.',
200
+ 'motion.tooSmall': 'This terminal is {줄} x {칸} — the office needs at least {최소줄} rows and {최소칸} columns.',
201
+
187
202
  // ── Screen language ───────────────────────────────────────────────────
188
203
  'lang.now': 'Screen language is {이름}.',
189
204
  'lang.ko': '한국어 (Korean)',
package/src/i18n/index.js CHANGED
@@ -70,11 +70,54 @@ export function 언어잡기({ env = process.env, cfg = null } = {}) {
70
70
  return 지금;
71
71
  }
72
72
 
73
+ /*
74
+ * 조사는 앞 글자의 받침을 보고 갈린다.
75
+ *
76
+ * 끼워 넣는 값이 그때그때 달라지는 자리에서 조사를 글에 박아 두면
77
+ * "기사 으로 바꿨습니다" 같은 것이 나온다. 안 터지고, 검사도 안 잡고,
78
+ * 화면에서만 어색하다 — 그래서 오래 남는다.
79
+ *
80
+ * `{것:으로}` 처럼 자리 이름 뒤에 조사를 적으면 값을 보고 골라 붙인다.
81
+ * 영어 글에는 안 적으면 그만이라 `{것}` 그대로 둬도 된다.
82
+ *
83
+ * 값과 조사 사이에 뭔가 끼어야 할 때 — 따옴표가 대표적이다 — 이름 앞에
84
+ * `~` 를 붙이면 **조사만** 나온다: `"{값}"{~값:은는}` → `"없는거"는`.
85
+ */
86
+ const 조사표 = {
87
+ 은는: ['은', '는'],
88
+ 이가: ['이', '가'],
89
+ 을를: ['을', '를'],
90
+ 과와: ['과', '와'],
91
+ 으로: ['으로', '로'],
92
+ };
93
+
94
+ /**
95
+ * 이 값 뒤에 붙일 조사.
96
+ *
97
+ * 한글이 아닌 것으로 끝나면(영어 낱말·기호) **받침 없는 쪽**을 씁니다.
98
+ * 한국어 화면에 들어오는 값은 거의 한글이라 여기까지 오는 일이 드물고,
99
+ * 여기서 더 따지면 규칙만 늘고 맞는 자리는 안 늘어납니다.
100
+ */
101
+ export function 조사붙이기(값, 갈래) {
102
+ const 짝 = 조사표[갈래];
103
+ if (!짝) return '';
104
+ // 색 입힌 값이 그대로 들어온다(`\x1b[92m⌂\x1b[0m`). 벗기지 않으면 마지막
105
+ // 글자가 언제나 리셋 문자열의 'm' 이라, 무슨 값이 와도 답이 하나로 굳는다.
106
+ const 끝 = String(값 ?? '').replace(/\x1b\[[0-9;]*m/g, '').trimEnd().slice(-1);
107
+ const cp = 끝.codePointAt(0);
108
+ if (cp === undefined || cp < 0xac00 || cp > 0xd7a3) return 짝[1];
109
+ const 받침 = (cp - 0xac00) % 28;
110
+ if (받침 === 0) return 짝[1];
111
+ // `으로` 만 다르다. ㄹ(받침 8) 뒤에서는 받침이 없는 것처럼 '로' 가 붙는다.
112
+ if (갈래 === '으로' && 받침 === 8) return 짝[1];
113
+ return 짝[0];
114
+ }
115
+
73
116
  /**
74
117
  * 말 한 마디.
75
118
  *
76
119
  * @param {string} 열쇠
77
- * @param {Record<string, string|number>} [채움] `{이름}` 자리에 끼워 넣을 것
120
+ * @param {Record<string, string|number>} [채움] `{이름}` · `{이름:조사}` 자리에 끼워 넣을 것
78
121
  * @returns {string}
79
122
  */
80
123
  export function 말(열쇠, 채움 = null) {
@@ -93,8 +136,14 @@ export function 말(열쇠, 채움 = null) {
93
136
  * 안 준 자리는 그대로 둔다. 지워 버리면 "0개 턴" 처럼 틀린 말이 되고,
94
137
  * 틀린 숫자는 안 적힌 것보다 나쁘다.
95
138
  */
96
- return String(글).replace(/\{([^{}\s]+)\}/g, (통째, 이름) =>
97
- (Object.prototype.hasOwnProperty.call(채움, 이름) ? String(채움[이름]) : 통째));
139
+ return String(글).replace(/\{([^{}\s:]+)(?::([^{}\s]+))?\}/g, (통째, 이름, 갈래) => {
140
+ const 조사만 = 이름.startsWith('~');
141
+ const 진짜이름 = 조사만 ? 이름.slice(1) : 이름;
142
+ if (!Object.prototype.hasOwnProperty.call(채움, 진짜이름)) return 통째;
143
+ const 값 = String(채움[진짜이름]);
144
+ if (조사만) return 갈래 ? 조사붙이기(값, 갈래) : '';
145
+ return 갈래 ? 값 + 조사붙이기(값, 갈래) : 값;
146
+ });
98
147
  }
99
148
 
100
149
  /** 이 열쇠가 지금 언어로 옮겨져 있나. 검사와 /lang 이 본다. */
package/src/i18n/ko.js CHANGED
@@ -69,6 +69,8 @@ export const ko = {
69
69
  'cmd.level.arg': '[수준]',
70
70
  'cmd.bell.desc': '다 됐을 때·물어볼 때 종소리와 창 제목 (on/off)',
71
71
  'cmd.bell.arg': '[on|off]',
72
+ 'cmd.motion.desc': '일하는 동안 뭐가 도나 — 기사·동물·사무실',
73
+ 'cmd.motion.arg': '[기사|동물|사무실|기본|끔]',
72
74
  'cmd.lang.desc': '화면 말 — 한국어 / English',
73
75
  'cmd.lang.arg': '[ko|en]',
74
76
  'cmd.init.desc': 'DEEL.md 규칙 파일 만들기',
@@ -103,7 +105,7 @@ export const ko = {
103
105
  'head.outside': '바깥',
104
106
  'head.offlineLock': '[오프라인 잠금]',
105
107
  'head.nowhereElse': '← 여기 말고는 어디로도 안 갑니다',
106
- 'head.glyphHint': '상태줄 맨 앞 {} 이 뜻입니다. 바깥으로 가면 {} 바뀝니다.',
108
+ 'head.glyphHint': '상태줄 맨 앞 {안:이가} 이 뜻입니다. 바깥으로 가면 {밖:으로} 바뀝니다.',
107
109
  'head.conn': '연결',
108
110
  'head.streaming': '스트리밍',
109
111
  'head.noStreaming': '스트리밍 없음',
@@ -116,7 +118,7 @@ export const ko = {
116
118
  'head.shiftTab': 'Shift+Tab',
117
119
  'head.shiftTabHint': '으로 바꿉니다 · ',
118
120
  'head.tabHint': 'Tab 은 치던 / 명령을 채웁니다',
119
- 'head.newlineHint': '줄바꿈: Alt+Enter(터미널이 지원하면) 또는 줄 끝에 빈칸+` 를 붙이고 Enter',
121
+ 'head.newlineHint': '줄바꿈: 줄 끝에 빈칸+` 를 붙이고 Enter — 어디서나 됩니다 (Alt+Enter 는 터미널이 보내 줄 때만)',
120
122
  'head.thisPC': '이 PC',
121
123
  'head.skills': '스킬 {n}',
122
124
  'head.commands': '명령 {n}',
@@ -171,12 +173,25 @@ export const ko = {
171
173
  'bell.justRang': '방금 한 번 울렸습니다. 안 들렸으면 터미널 설정에서 소리를 확인해 주세요.',
172
174
  'bell.appliesNow': '이 세션에도 바로 먹습니다.',
173
175
 
176
+ 'motion.now': '지금:',
177
+ 'motion.plain': '한 칸짜리 돌림표. 조용합니다',
178
+ 'motion.knight': '중세 기사가 칼을 휘두릅니다. 가끔 깔려서 넘어집니다',
179
+ 'motion.animal': '픽셀 동물들이 돌아다닙니다',
180
+ 'motion.office': '입력칸 위에 사무실 12줄. 지금 무슨 일이 도는지를 방으로',
181
+ 'motion.off': '그림을 아예 끕니다 (화면 읽기 프로그램을 쓰는 자리)',
182
+ 'motion.set': '{것:으로} 바꿨습니다.',
183
+ 'motion.appliesNow': '다음 판부터 바로 보입니다. 설정에도 남았습니다.',
184
+ 'motion.unknown': '"{값}"{~값:은는} 모르는 그림입니다.',
185
+ 'motion.pickOne': '고를 수 있는 것:',
186
+ 'motion.envWins': '환경변수(DEEL_MOTION·DEEL_OFFICE)가 켜져 있어 그쪽이 이깁니다.',
187
+ 'motion.tooSmall': '이 터미널은 {줄}줄 × {칸}칸입니다 — 사무실은 {최소줄}줄 × {최소칸}칸부터 뜹니다.',
188
+
174
189
  // ── 화면 말 (/lang) ───────────────────────────────────────────────────
175
190
  'lang.now': '화면 말은 지금 {이름} 입니다.',
176
191
  'lang.ko': '한국어',
177
192
  'lang.en': 'English (영어)',
178
193
  'lang.howto': '바꾸려면',
179
- 'lang.changed': '화면 말을 {이름} 으로 바꿨습니다.',
194
+ 'lang.changed': '화면 말을 {이름:으로} 바꿨습니다.',
180
195
  'lang.unknown': 'ko 나 en 으로 적어 주세요.',
181
196
  'lang.progress': '{언어}: {옮김}/{전체} 마디가 옮겨져 있습니다. 나머지는 한국어로 나옵니다.',
182
197
  'lang.partial': '아직 다 안 옮겼습니다 — 빈칸을 내는 대신 한국어를 그대로 냅니다.',
package/src/repl.js CHANGED
@@ -9,7 +9,7 @@ import { 보이기 as 인트로, 기본곁말 } from './ui/intro.js';
9
9
  import { 언어잡기 } from './i18n/index.js';
10
10
  import { 화면고르기 } from './ui/screen.js';
11
11
  import { STAGES } from './agent/effort.js';
12
- import { handle, COMMANDS, 미리보기끄기 } from './commands.js';
12
+ import { handle, COMMANDS, 미리보기끄기, 적용하기 as 그림적용 } from './commands.js';
13
13
  import { next as nextWork, get as getWork, canWrite, 보일이름, 보일한줄 } from './agent/modes.js';
14
14
  import { route } from './agent/route.js';
15
15
  import { run } from './agent/loop.js';
@@ -196,6 +196,10 @@ export async function chatLoop(opts = {}) {
196
196
  */
197
197
  const 알림 = { 켬: cfg.bell !== false, 폴더: basename(root) };
198
198
 
199
+ // 지난번에 /motion 으로 골라 둔 그림을 되살린다. 화면을 세우기 **전에**
200
+ // 해야 첫 판부터 맞다 — 뒤에 하면 켜자마자 한 번은 기본 그림이 스친다.
201
+ 그림적용(cfg.motion);
202
+
199
203
  /*
200
204
  * 화면 말을 여기서 한 번 정한다.
201
205
  *
@@ -1114,12 +1118,17 @@ export async function chatLoop(opts = {}) {
1114
1118
  clearThinking();
1115
1119
  if (streamed) { 답비우기(); say(''); streamed = false; }
1116
1120
  화면.일바꿈(갈래고르기(ev.names?.[0]));
1121
+ // 사무실은 이만큼 자리를 채운다 — 이것이 진짜로 동시에 도는 수다.
1122
+ 화면.함께갱신(ev.count);
1117
1123
  say('');
1118
1124
  say(` ${c.gray(`${ev.count}개를 함께 돌립니다`)} ${c.gray('·')} ${c.gray(ev.names.join(' '))}`);
1119
1125
  break;
1120
1126
 
1121
1127
  case 'tool':
1122
1128
  tools++;
1129
+ // 결과가 오면 그 묶음은 끝난 것이다. 안 지우면 다음 한 개짜리
1130
+ // 도구까지 여럿이 도는 것처럼 보인다 — 자리 수가 거짓이 된다.
1131
+ 화면.함께갱신(0);
1123
1132
  // 걸러져 나온 것(인자가 잘렸거나, 모르는 도구거나, 거부된 것)은
1124
1133
  // '시작' 을 안 거쳤다. 그래서 '생각 중…' 줄이 안 지워진 채로 결과가
1125
1134
  // 그 줄 뒤에 가서 붙고, 이름도 없이 "└ 인자가 잘렸습니다" 만 남는다.
@@ -1422,6 +1431,10 @@ export async function chatLoop(opts = {}) {
1422
1431
  // 어떻게 끝났든 일하는 표시는 반드시 걷는다. 오류로 빠져나온 길에서
1423
1432
  // 안 걷으면 돌아가는 표시가 화면에 붙박이로 남고, 시계도 계속 돈다.
1424
1433
  화면.일끝();
1434
+ // 중간에 끊겨도 자리는 비운다. 안 비우면 다음 턴이 시작될 때까지
1435
+ // 아무도 안 도는 방에 사람이 앉아 있다.
1436
+ 화면.함께갱신(0);
1437
+ 화면.하위갱신(0);
1425
1438
  clearInterval(제목시계);
1426
1439
  /*
1427
1440
  * 오래 걸린 턴만 알린다. "안녕" 에 1초 만에 답할 때마다 딩 소리가 나면
@@ -355,6 +355,16 @@ export class InputBox {
355
355
  this.할일들 = [];
356
356
  // 지금 도는 하위 작업 수 (사무실 자리 수). 모르면 0 이다.
357
357
  this.도는하위 = 0;
358
+ /*
359
+ * 지금 **한꺼번에** 도는 도구 수.
360
+ *
361
+ * 하위 작업은 순서대로 돈다(loop.js 가 하위를 `for await` 로 기다린다).
362
+ * 그래서 하위 수만 보면 자리는 거의 늘 하나다 — 방이 늘 한 명짜리로
363
+ * 보이던 진짜 이유가 이것이다. 실제로 동시에 도는 것은 읽기 도구 묶음
364
+ * 쪽이고(loop.js 의 Promise.all), 그 개수는 이미 알고 있었는데 방만
365
+ * 안 보고 있었다.
366
+ */
367
+ this.함께도는 = 0;
358
368
  }
359
369
 
360
370
  /** 할 일이 바뀌었다. 사무실 화이트보드가 이걸 본다. */
@@ -367,6 +377,11 @@ export class InputBox {
367
377
  this.도는하위 = Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
368
378
  }
369
379
 
380
+ /** 한꺼번에 도는 도구 수. 묶음이 끝나면 0 으로 돌아온다. */
381
+ 함께갱신(n) {
382
+ this.함께도는 = Number.isFinite(n) && n > 1 ? Math.trunc(n) : 0;
383
+ }
384
+
370
385
  /** 대화 글이 줄 중간까지 쓰였다. 줄이 끝날 때까지 상자를 안 세운다. */
371
386
  줄중간표시() {
372
387
  this.지우기();
@@ -526,7 +541,7 @@ export class InputBox {
526
541
  this.그린높이 = 높이;
527
542
  }
528
543
  const 방 = 사무실켜나(process.stdout.rows, 칸수)
529
- ? 사무실줄들(사무실상태(this.session, this.일감, this.틱, this.할일들, this.도는하위), 폭)
544
+ ? 사무실줄들(사무실상태(this.session, this.일감, this.틱, this.할일들, this.도는하위, this.함께도는), 폭)
530
545
  : null;
531
546
 
532
547
  const 짜기 = (방줄들) => 프레임({
package/src/ui/motion.js CHANGED
@@ -469,8 +469,24 @@ const 이름표 = {
469
469
  기사: '기사', knight: '기사', 동물: '동물', animal: '동물', animals: '동물',
470
470
  기본: '기본', default: '기본', plain: '기본',
471
471
  };
472
+ /*
473
+ * 설정 파일에서 고른 그림. `/motion` 이 여기에 넣는다.
474
+ *
475
+ * **파일을 여기서 읽으면 안 된다.** 이 함수는 90ms 마다 불린다 — 판마다
476
+ * config.json 을 여는 셈이 된다. 그래서 켤 때 한 번 읽어서 이 값에 넣어 두고,
477
+ * 명령으로 바꿀 때 같이 갈아 끼운다.
478
+ *
479
+ * 환경변수가 이긴다. 한 번 띄우는 동안만 다르게 보고 싶을 때
480
+ * `DEEL_MOTION=기사 deel` 로 덮어쓸 수 있어야 하고, 검사도 그 길로 잰다.
481
+ */
482
+ let 고른그림 = null;
483
+ export function 그림고르기설정(값) {
484
+ 고른그림 = 이름표[String(값 ?? '').trim().toLowerCase()] ?? 이름표[String(값 ?? '').trim()] ?? null;
485
+ }
486
+
472
487
  export const 테마이름 = () => 이름표[String(process.env.DEEL_MOTION ?? '').trim().toLowerCase()]
473
488
  ?? 이름표[String(process.env.DEEL_MOTION ?? '').trim()]
489
+ ?? 고른그림
474
490
  ?? '기본';
475
491
  export const 테마 = () => 테마들[테마이름()] ?? 테마들.기본;
476
492
 
@@ -481,7 +497,10 @@ export const 테마 = () => 테마들[테마이름()] ?? 테마들.기본;
481
497
  * 자리가 있다. 그럴 때 DEEL_NO_MOTION=1 이면 예전처럼 한 칸짜리 돌림표만 돈다.
482
498
  * 아예 못 끄게 해 두면 그런 사람은 도구를 못 쓴다.
483
499
  */
484
- export const = () => !!process.env.DEEL_NO_MOTION;
500
+ let 설정으로껐나 = false;
501
+ /** `/motion 끔` 이 여기에 넣는다. 환경변수는 따로 살아 있고, 둘 중 하나면 꺼진다. */
502
+ export function 끔설정(값) { 설정으로껐나 = !!값; }
503
+ export const 끔 = () => !!process.env.DEEL_NO_MOTION || 설정으로껐나;
485
504
 
486
505
  // 못 끄는 자리에서 쓸 한 칸짜리. working.js 의 돌림틀과 같은 것이다.
487
506
  const 한칸 = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];