deel-local-cli 1.20.13 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.ko.md +96 -78
  2. package/README.md +119 -81
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +23 -3
  7. package/src/acp/serve.js +333 -25
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +253 -26
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +300 -0
  58. package/src/commands/model.js +852 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +452 -39
  64. package/src/configexplain.js +125 -14
  65. package/src/doctor.js +164 -26
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +507 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +11 -1
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +444 -36
  113. package/src/tools/fig.js +21 -4
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1029 -145
  118. package/src/tools/jobs.js +325 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +366 -56
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +312 -43
  127. package/src/tools/webfetch.js +184 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
  150. /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
  151. /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
  152. /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
  153. /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
@@ -0,0 +1,328 @@
1
+ // 슬래시 명령 — 보여 주는 것: 도움말 · 화면 수준 · 생각 세기 · 컨텍스트 · 작업 모드 목록.
2
+ // commands.js 에서 역할별로 나눠 옮겼다(2.0.0). 명령을 가르는 자리(handle)는 그대로 commands.js 에 있다.
3
+ import { c, say, rule, pad, bar, mark, width, clip } from '../ui/ansi.js';
4
+ import { load } from '../config.js';
5
+ import { 걸음수 } from '../agent/budget.js';
6
+ import { 말, 언어 } from '../i18n/index.js';
7
+ import { 나가는눈금 } from '../backend/adapter.js';
8
+ import { PROFILES, LEVELS as THINK_LEVELS, table as effortTable, 가벼운강도, shiftLevel } from '../agent/effort.js';
9
+ import { 전선말 } from '../backend/wire.js';
10
+ import { MODES as WORK_MODES, ORDER as WORK_ORDER, DEFAULT as WORK_DEFAULT, normalize as normWork, canWrite, 보일이름, 보일한줄 } from '../agent/modes.js';
11
+ import { LEVELS, ORDER as LEVEL_ORDER, DEFAULT as LEVEL_DEFAULT, normalize as normLevel, shows as levelShows } from '../ui/level.js';
12
+ import { COMMANDS, 설정남기기 } from './common.js';
13
+
14
+ export function help(session) {
15
+ const level = session?.level ?? LEVEL_DEFAULT;
16
+ const 쉬움 = !levelShows(level, '__전부__'); // 개발자면 show 가 null 이라 전부 참
17
+ say('');
18
+ rule(말(쉬움 ? 'help.titleCommon' : 'help.title'), 70);
19
+ let 감춘수 = 0;
20
+ for (const [n, m] of Object.entries(COMMANDS)) {
21
+ if (n === 'quit') continue;
22
+ if (!levelShows(level, n)) { 감춘수++; continue; }
23
+ say(` ${c.cyan(pad('/' + n + (m.arg ? ' ' + m.arg : ''), 22))} ${c.gray(m.desc)}`);
24
+ }
25
+ say('');
26
+ // 감춘 것은 '못 쓰는 것' 이 아니다. 그 말을 분명히 해 둔다.
27
+ if (감춘수) {
28
+ say(` ${c.gray(말('help.moreHidden', { n: 감춘수 }))}`);
29
+ say(` ${c.gray(말('help.showAll'))} ${c.cyan('/level developer')}`);
30
+ say('');
31
+ }
32
+ say(` ${c.gray(말('help.restGoesToModel'))}`);
33
+ say('');
34
+ }
35
+
36
+ // /level — 수준 보기·바꾸기. 설정 파일에 남겨 다음에도 그대로 쓴다.
37
+ export function showLevel(session, arg) {
38
+ if (!arg) {
39
+ rule('사용자 수준', 70);
40
+ for (const k of LEVEL_ORDER) {
41
+ const lv = LEVELS[k];
42
+ const 지금 = k === (normLevel(session.level) ?? LEVEL_DEFAULT);
43
+ say(` ${지금 ? c.hgreen('●') : c.gray('·')} ${지금 ? c.bold(c.white(pad(lv.name, 10))) : c.gray(pad(lv.name, 10))}${c.gray(lv.hint)}`);
44
+ }
45
+ say('');
46
+ say(` ${c.gray('안전 장치는 두 수준이 똑같습니다 — 되돌리기·작업 범위·위험 명령 차단.')}`);
47
+ say(` ${c.gray('수준은 무엇을 보여줄지만 정합니다.')}`);
48
+ say('');
49
+ say(` ${c.gray('바꾸려면')} ${c.cyan('/level 쉬움')} ${c.gray('또는')} ${c.cyan('/level 개발자')}`);
50
+ say('');
51
+ return;
52
+ }
53
+ const 골라진 = normLevel(arg);
54
+ if (!골라진) {
55
+ say(` ${mark.warn} 그런 수준은 없습니다: ${c.white(arg)} ${c.gray('(쉬움 · 개발자)')}`);
56
+ say('');
57
+ return;
58
+ }
59
+ session.level = 골라진;
60
+ /*
61
+ * 못 남기면 말한다. 바로 아래가 `✓ 개발자` 를 찍는데, 못 남긴 판에서는
62
+ * 그 말이 이번 판에서만 참이다 — 설정남기기() 가 아홉 자리에서 없앤 그
63
+ * 꼴인데 이 자리만 옛 모양으로 남아 있었다.
64
+ */
65
+ try {
66
+ const cfg = load();
67
+ cfg.level = 골라진;
68
+ 설정남기기(cfg);
69
+ } catch (e) {
70
+ /*
71
+ * 설정 파일 자체를 못 읽는 경우다(JSON 이 깨졌다). 바로 위 머리글이
72
+ * 「못 남기면 말한다」 라고 적어 놓고 여기만 통째로 삼키고 있었다.
73
+ * 켤 때는 repl 이 말해 주지만 **대화 도중에 깨진 경우는 아무도 안 말한다** —
74
+ * 화면은 `✓ 개발자` 를 찍고 다음에 켜면 그대로 쉬움이다.
75
+ */
76
+ say(` ${mark.warn} ${c.yellow(말('common.cfgSaveFailed', { 왜: clip(String(e?.message ?? e), 70) }))}`);
77
+ }
78
+ const lv = LEVELS[골라진];
79
+ say('');
80
+ say(` ${mark.ok} ${c.bold(lv.name)} ${c.gray('— ' + lv.hint)}`);
81
+ say(` ${c.gray('안전 장치는 그대로입니다.')}`);
82
+ say('');
83
+ }
84
+
85
+ /**
86
+ * 찾은 낱말에 색을 입힌다.
87
+ *
88
+ * 토막만 보여 주면 **왜 이게 걸렸는지** 안 보인다. 특히 조사를 떼고 찾기
89
+ * 때문에("인코딩을" 로 "인코딩" 을 찾는다) 눈으로는 안 맞는 것처럼 보이는
90
+ * 경우가 있다. 맞은 자리를 칠해 주면 그 의심이 사라진다.
91
+ *
92
+ * 낱말들은 **소문자로** 온다 — 유일한 부르는 자리(commands.js 의 /recall)가
93
+ * 낱말쪼개기() 가 소문자로 쪼갠 것을 그대로 넘긴다. 여기서 다시 안 낮춘다.
94
+ */
95
+ export function 강조(글, 낱말들) {
96
+ let 조각 = [{ 글, 맞음: false }];
97
+ for (const w of 낱말들) {
98
+ const 다음 = [];
99
+ for (const p of 조각) {
100
+ if (p.맞음) { 다음.push(p); continue; }
101
+ const 낮은 = p.글.toLowerCase();
102
+ let i = 0;
103
+ let 자리 = 낮은.indexOf(w);
104
+ while (자리 >= 0) {
105
+ if (자리 > i) 다음.push({ 글: p.글.slice(i, 자리), 맞음: false });
106
+ 다음.push({ 글: p.글.slice(자리, 자리 + w.length), 맞음: true });
107
+ i = 자리 + w.length;
108
+ 자리 = 낮은.indexOf(w, i);
109
+ }
110
+ if (i < p.글.length) 다음.push({ 글: p.글.slice(i), 맞음: false });
111
+ }
112
+ 조각 = 다음;
113
+ }
114
+ return 조각.map((p) => (p.맞음 ? c.hyellow(p.글) : c.gray(p.글))).join('');
115
+ }
116
+
117
+ // 추론 강도는 값 하나가 아니라 '단계별 배분' 이다. 그 배분을 눈에 보이게 그린다.
118
+ /**
119
+ * 지금 추론 강도가 어떻게 되어 있는지.
120
+ *
121
+ * 기본은 **한 줄**이다. 사람이 알고 싶은 것은 '지금 얼마나 생각하나' 이지
122
+ * 단계별 표가 아니다. 표는 /think 자세히 로 뺐다.
123
+ *
124
+ * 쉬움 수준에서는 배분 이야기를 아예 안 꺼낸다 — 고를 일이 없는 사람에게
125
+ * 고르는 법을 보여 주면 그것부터 걱정하게 된다.
126
+ */
127
+ export function showThink(session, { 자세히 = false } = {}) {
128
+ const b = session.breakdown();
129
+ const 아는상한 = session.conn.maxTokens ?? session.conn.maxOut ?? null;
130
+ const t = effortTable(session.think, session.effort, { ctx: b.total, used: b.used, max: 아는상한 });
131
+ const 개발자 = session.level === '개발자';
132
+ const 단계 = t.rows.map((r) => `${r.label} ${r.level}`).join(c.gray(' · '));
133
+
134
+ /*
135
+ * 이 화면은 여태 통째로 한국어였다.
136
+ *
137
+ * `/lang en` 으로 켠 사람에게 「추론 강도 medium (첫 판단 medium · …)」 이
138
+ * 뜨면 아무것도 안 읽힌다. 명령 이름만 영어고 화면이 한국어인 자리는
139
+ * 「덜 옮겨졌다」 가 아니라 「고장 났다」 로 읽힌다 — i18n/index.js 머리말.
140
+ *
141
+ * 예로 드는 명령도 말을 따라간다. `/think 배분 절약` 은 영어로 켠 사람이
142
+ * 칠 수 없는 글자다. 두 이름 다 진짜로 받는다(위 배분말·자세히 갈래).
143
+ */
144
+ const 한국어 = 언어() === 'ko';
145
+ const 배분예 = 한국어 ? '/think 배분 절약' : '/think profile save';
146
+ const 자세히예 = 한국어 ? '/think 자세히' : '/think detail';
147
+ say('');
148
+ say(` ${c.gray(말('think.effort'))} ${c.bold(session.think)} ${c.gray(`(${단계})`)}`);
149
+ /*
150
+ * ── 정한 값과 **실제로 나가는 값**을 같이 보여 준다 ──────────────────
151
+ *
152
+ * 여기가 여태 비어 있던 자리다. `/think max` 를 쳐도 전선이 max 를 안
153
+ * 받으면 high 가 나갔고, 화면에는 그 사실이 어디에도 없었다. 사람은
154
+ * 세게 생각하라고 시켰다고 믿는다.
155
+ *
156
+ * 자동 조절도 같이 적는다. 정한 값이 천장이 되므로, 「max 라고 했는데 왜
157
+ * medium 인가」 에 답할 수 있어야 한다.
158
+ */
159
+ /*
160
+ * 자동 조절이 **실제로 무언가를 바꿀 때만** 적는다.
161
+ *
162
+ * `/think low` 로 정해 둔 사람에게 「가벼운 말은 low 까지 낮춰 씁니다」 는
163
+ * 아무 말도 아니다 — 이미 low 다. 아무 일도 안 하는 줄을 화면에 남기면
164
+ * 사람은 그 줄을 읽는 데 시간을 쓰고 아무것도 얻지 못한다.
165
+ */
166
+ if (session.autoThink !== false) {
167
+ const 가장낮게 = 가벼운강도(session.think);
168
+ if (가장낮게 !== session.think) {
169
+ say(` ${c.gray(pad(말('think.auto'), 한국어 ? 8 : 10))}${c.gray(말('think.autoNote', { 천장: session.think, 지금: 가장낮게 }))}`);
170
+ }
171
+ }
172
+ if (session.conn?.전선) {
173
+ const 카드 = session.conn.전선;
174
+ /*
175
+ * **정말로 나가는 것만** 적는다.
176
+ *
177
+ * 여태 여기서 `눈금맞추기()` 를 직접 불렀다. 그 함수는 카드가 아는 눈금으로
178
+ * 낮추는 일만 하고 그 눈금이 실릴 칸이 있는지는 모른다 — 그래서 눈금이
179
+ * 아예 안 나가는 전선(budget·boolean·none)에서도 「지금 나가는 값 high」
180
+ * 가 떴다. 몸을 만드는 자리와 같은 함수를 쓴다(backend/adapter.js).
181
+ */
182
+ const 나가는것 = 나가는눈금(session.conn.kind, 카드, session.think);
183
+ /*
184
+ * ── 「끈다」 가 정말로 꺼지는 창구인가 ────────────────────────────
185
+ *
186
+ * off 인데 나갈 말이 없으면 두 가지 중 하나다.
187
+ *
188
+ * a. 이 규격에서는 **칸을 안 싣는 것이 곧 끄는 것**이다.
189
+ * anthropic 은 thinking 을 빼면 안 생각하고, ollama 는 think:false
190
+ * 를 싣는다. 이쪽은 아무 말도 안 해도 된다.
191
+ * b. 이 규격에서는 칸을 안 실으면 **서버 기본값**으로 돈다. openai 꼴의
192
+ * 추론 모델이 그렇다 — reasoning_effort 를 빼면 서버가 제 기본값
193
+ * (보통 medium)으로 생각한다. 여기서 아무 말도 안 하면 화면은 off
194
+ * 라고 적어 두고 실제로는 생각이 도는 셈이 된다.
195
+ *
196
+ * b 를 잠자코 두면 화면이 거짓말을 한다. 창구가 끄는 말을 알려 준 적이
197
+ * 없으면 우리는 못 끈다 — 못 끄면 못 끈다고 적는다.
198
+ */
199
+ const 못끄나 = session.think === 'off' && !나가는것 && 카드.생각형식 === 'effort';
200
+ say(` ${c.gray(pad(말('think.wire'), 한국어 ? 8 : 10))}${c.gray(전선말(카드))}`
201
+ + (나가는것 ? ` ${c.gray(말('think.wireSends', { 값: 나가는것 }))}` : '')
202
+ + (못끄나 ? ` ${c.yellow(말('think.wireNoOff'))}` : ''));
203
+ }
204
+ if (개발자 || 자세히) say(` ${c.gray(pad(말('think.profile'), 한국어 ? 8 : 10))}${c.bold(t.name)} ${c.gray(t.desc)}`);
205
+
206
+ if (자세히) {
207
+ say('');
208
+ say(` ${c.gray(pad(말('think.col.stage'), 12) + pad(말('think.col.level'), 10) + pad(말('think.col.cap'), 10) + 말('think.col.when'))}`);
209
+ for (const r of t.rows) {
210
+ const 화살 = r.moved > 0 ? c.yellow('↑') : r.moved < 0 ? c.cyan('↓') : c.gray('·');
211
+ say(` ${pad(r.label, 12)}${화살} ${pad(r.level, 8)}${pad(r.cap.toLocaleString(), 10, 'right')} ${c.gray(r.why)}`);
212
+ }
213
+ say('');
214
+ // 상한이 어디서 왔는지 밝힌다. 세 줄이 같은 값일 때 그게 고장인지 아닌지
215
+ // 이 한 줄로 갈린다 — 아는 상한이 낮으면 셋이 같아지는 것이 맞다.
216
+ const 어디서 = session.conn.maxTokens ? 말('think.cap.you')
217
+ : session.conn.maxOut ? 말('think.cap.server') : 말('think.cap.guess');
218
+ say(` ${c.gray(말('think.cap.head'))} ${c.white((아는상한 ?? 16384).toLocaleString())} ${c.gray(어디서)}${c.gray(말('think.cap.tail'))} ${c.cyan('/out')}`);
219
+ say(` ${c.gray(말('think.ctx'))} ${c.white(t.ctx.toLocaleString())}${c.gray(말('think.ctx.used'))}${c.white(t.used.toLocaleString())}`);
220
+ say('');
221
+ say(` ${c.gray(말('think.effort'))} ${THINK_LEVELS.join(' · ')} ${c.gray(말('think.eg'))} ${c.cyan('/think high')}`);
222
+ say(` ${c.gray(말('think.profile'))} ${Object.entries(PROFILES).map(([k, v]) => { const n = 한국어 ? v.name : (v.en ?? v.name); return n === k ? k : `${k}(${n})`; }).join(' · ')} ${c.gray(말('think.eg'))} ${c.cyan(배분예)}`);
223
+ } else if (개발자) {
224
+ say(` ${c.gray(말('think.effort'))} ${c.cyan('/think high')} ${c.gray(말('think.profile'))} ${c.cyan(배분예)} ${c.gray(말('think.detail'))} ${c.cyan(자세히예)}`);
225
+ } else {
226
+ /*
227
+ * 다음 칸을 **지금 값에서** 셈한다.
228
+ *
229
+ * 여태 이 두 자리는 high · low 로 박혀 있었다. 눈금이 넷일 때는 그럭저럭
230
+ * 맞았는데 xhigh 가 늘면서 티가 났다 — max 로 쓰고 있는 사람에게
231
+ * 「더 세게 → /think high」 라고 적어 준다. high 는 지금보다 **약하다.**
232
+ *
233
+ * 끝에 서 있으면 그쪽은 아예 안 적는다. 갈 데가 없는 길을 적어 주는 것은
234
+ * 없는 기능을 알려 주는 것과 같다.
235
+ */
236
+ const 위 = shiftLevel(session.think, 1);
237
+ const 아래 = shiftLevel(session.think, -1);
238
+ const 조각 = [];
239
+ if (위 !== session.think) 조각.push(`${c.gray(말('think.harder'))} ${c.cyan(`/think ${위}`)}`);
240
+ if (아래 !== session.think && 아래 !== 'off') 조각.push(`${c.gray(말('think.faster'))} ${c.cyan(`/think ${아래}`)}`);
241
+ if (조각.length) say(` ${조각.join(' ')}`);
242
+ }
243
+
244
+ if (!session.conn.think && session.think !== 'off') {
245
+ say(` ${c.yellow(말('think.nomodel'))} ${c.gray(말('think.nomodel.note'))}`);
246
+ }
247
+ say('');
248
+ }
249
+
250
+ export function showContext(session) {
251
+ const b = session.breakdown();
252
+ say('');
253
+ rule(말('scr.context'), 70);
254
+ say(` ${c.bold(session.conn.model)} ${c.gray('·')} ${말('unit.tokens', { n: b.total.toLocaleString() })}`);
255
+ say('');
256
+ say(` ${bar(b.used, b.total, 32)} ${b.used.toLocaleString()} / ${b.total.toLocaleString()} ${c.gray(`${Math.round((b.used / b.total) * 100)}%`)}`);
257
+ say('');
258
+ for (const r of b.rows) {
259
+ if (!r.n) continue;
260
+ say(` ${c.gray(pad(r.label, 26))} ${pad(r.n.toLocaleString(), 8, 'right')}`);
261
+ }
262
+ say(` ${c.gray('─'.repeat(35))}`);
263
+ say(` ${c.gray(pad(말('ctx.left'), 26))} ${pad(b.left.toLocaleString(), 8, 'right')}`);
264
+ say('');
265
+ say(` ${c.gray(`/compact ${말('ctx.compactHint')} /clear ${말('ctx.clearHint')}`)}`);
266
+ /*
267
+ * 추정이라고만 적어 두면 사람은 얼마나 믿어야 할지 모른다. 서버가 알려 준
268
+ * 실제값에 맞춰 가고 있으면 그 사실을 적는다 — '추정' 과 '맞춰 본 추정' 은
269
+ * 믿을 만한 정도가 다르다.
270
+ */
271
+ if (b.보정잰것 > 0) {
272
+ const 차이 = Math.round((b.보정 - 1) * 100);
273
+ say(` ${c.gray(말('ctx.calibrated', { 부호: 차이 >= 0 ? '+' : '', 퍼센트: 차이, 번: b.보정잰것 }))}`);
274
+ } else {
275
+ say(` ${c.gray(말('ctx.estimate'))}`);
276
+ }
277
+ /*
278
+ * 재 봤는데 **못 믿어서 안 쓴** 것도 말한다.
279
+ *
280
+ * session.배운다() 는 서버가 알려 준 값이 우리 추정의 절반 아래거나 두 배
281
+ * 위면 표본을 버린다. 튄 값 하나에 게이지가 휘둘리면 안 되니 버리는 것은
282
+ * 맞다. 그런데 버린 것을 아무 데도 안 남기면, 「아직 한 번도 못 재 봤다」 와
283
+ * 「재 봤는데 세 배가 나와서 못 믿었다」 가 화면에서 똑같아진다.
284
+ *
285
+ * 뒤엣것은 우리 추정이 크게 틀렸다는 단서다. 그 말을 안 하면 게이지는 영영
286
+ * 짐작인 채로 도는데 사람은 그저 「추정입니다」 만 읽는다.
287
+ */
288
+ if (session?.보정버림 > 0) {
289
+ const 배 = session.보정마지막버린비율;
290
+ say(` ${c.gray(`서버가 알려 준 값이 우리 추정과 너무 달라 ${session.보정버림}번은 안 썼습니다`
291
+ + (배 ? ` (마지막에 잰 것은 추정의 ${배.toFixed(1)}배)` : ''))}`);
292
+ say(` ${c.gray('이 창구에서는 위 숫자가 실제와 크게 다를 수 있습니다.')}`);
293
+ }
294
+ say('');
295
+ }
296
+
297
+ // /work 를 인자 없이 부르면 지금 모드와 고를 수 있는 것들을 보여 준다.
298
+ export function showWork(session) {
299
+ rule(말('work.title'), 70);
300
+ /*
301
+ * 이름 칸을 **제일 긴 이름에 맞춰** 잡는다.
302
+ *
303
+ * 8칸으로 못 박아 뒀더니 Orchestrator(12자)가 칸을 넘어 다음 칸을 밀었다.
304
+ * 화면에 `OrchestratorOrchestrator` 라고 붙어 나왔다 — 사진을 찍어 보고서야
305
+ * 알았다. 목록의 값은 칸이 맞아야 목록이다.
306
+ */
307
+ const 이름들 = WORK_ORDER.map((k) => 보일이름(WORK_MODES[k].id));
308
+ const 칸너비 = Math.max(...이름들.map((n) => width(n))) + 2;
309
+ // 영어 화면에서는 이름과 영문 이름이 같은 글자다. 같은 것을 두 번 적지 않는다.
310
+ const 영문칸 = 언어() === 'en' ? 0 : 14;
311
+
312
+ for (const k of WORK_ORDER) {
313
+ const w = WORK_MODES[k];
314
+ const 지금 = k === (normWork(session.work) ?? WORK_DEFAULT);
315
+ const 표 = 지금 ? c.hgreen('●') : c.gray('·');
316
+ const 보임 = pad(보일이름(w.id), 칸너비);
317
+ const 이름 = 지금 ? c.bold(c.white(보임)) : c.gray(보임);
318
+ say(` ${표} ${c.hcyan(w.glyph)} ${이름}${영문칸 ? c.gray(pad(w.en, 영문칸)) : ''}${c.gray(보일한줄(w.id))}`);
319
+ const 강도 = `${w.think ?? '—'}·${w.effort}`;
320
+ const 걸음 = 말('work.steps', { n: 걸음수(k, session.conn?.ctx) });
321
+ say(` ${canWrite(k) ? c.gray(말('work.canEdit')) : c.green(말('work.readOnly'))}`
322
+ + `${c.gray(` · ${말('work.think')} ${강도} · ${걸음}`)}`);
323
+ }
324
+ say('');
325
+ say(` ${c.gray(말('work.howTo', { a: c.cyan('/plan'), b: c.cyan('/code'), c: c.cyan('/debug'), 키: c.cyan('Ctrl+O') }))}`);
326
+ say(` ${c.gray(말('work.axis', { 명령: c.cyan('/mode') }))}`);
327
+ say('');
328
+ }