deel-local-cli 1.6.1 → 1.6.3
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.md +2 -0
- package/package.json +2 -2
- package/src/agent/askcheck.js +281 -0
- package/src/agent/asks.js +186 -0
- package/src/agent/compact.js +28 -1
- package/src/agent/loop.js +128 -3
- package/src/agent/route.js +54 -3
- package/src/repl.js +183 -10
- package/src/tools/convert.js +38 -10
- package/src/tools/desc.en.js +7 -3
- package/src/tools/fastgrep.js +8 -11
- package/src/tools/fsutil.js +13 -1
- package/src/tools/index.js +127 -19
- package/src/tools/outline.js +2 -1
- package/src/tools/spawn.js +99 -0
- package/src/tools/todo.js +28 -0
- package/src/ui/inputbox.js +11 -2
- package/src/ui/motion.js +30 -0
- package/src/ui/pastechip.js +87 -0
- package/src/ui/working.js +14 -0
package/src/tools/desc.en.js
CHANGED
|
@@ -134,12 +134,16 @@ export const 도구설명EN = {
|
|
|
134
134
|
},
|
|
135
135
|
},
|
|
136
136
|
Ask: {
|
|
137
|
-
desc: 'Ask the person one question at a genuine fork.
|
|
138
|
-
+ '
|
|
139
|
-
+ '
|
|
137
|
+
desc: 'Ask the person one question at a genuine fork. **First fill in `이해` with what you'
|
|
138
|
+
+ ' understood this request to be**, and only ask if something is genuinely left to decide.'
|
|
139
|
+
+ ' Never write "let me know" and stop; asking in prose ends the turn and throws away'
|
|
140
|
+
+ ' everything you have looked at. Give 2-4 options and they answer with a single number.'
|
|
140
141
|
+ ' **Never ask back something the user already said.** "Tidy up the files" is the answer —'
|
|
141
142
|
+ ' go and do it. Deciding how is your job, not theirs.',
|
|
142
143
|
params: {
|
|
144
|
+
이해: 'one line: what you understood this request to be. The person reads this line to judge'
|
|
145
|
+
+ ' whether you actually read them. "I understand your request" is not an understanding —'
|
|
146
|
+
+ ' name the actual work',
|
|
143
147
|
question: 'one sentence. Make it clear what has to be decided',
|
|
144
148
|
options: '2-4 choices, one line each, so the difference is visible',
|
|
145
149
|
},
|
package/src/tools/fastgrep.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// 결과가 없는 것이 아니라 우리가 못 물어본 것이다. 그때 '일치 없음' 을
|
|
25
25
|
// 돌려주면 사람은 없는 줄 안다. 그래서 실패는 실패로 두고 JS 로 다시 찾는다.
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import { 돌려보기 } from './spawn.js';
|
|
27
28
|
import { join } from 'node:path';
|
|
28
29
|
|
|
29
30
|
/*
|
|
@@ -99,7 +100,7 @@ export function 저장소인가(폴더) {
|
|
|
99
100
|
*
|
|
100
101
|
* @returns {{ok:true, 줄들:string[], 잘림:boolean} | {ok:false, 왜:string}}
|
|
101
102
|
*/
|
|
102
|
-
export function rg로찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000 }) {
|
|
103
|
+
export async function rg로찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, timeout = 20000, signal = null }) {
|
|
103
104
|
const 인자 = [
|
|
104
105
|
'--line-number',
|
|
105
106
|
'--no-heading',
|
|
@@ -132,9 +133,7 @@ export function rg로찾기({ 무늬, 자리, glob = null, 대소문자무시 =
|
|
|
132
133
|
// `--` 뒤로 넘겨서 무늬가 옵션으로 안 읽히게 한다. `-foo` 같은 무늬가 실제로 있다.
|
|
133
134
|
인자.push('--regexp', 무늬, '--', 자리);
|
|
134
135
|
|
|
135
|
-
const r =
|
|
136
|
-
encoding: 'utf8', timeout, windowsHide: true, maxBuffer: 32 * 1024 * 1024,
|
|
137
|
-
});
|
|
136
|
+
const r = await 돌려보기('rg', 인자, { timeout, signal });
|
|
138
137
|
// rg 는 못 찾으면 1, 진짜 탈이 나면 2 를 준다. 1 은 성공(빈 결과)이다.
|
|
139
138
|
if (r.error) return { ok: false, 왜: r.error.message };
|
|
140
139
|
if (r.status === 2) return { ok: false, 왜: (r.stderr ?? '').split('\n')[0] || 'rg 가 무늬를 못 읽었습니다' };
|
|
@@ -143,14 +142,12 @@ export function rg로찾기({ 무늬, 자리, glob = null, 대소문자무시 =
|
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
/** `git grep` 으로 찾는다. rg 가 없고 여기가 저장소일 때. */
|
|
146
|
-
export function git로찾기({ 무늬, 자리, 대소문자무시 = false, 최대 = 5000, timeout = 20000 }) {
|
|
145
|
+
export async function git로찾기({ 무늬, 자리, 대소문자무시 = false, 최대 = 5000, timeout = 20000, signal = null }) {
|
|
147
146
|
const 인자 = ['-C', 자리, '--no-pager', 'grep', '--line-number', '--no-color', '-I', '-E'];
|
|
148
147
|
if (대소문자무시) 인자.push('-i');
|
|
149
148
|
// 여기도 같은 목록으로 뺀다. git 은 경로무늬 앞머리에 마법을 붙여 빼낸다.
|
|
150
149
|
인자.push('-e', 무늬, '--', '.', ...안볼확장자.map((x) => `:(exclude,icase)*.${x}`));
|
|
151
|
-
const r =
|
|
152
|
-
encoding: 'utf8', timeout, windowsHide: true, maxBuffer: 32 * 1024 * 1024,
|
|
153
|
-
});
|
|
150
|
+
const r = await 돌려보기('git', 인자, { timeout, signal });
|
|
154
151
|
if (r.error) return { ok: false, 왜: r.error.message };
|
|
155
152
|
if (r.status !== 0 && r.status !== 1) {
|
|
156
153
|
return { ok: false, 왜: (r.stderr ?? '').split('\n')[0] || 'git grep 이 무늬를 못 읽었습니다' };
|
|
@@ -191,10 +188,10 @@ export function 줄가르기(줄) {
|
|
|
191
188
|
*
|
|
192
189
|
* @returns {{엔진:string, 줄들:Array<{파일,줄,내용}>, 잘림:boolean} | null}
|
|
193
190
|
*/
|
|
194
|
-
export function 빠르게찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000 }) {
|
|
191
|
+
export async function 빠르게찾기({ 무늬, 자리, glob = null, 대소문자무시 = false, 무시파일 = null, 최대 = 5000, signal = null }) {
|
|
195
192
|
const 것 = 엔진찾기();
|
|
196
193
|
if (것.rg) {
|
|
197
|
-
const r = rg로찾기({ 무늬, 자리, glob, 대소문자무시, 무시파일,
|
|
194
|
+
const r = await rg로찾기({ 무늬, 자리, glob, 대소문자무시, 무시파일, 최대, signal });
|
|
198
195
|
if (r.ok) return { 엔진: 'rg', 줄들: r.줄들.map(줄가르기).filter(Boolean), 잘림: r.잘림 };
|
|
199
196
|
// rg 가 무늬를 못 읽은 것일 수 있다(Rust regex 에는 되돌아보기가 없다).
|
|
200
197
|
// 그건 '없다' 가 아니라 '못 물어봤다' 이므로 예전 길로 내려간다.
|
|
@@ -203,7 +200,7 @@ export function 빠르게찾기({ 무늬, 자리, glob = null, 대소문자무
|
|
|
203
200
|
// git grep 은 .deelignore 를 시킬 방법이 없다. 그 파일이 있으면 예전 길로 간다 —
|
|
204
201
|
// 사람이 "deel 은 여기 보지 마라" 고 적어 둔 것을 못 지키면 빠른 것이 뜻이 없다.
|
|
205
202
|
if (것.gitgrep && !무시파일 && 저장소인가(자리)) {
|
|
206
|
-
const r = git로찾기({ 무늬, 자리, 대소문자무시,
|
|
203
|
+
const r = await git로찾기({ 무늬, 자리, 대소문자무시, 최대, signal });
|
|
207
204
|
if (r.ok) return { 엔진: 'git grep', 줄들: r.줄들.map(줄가르기).filter(Boolean), 잘림: r.잘림 };
|
|
208
205
|
return null;
|
|
209
206
|
}
|
package/src/tools/fsutil.js
CHANGED
|
@@ -175,11 +175,22 @@ export function 훑기상한(env = process.env) {
|
|
|
175
175
|
return Math.floor(n);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
/**
|
|
179
|
+
* 폴더를 훑는다.
|
|
180
|
+
*
|
|
181
|
+
* @param {AbortSignal|null} signal 멈추라면 훑다 말고 나온다.
|
|
182
|
+
* 사내망 드라이브에서는 파일 20,000개 훑기가 몇 초다. 그 사이에
|
|
183
|
+
* ESC 를 눌렀는데 끝까지 다 훑고 나서 멈추면, 사람 눈에는 멈추지
|
|
184
|
+
* 않는 것으로 보인다. 나온 것에 `끊김` 을 달아 부르는 쪽이 알게 한다 —
|
|
185
|
+
* 조용히 적게 돌려주면 「그런 파일이 없다」가 되어 버린다.
|
|
186
|
+
*/
|
|
187
|
+
export function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignore = true, signal = null } = {}) {
|
|
179
188
|
const out = [];
|
|
180
189
|
const 건너뜀 = { 폴더: 0, 파일: 0 };
|
|
190
|
+
let 끊김 = false;
|
|
181
191
|
const stack = [{ dir: root, rel: '', 규칙: ignore ? 뿌리규칙읽기(root) : [] }];
|
|
182
192
|
while (stack.length && out.length < limit) {
|
|
193
|
+
if (signal?.aborted) { 끊김 = true; break; }
|
|
183
194
|
const { dir, rel, 규칙 } = stack.pop();
|
|
184
195
|
let entries;
|
|
185
196
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
@@ -211,6 +222,7 @@ export function walk(root, { limit = 훑기상한(), skipDirs = SKIP_DIRS, ignor
|
|
|
211
222
|
// 사람을 한 번 더 보게 만들 뿐이고, 다 봤다고 말하는 것은 못 보게 만든다.
|
|
212
223
|
Object.defineProperty(out, '잘림', { value: out.length >= limit, enumerable: false });
|
|
213
224
|
Object.defineProperty(out, '상한', { value: limit, enumerable: false });
|
|
225
|
+
Object.defineProperty(out, '끊김', { value: 끊김, enumerable: false });
|
|
214
226
|
return out;
|
|
215
227
|
}
|
|
216
228
|
|
package/src/tools/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { 도구정의, 이름풀기 } from '../backend/mcp.js';
|
|
|
24
24
|
import { isExcelPath, readExcel, toText as excelText, summarize as excelSummary } from './excel.js';
|
|
25
25
|
import { isDocPath, readDoc, toText as docText, summarize as docSummary, looksOldHwp, 옛hwp안내, 문서는못고침 } from './docs.js';
|
|
26
26
|
import { 바꿔볼까, 직접못읽나, 변환기찾기, 글로바꾸기, 못바꿈말 } from './convert.js';
|
|
27
|
+
import { 물음검사 } from '../agent/askcheck.js';
|
|
27
28
|
import { isPdfPath, readPdf, toText as pdfText, summarize as pdfSummary, 못읽은말, pdf는못고침, 한쪽도못읽음말 } from './pdf.js';
|
|
28
29
|
import { diffLines } from '../ui/diff.js';
|
|
29
30
|
import { 읽을줄수, 찾을개수, 찾을줄수, 설명길이 } from '../agent/budget.js';
|
|
@@ -337,7 +338,7 @@ async function 엑셀읽기(abs, args, ctx) {
|
|
|
337
338
|
* LibreOffice 가 있으면 그것이 곧 길이다. 표는 아니고 글이지만,
|
|
338
339
|
* 아무것도 못 읽는 것보다 낫다.
|
|
339
340
|
*/
|
|
340
|
-
const 빌린것 = 빌려읽기(abs, ctx, r.error);
|
|
341
|
+
const 빌린것 = await 빌려읽기(abs, ctx, r.error);
|
|
341
342
|
if (빌린것) return 빌린것;
|
|
342
343
|
if (직접못읽나(abs)) return { error: 못바꿈말(ctx.scope.show(abs), extname(abs)), 끝났다: true };
|
|
343
344
|
return { error: r.error };
|
|
@@ -376,7 +377,7 @@ async function 엑셀읽기(abs, args, ctx) {
|
|
|
376
377
|
*
|
|
377
378
|
* @returns {object|null} 읽어냈으면 도구 결과. 못 하면 null (부르는 쪽이 원래 오류를 낸다)
|
|
378
379
|
*/
|
|
379
|
-
function 빌려읽기(abs, ctx, 원래오류) {
|
|
380
|
+
async function 빌려읽기(abs, ctx, 원래오류) {
|
|
380
381
|
if (!바꿔볼까(abs)) return null;
|
|
381
382
|
const root = ctx.scope?.root;
|
|
382
383
|
if (!root) return null;
|
|
@@ -386,7 +387,9 @@ function 빌려읽기(abs, ctx, 원래오류) {
|
|
|
386
387
|
const 있는것 = ctx.변환기 ?? 변환기찾기();
|
|
387
388
|
if (!있는것.soffice && !있는것.textutil) return null;
|
|
388
389
|
|
|
389
|
-
|
|
390
|
+
// signal 을 같이 넘긴다. 멈추라고 하면 soffice 를 죽여야 한다 — 안 그러면
|
|
391
|
+
// ESC 를 듣고도 남은 90초를 그대로 기다린다.
|
|
392
|
+
const r = await 글로바꾸기(abs, root, { 찾은것: 있는것, signal: ctx.signal ?? null });
|
|
390
393
|
if (!r.ok || !r.text.trim()) return null;
|
|
391
394
|
|
|
392
395
|
const 줄수 = r.text.split('\n').length;
|
|
@@ -400,11 +403,11 @@ function 빌려읽기(abs, ctx, 원래오류) {
|
|
|
400
403
|
};
|
|
401
404
|
}
|
|
402
405
|
|
|
403
|
-
function 문서읽기(abs, ctx) {
|
|
406
|
+
async function 문서읽기(abs, ctx) {
|
|
404
407
|
const r = readDoc(abs);
|
|
405
408
|
if (!r.ok) {
|
|
406
409
|
// 우리가 못 읽는다고 끝이 아니다. 이 PC 에 변환기가 있으면 빌려 본다.
|
|
407
|
-
const 빌린것 = 빌려읽기(abs, ctx, r.error);
|
|
410
|
+
const 빌린것 = await 빌려읽기(abs, ctx, r.error);
|
|
408
411
|
if (빌린것) return 빌린것;
|
|
409
412
|
// 끝났다 를 그대로 넘긴다. 여기서 떨구면 docs.js 가 「다시 열어도 같다」고
|
|
410
413
|
// 판정해 놓은 것이 루프까지 못 가서, 되풀이 억제가 안 걸린다.
|
|
@@ -585,10 +588,24 @@ function 한개옮기기({ from, to, overwrite = false }, ctx) {
|
|
|
585
588
|
function 여러개옮기기(목록, ctx) {
|
|
586
589
|
const 된것 = [];
|
|
587
590
|
const 안된것 = [];
|
|
591
|
+
/*
|
|
592
|
+
* ── 무엇이 움직였는지 모아서 돌려준다 ──────────────────────────────
|
|
593
|
+
*
|
|
594
|
+
* 여기서 한 개짜리 결과의 `changed` 를 버리고 있었다. 그래서 스물두
|
|
595
|
+
* 개를 옮기면 화면에는 `22개 옮김` 이 뜨는데, **턴이 아는 「손댄
|
|
596
|
+
* 파일」 은 0개**였다. 그 값은 여러 군데가 본다 — 턴 끝의 만든 파일
|
|
597
|
+
* 목록, `/commit`, 그리고 헛도는지 재는 자리. 그 자리들이 전부
|
|
598
|
+
* "아무 일도 안 일어났다" 로 알고 있었다.
|
|
599
|
+
*
|
|
600
|
+
* 한 개씩 옮길 때는 멀쩡했다. 배열로 부를 때만 새던 자리다.
|
|
601
|
+
*/
|
|
602
|
+
const 바뀐것들 = [];
|
|
588
603
|
for (const 하나 of 목록) {
|
|
589
604
|
const r = 한개옮기기(하나, ctx);
|
|
590
|
-
if (r.error) 안된것.push(`${하나.from ?? '?'} → ${하나.to ?? '?'}: ${r.error}`);
|
|
591
|
-
|
|
605
|
+
if (r.error) { 안된것.push(`${하나.from ?? '?'} → ${하나.to ?? '?'}: ${r.error}`); continue; }
|
|
606
|
+
된것.push(r.content);
|
|
607
|
+
// 폴더를 옮겼으면 그 안의 파일 하나하나가, 아니면 옮겨 간 자리가 답이다.
|
|
608
|
+
for (const f of r.바뀐것들 ?? (r.changed ? [r.changed] : [])) 바뀐것들.push(f);
|
|
592
609
|
}
|
|
593
610
|
/*
|
|
594
611
|
* 하나도 못 옮겼으면 오류로 돌려준다. 그래야 모델이 '됐다' 고 넘어가지
|
|
@@ -602,6 +619,7 @@ function 여러개옮기기(목록, ctx) {
|
|
|
602
619
|
+ (안된것.length ? `\n${안된것.map((e) => ` ✗ ${e.split('\n')[0]}`).join('\n')}` : '')
|
|
603
620
|
+ (안된것.length ? '\n\n실패한 것만 다시 보내세요. 된 것은 다시 안 보내도 됩니다.' : ''),
|
|
604
621
|
summary: `${된것.length}개 옮김${안된것.length ? ` · ${안된것.length}개 실패` : ''}`,
|
|
622
|
+
바뀐것들,
|
|
605
623
|
};
|
|
606
624
|
}
|
|
607
625
|
|
|
@@ -895,7 +913,7 @@ export const TOOLS = {
|
|
|
895
913
|
required: ['file_path'],
|
|
896
914
|
},
|
|
897
915
|
},
|
|
898
|
-
run(args, ctx) {
|
|
916
|
+
async run(args, ctx) {
|
|
899
917
|
const abs = ctx.scope.resolve(args.file_path);
|
|
900
918
|
if (!existsSync(abs)) return { error: `파일이 없습니다: ${args.file_path}` };
|
|
901
919
|
if (statSync(abs).isDirectory()) return { error: `폴더입니다. Glob 을 쓰세요: ${args.file_path}` };
|
|
@@ -923,7 +941,9 @@ export const TOOLS = {
|
|
|
923
941
|
if (looksOldHwp(abs, 머리)) {
|
|
924
942
|
// 여기도 먼저 빌려 본다. 이 PC 에 변환기가 있으면 안내문보다 글이 낫다.
|
|
925
943
|
const 안내 = 옛hwp안내(ctx.scope.show(abs));
|
|
926
|
-
|
|
944
|
+
// 괄호가 있어야 한다. await 를 빼면 `??` 가 **약속(Promise)** 을
|
|
945
|
+
// 보고 "값이 있다" 고 판단해서, 안내문이 영영 안 나간다.
|
|
946
|
+
return (await 빌려읽기(abs, ctx, 안내)) ?? { error: 안내 };
|
|
927
947
|
}
|
|
928
948
|
} catch { /* 아래 일반 읽기가 제 오류를 낸다 */ }
|
|
929
949
|
}
|
|
@@ -941,7 +961,7 @@ export const TOOLS = {
|
|
|
941
961
|
*/
|
|
942
962
|
if (직접못읽나(abs)) {
|
|
943
963
|
const 보인이름 = ctx.scope.show(abs);
|
|
944
|
-
const 빌린것 = 빌려읽기(abs, ctx, `${보인이름} 을 deel 이 직접 못 읽습니다.`);
|
|
964
|
+
const 빌린것 = await 빌려읽기(abs, ctx, `${보인이름} 을 deel 이 직접 못 읽습니다.`);
|
|
945
965
|
if (빌린것) return 빌린것;
|
|
946
966
|
return { error: 못바꿈말(보인이름, extname(abs)), 끝났다: true };
|
|
947
967
|
}
|
|
@@ -1215,7 +1235,9 @@ export const TOOLS = {
|
|
|
1215
1235
|
run(args, ctx) {
|
|
1216
1236
|
const root = args.path ? ctx.scope.resolve(args.path) : ctx.scope.root;
|
|
1217
1237
|
const re = globToRegex(args.pattern);
|
|
1218
|
-
const 전부 = walk(root);
|
|
1238
|
+
const 전부 = walk(root, { signal: ctx.signal });
|
|
1239
|
+
// 훑다 말고 나왔으면 그렇다고 말한다. 조용히 적게 주면 「그런 파일이 없다」가 된다.
|
|
1240
|
+
if (전부.끊김) return { error: '중단했습니다. 폴더를 끝까지 안 훑었습니다.', 끝났다: true, 중단됨: true };
|
|
1219
1241
|
// .gitignore 로 건너뛴 것은 수를 말한다 — 조용히 빼면 '그 파일이 없다' 로 읽힌다 (tools/ignore.js).
|
|
1220
1242
|
const 건너뜀 = 건너뜀말(전부.건너뜀, 전부.잘림, 전부.상한);
|
|
1221
1243
|
const 맞는것 = 전부
|
|
@@ -1259,7 +1281,9 @@ export const TOOLS = {
|
|
|
1259
1281
|
required: ['pattern'],
|
|
1260
1282
|
},
|
|
1261
1283
|
},
|
|
1262
|
-
|
|
1284
|
+
// async 인 까닭: 아래 빠르게찾기() 가 rg·git grep 을 **비동기로** 부른다.
|
|
1285
|
+
// 큰 저장소에서 20초를 도는 동안에도 ESC 가 들려야 하기 때문이다.
|
|
1286
|
+
async run(args, ctx) {
|
|
1263
1287
|
let re;
|
|
1264
1288
|
try { re = new RegExp(args.pattern, args['-i'] ? 'i' : ''); }
|
|
1265
1289
|
catch (err) { return { error: `정규식이 잘못됐습니다: ${err.message}` }; }
|
|
@@ -1282,13 +1306,15 @@ export const TOOLS = {
|
|
|
1282
1306
|
* 다르게 나왔을 때 무엇으로 찾은 것인지 모르면 사람은 코드를 의심한다.
|
|
1283
1307
|
*/
|
|
1284
1308
|
const 무시파일 = join(ctx.scope.root, '.deelignore');
|
|
1285
|
-
const 빠른것 = isFile ? null : 빠르게찾기({
|
|
1309
|
+
const 빠른것 = isFile ? null : await 빠르게찾기({
|
|
1286
1310
|
무늬: args.pattern,
|
|
1287
1311
|
자리: root,
|
|
1288
1312
|
glob: args.glob ?? null,
|
|
1289
1313
|
대소문자무시: !!args['-i'],
|
|
1290
1314
|
무시파일: existsSync(무시파일) ? 무시파일 : null,
|
|
1291
1315
|
최대: Math.max(limit * 4, 2000),
|
|
1316
|
+
// 멈추라고 하면 rg 를 죽인다. 이게 없으면 ESC 를 듣고도 20초를 더 기다린다.
|
|
1317
|
+
signal: ctx.signal ?? null,
|
|
1292
1318
|
});
|
|
1293
1319
|
if (빠른것) {
|
|
1294
1320
|
const 파일별 = new Map();
|
|
@@ -1339,7 +1365,8 @@ export const TOOLS = {
|
|
|
1339
1365
|
*/
|
|
1340
1366
|
let files = isFile
|
|
1341
1367
|
? [{ path: root, rel: ctx.scope.show(root) }]
|
|
1342
|
-
: walk(root);
|
|
1368
|
+
: walk(root, { signal: ctx.signal });
|
|
1369
|
+
if (files.끊김) return { error: '중단했습니다. 폴더를 끝까지 안 훑었습니다.', 끝났다: true, 중단됨: true };
|
|
1343
1370
|
const 안본것 = isFile ? '' : 건너뜀말(files.건너뜀, files.잘림, files.상한).trim(); // .gitignore 로 건너뛴 수 — 꼬리에 적는다
|
|
1344
1371
|
// 훑기 상한에 걸렸으면 "일치 없음" 이라고 잘라 말하면 안 된다. 안 본 것이다.
|
|
1345
1372
|
const 다못봄 = !isFile && !!files.잘림;
|
|
@@ -1662,13 +1689,21 @@ export const TOOLS = {
|
|
|
1662
1689
|
* 첫 문장에 제일 중요한 것을 둔다 — 좁은 창에서는 설명줄이기가
|
|
1663
1690
|
* **뒤에서부터 문장째로** 잘라 낸다. 8k 에서 남는 것은 앞 90자다.
|
|
1664
1691
|
*/
|
|
1665
|
-
description: '갈림길에서 사람에게 하나 묻는다.
|
|
1666
|
-
+ '
|
|
1692
|
+
description: '갈림길에서 사람에게 하나 묻는다. **먼저 `이해` 에 이번 요청을 무엇으로'
|
|
1693
|
+
+ ' 알아들었는지 한 줄로 적고**, 그러고도 정말 정할 것이 남았을 때만 묻는다.'
|
|
1694
|
+
+ ' 글로 "알려주세요" 하고 끝내지 마라 — 턴이 끝나서 여태 본 것이 버려진다.'
|
|
1695
|
+
+ ' options 에 2~4개를 주면 숫자로 답한다.'
|
|
1667
1696
|
+ ' **이미 사용자가 말한 것을 다시 묻지 마라.** "파일 정리해 줘" 라고 했으면'
|
|
1668
1697
|
+ ' 그게 답이다 — 그대로 하면 된다. 어떻게 할지 정하는 것은 네 일이다.',
|
|
1669
1698
|
parameters: {
|
|
1670
1699
|
type: 'object',
|
|
1671
1700
|
properties: {
|
|
1701
|
+
이해: {
|
|
1702
|
+
type: 'string',
|
|
1703
|
+
description: '이번 요청을 무엇으로 알아들었는지 한 줄. 사람은 이 줄을 보고 네가'
|
|
1704
|
+
+ ' 제대로 읽었는지 판단한다. "요청을 이해했습니다" 같은 인사말은 안 된다 —'
|
|
1705
|
+
+ ' 그 일의 내용이 들어가야 한다',
|
|
1706
|
+
},
|
|
1672
1707
|
question: { type: 'string', description: '한 문장짜리 질문. 무엇을 정해야 하는지 분명하게' },
|
|
1673
1708
|
options: {
|
|
1674
1709
|
type: 'array',
|
|
@@ -1676,7 +1711,7 @@ export const TOOLS = {
|
|
|
1676
1711
|
description: '고를 것 2~4개. 각각 한 줄로, 무엇이 달라지는지 알 수 있게',
|
|
1677
1712
|
},
|
|
1678
1713
|
},
|
|
1679
|
-
required: ['question'],
|
|
1714
|
+
required: ['이해', 'question'],
|
|
1680
1715
|
},
|
|
1681
1716
|
},
|
|
1682
1717
|
async run(args, ctx) {
|
|
@@ -1685,11 +1720,16 @@ export const TOOLS = {
|
|
|
1685
1720
|
|
|
1686
1721
|
const 고를것 = (Array.isArray(args.options) ? args.options : [])
|
|
1687
1722
|
.map((x) => String(x ?? '').trim()).filter(Boolean).slice(0, 4);
|
|
1723
|
+
const 이해 = String(args.이해 ?? '').trim();
|
|
1688
1724
|
|
|
1689
1725
|
/*
|
|
1690
1726
|
* 물어볼 자리가 없는 데서도 안 죽어야 한다 — `deel -p` 한 방 실행, 파이프,
|
|
1691
1727
|
* 하위 작업. 거기서는 **막히지 말고** 스스로 판단하라고 돌려준다.
|
|
1692
1728
|
* 답을 기다리며 서 있으면 그 실행은 영영 안 끝난다.
|
|
1729
|
+
*
|
|
1730
|
+
* 아래 관문보다 **먼저** 본다. 어차피 아무도 못 듣는 자리에서 "이해를
|
|
1731
|
+
* 채워서 다시 물어라" 라고 돌려주면, 고쳐서 다시 불러도 결과가 같다 —
|
|
1732
|
+
* 왕복만 한 번 늘고 그 실행은 그만큼 늦어진다.
|
|
1693
1733
|
*/
|
|
1694
1734
|
/*
|
|
1695
1735
|
* 결과는 반드시 `content` 로 돌려준다.
|
|
@@ -1706,7 +1746,28 @@ export const TOOLS = {
|
|
|
1706
1746
|
};
|
|
1707
1747
|
}
|
|
1708
1748
|
|
|
1709
|
-
|
|
1749
|
+
/*
|
|
1750
|
+
* 사람에게 내보내기 전에 한 번 거른다 (agent/askcheck.js).
|
|
1751
|
+
*
|
|
1752
|
+
* 막힌 물음은 **오류로** 돌려준다. content 로 돌려주면 모델은 그것을
|
|
1753
|
+
* 사람의 답으로 읽고 그대로 이어 간다 — 묻지도 않은 답을 받은 셈이 된다.
|
|
1754
|
+
* 오류여야 되풀이 감지에도 걸리고, 같은 물음을 또 던지지 않는다.
|
|
1755
|
+
*/
|
|
1756
|
+
const 관문 = 물음검사({
|
|
1757
|
+
물음, 고를것, 이해, 요청: ctx.요청 ?? '', 이미물은것: ctx.물은것 ?? [],
|
|
1758
|
+
});
|
|
1759
|
+
if (!관문.ok) return { error: 관문.할말, 끝났다: true, 물음막힘: 관문.왜 };
|
|
1760
|
+
|
|
1761
|
+
const 답 = await ctx.ask물음(물음, 고를것, 이해);
|
|
1762
|
+
|
|
1763
|
+
/*
|
|
1764
|
+
* 물어본 것을 적어 둔다.
|
|
1765
|
+
*
|
|
1766
|
+
* 관문이 "이미 물었나" 를 보려면 기억할 자리가 있어야 한다. 답까지 같이
|
|
1767
|
+
* 적는 것은, 또 물으려 할 때 **앞의 답을 그대로 돌려주기** 위해서다 —
|
|
1768
|
+
* "이미 물었습니다" 만 말하면 모델은 그 답이 무엇이었는지 못 찾는다.
|
|
1769
|
+
*/
|
|
1770
|
+
(ctx.물은것 ??= []).push({ 물음, 고를것, 답: 답 == null ? '' : String(답) });
|
|
1710
1771
|
if (답 === null || 답 === undefined || String(답).trim() === '') {
|
|
1711
1772
|
return { content: '사람이 답하지 않았습니다. 되묻지 말고 스스로 판단해 이어가세요.' };
|
|
1712
1773
|
}
|
|
@@ -1745,7 +1806,24 @@ export const TOOLS = {
|
|
|
1745
1806
|
const 왜 = r.예산초과
|
|
1746
1807
|
? `지난 대화 ${r.전체파일}개 중 ${r.본파일}개까지만 뒤졌습니다(양이 많아 멈춤). 못 찾았습니다`
|
|
1747
1808
|
: `지난 대화 ${r.본파일}개를 다 뒤졌지만 없습니다`;
|
|
1748
|
-
|
|
1809
|
+
/*
|
|
1810
|
+
* content 를 반드시 채운다.
|
|
1811
|
+
*
|
|
1812
|
+
* 대화에 실리는 것은 content 다. 여태 여기는 summary 만 돌려줬고,
|
|
1813
|
+
* 그래서 **모델에게는 빈 글이 갔다.** 사람 화면에는 「없습니다」가
|
|
1814
|
+
* 멀쩡히 찍히니 아무도 못 알아챘다. 모델은 못 찾은 줄도 모르고
|
|
1815
|
+
* 찾아본 줄도 몰라서, 그 자리에서 엉뚱한 선택지를 들이밀었다.
|
|
1816
|
+
*
|
|
1817
|
+
* 「없다」는 것도 알아낸 것이다. 알아낸 것은 반드시 전한다.
|
|
1818
|
+
*/
|
|
1819
|
+
const 없다는말 = `${왜}: ${q}\n`
|
|
1820
|
+
+ '이 대화 밖에는 단서가 없습니다. 없는 것을 지어내지 말고,'
|
|
1821
|
+
+ ' 지금 대화에 있는 것으로 판단하거나 무엇이 없는지 밝히세요.';
|
|
1822
|
+
return {
|
|
1823
|
+
content: 없다는말,
|
|
1824
|
+
summary: `${왜}: ${q}`,
|
|
1825
|
+
hits: [], searched: r.본파일, total: r.전체파일, partial: r.예산초과,
|
|
1826
|
+
};
|
|
1749
1827
|
}
|
|
1750
1828
|
|
|
1751
1829
|
const 줄들 = r.맞은것.map((h) => {
|
|
@@ -2069,9 +2147,39 @@ export async function runTool(name, args, ctx) {
|
|
|
2069
2147
|
|
|
2070
2148
|
const t = TOOLS[name];
|
|
2071
2149
|
if (!t) return { error: `모르는 도구: ${name}` };
|
|
2150
|
+
|
|
2151
|
+
/*
|
|
2152
|
+
* ── 멈추라고 했으면 시작도 안 한다 ──────────────────────────────────
|
|
2153
|
+
*
|
|
2154
|
+
* 여럿을 함께 돌릴 때(loop.js 의 Promise.all) 앞엣것이 도는 사이 사람이
|
|
2155
|
+
* ESC 를 누르면, 뒤엣것들은 **아직 아무 일도 안 했는데** 그대로 돌았다.
|
|
2156
|
+
* 여기서 한 번 보면 그 자리가 막힌다.
|
|
2157
|
+
*
|
|
2158
|
+
* 중단은 **실패가 아니다.** 그래서 중단됨 을 따로 단다 — 이걸 실패로
|
|
2159
|
+
* 세면 되풀이 감지가 엉뚱하게 걸려서, 다음에 같은 도구를 부르는 것까지
|
|
2160
|
+
* "또 그러네" 로 막아 버린다. 사람이 멈춘 것은 도구 잘못이 아니다.
|
|
2161
|
+
*/
|
|
2162
|
+
if (ctx.signal?.aborted) {
|
|
2163
|
+
return { error: '중단했습니다. 실행하지 않았습니다.', 끝났다: true, 중단됨: true };
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2072
2166
|
try {
|
|
2073
2167
|
const r = await t.run(args ?? {}, ctx);
|
|
2074
2168
|
ctx.audit.tool(name, args, r);
|
|
2169
|
+
/*
|
|
2170
|
+
* 도는 중에 멈췄다면 결과를 안 쓴다.
|
|
2171
|
+
*
|
|
2172
|
+
* 다만 **이미 바꿔 놓은 것은 사실대로 말한다.** 파일을 고친 뒤에
|
|
2173
|
+
* "중단했습니다" 만 돌려주면 모델은 안 고쳐진 줄 알고 또 고친다 —
|
|
2174
|
+
* 그러면 같은 편집이 두 번 들어가거나, 되돌리기 기록과 어긋난다.
|
|
2175
|
+
* 읽기만 한 것은 버려도 잃을 것이 없으니 중단으로 끝낸다.
|
|
2176
|
+
*/
|
|
2177
|
+
if (ctx.signal?.aborted) {
|
|
2178
|
+
if (!isMutating(name) || r.error) {
|
|
2179
|
+
return { error: '중단했습니다.', 끝났다: true, 중단됨: true };
|
|
2180
|
+
}
|
|
2181
|
+
return { ...r, 중단됨: true, 중단전에끝남: true };
|
|
2182
|
+
}
|
|
2075
2183
|
return await 고친뒤진단(name, r, ctx);
|
|
2076
2184
|
} catch (err) {
|
|
2077
2185
|
const r = { error: err.message };
|
package/src/tools/outline.js
CHANGED
|
@@ -232,7 +232,8 @@ export const OUTLINE_TOOL = {
|
|
|
232
232
|
const 하나인가 = statSync(시작).isFile();
|
|
233
233
|
let 파일들 = 하나인가
|
|
234
234
|
? [{ path: 시작, rel: ctx.scope.show(시작), mtime: statSync(시작).mtimeMs }]
|
|
235
|
-
: walk(시작, { skipDirs: SKIP_DIRS });
|
|
235
|
+
: walk(시작, { skipDirs: SKIP_DIRS, signal: ctx.signal });
|
|
236
|
+
if (파일들.끊김) return { error: '중단했습니다. 폴더를 끝까지 안 훑었습니다.', 끝났다: true, 중단됨: true };
|
|
236
237
|
const 건너뜀 = 하나인가 ? '' : 건너뜀말(파일들.건너뜀, 파일들.잘림, 파일들.상한); // .gitignore 로 건너뛴 수 (tools/ignore.js)
|
|
237
238
|
|
|
238
239
|
// 좁히는 방식은 Glob 도구와 **같은 것**을 쓴다. 두 도구가 같은 패턴에
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// 바깥 프로그램을 부른다 — **기다리는 동안 귀를 열어 둔 채로.**
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 이 파일이 따로 있나 ─────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 전에는 찾기(fastgrep.js)도 변환(convert.js)도 각자 spawnSync 를 썼다.
|
|
6
|
+
// 쓰기는 편하다. 그런데 그 호출이 도는 동안 Node 는 이벤트 루프를 통째로
|
|
7
|
+
// 멈춘다. 그 사이에는 키 입력이 배달되지 않는다 — 사람이 ESC 를 몇 번을
|
|
8
|
+
// 눌러도, 그 키는 버퍼에만 쌓여 있다가 **일이 다 끝난 뒤에야** 들어온다.
|
|
9
|
+
//
|
|
10
|
+
// rg 로 큰 저장소 찾기 최대 20초
|
|
11
|
+
// soffice 로 문서 바꾸기 최대 90초
|
|
12
|
+
//
|
|
13
|
+
// 그동안 프로그램은 귀머거리다. "ESC 를 눌러도 안 멈춘다" 는 제보의 뿌리가
|
|
14
|
+
// 여기였다. 화면에는 여전히 돌아가는 그림이 보이니 사람은 더 답답하다.
|
|
15
|
+
//
|
|
16
|
+
// 두 군데에 따로 적어 두면 언젠가 한쪽만 고쳐지고, 그때부터 "검색은 멈추는데
|
|
17
|
+
// 문서 변환은 안 멈추는" 프로그램이 된다. 그건 고장보다 설명하기 어렵다.
|
|
18
|
+
// 그래서 한 벌만 둔다.
|
|
19
|
+
//
|
|
20
|
+
// ── 돌려주는 모양 ──────────────────────────────────────────────────────
|
|
21
|
+
//
|
|
22
|
+
// 일부러 spawnSync 와 **똑같이** 맞췄다 — {error, status, stdout, stderr}.
|
|
23
|
+
// 부르는 쪽의 판단 코드(`status === 2 면 실패` 같은 것)를 한 줄도 안 건드리고
|
|
24
|
+
// 바꾸려는 것이다. 바꾼 자리가 적을수록 조용히 달라지는 것도 적다.
|
|
25
|
+
import { spawn } from 'node:child_process';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 자식 프로그램을 부르고, 끝날 때까지 기다린다 — 루프는 계속 돈다.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} 이름 실행할 것
|
|
31
|
+
* @param {string[]} 인자 인자들. 셸을 안 거치므로 여기 든 따옴표·세미콜론은 글자다.
|
|
32
|
+
* @param {object} [옵션]
|
|
33
|
+
* @param {number} [옵션.timeout] 이 시간을 넘기면 죽이고 실패로 돌려준다
|
|
34
|
+
* @param {number} [옵션.maxBuffer] 나온 글이 이보다 커지면 죽인다
|
|
35
|
+
* @param {AbortSignal|null} [옵션.signal] 눌리면 자식을 죽인다
|
|
36
|
+
* @param {object} [옵션.덤] spawn 에 그대로 넘길 것 (windowsVerbatimArguments 같은)
|
|
37
|
+
* @returns {Promise<{error:Error|null, status:number|null, stdout:string, stderr:string}>}
|
|
38
|
+
*/
|
|
39
|
+
export function 돌려보기(이름, 인자, { timeout = 20000, maxBuffer = 32 * 1024 * 1024, signal = null, 덤 = {} } = {}) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
// 이미 눌렸으면 띄우지도 않는다. 여럿을 함께 돌릴 때 뒤엣것이 여기로 온다.
|
|
42
|
+
if (signal?.aborted) { resolve({ error: new Error('중단했습니다'), status: null, stdout: '', stderr: '' }); return; }
|
|
43
|
+
|
|
44
|
+
let 아이;
|
|
45
|
+
try {
|
|
46
|
+
아이 = spawn(이름, 인자, { windowsHide: true, ...덤 });
|
|
47
|
+
} catch (탈난것) {
|
|
48
|
+
resolve({ error: 탈난것, status: null, stdout: '', stderr: '' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let 밖 = ''; let 탈 = ''; let 끝났나 = false;
|
|
53
|
+
const 끝내기 = (것) => {
|
|
54
|
+
if (끝났나) return;
|
|
55
|
+
끝났나 = true;
|
|
56
|
+
clearTimeout(시계);
|
|
57
|
+
signal?.removeEventListener?.('abort', 중단하기);
|
|
58
|
+
resolve(것);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* 멈추라고 하면 **자식을 죽인다.**
|
|
63
|
+
*
|
|
64
|
+
* 여기가 없으면 반쪽짜리다. 비동기로 바꾼 덕에 ESC 는 들리지만, 정작
|
|
65
|
+
* soffice 는 남은 시간을 마저 돈다 — 사람 눈에는 여전히 "눌렀는데 안
|
|
66
|
+
* 멈춘다". 듣는 것과 멈추는 것은 다른 일이라, 둘 다 해야 한 가지가 끝난다.
|
|
67
|
+
*/
|
|
68
|
+
const 중단하기 = () => {
|
|
69
|
+
try { 아이.kill(); } catch { /* 이미 죽었다 */ }
|
|
70
|
+
끝내기({ error: new Error('중단했습니다'), status: null, stdout: '', stderr: '' });
|
|
71
|
+
};
|
|
72
|
+
signal?.addEventListener?.('abort', 중단하기, { once: true });
|
|
73
|
+
|
|
74
|
+
const 시계 = setTimeout(() => {
|
|
75
|
+
try { 아이.kill(); } catch { /* 이미 죽었다 */ }
|
|
76
|
+
끝내기({ error: new Error(`${이름} 가 ${timeout}ms 안에 안 끝났습니다`), status: null, stdout: 밖, stderr: 탈 });
|
|
77
|
+
}, timeout);
|
|
78
|
+
// 이 타이머가 프로그램을 붙들면 안 된다. 자식의 파이프가 이미 루프를 붙들고 있다.
|
|
79
|
+
시계.unref?.();
|
|
80
|
+
|
|
81
|
+
아이.stdout?.setEncoding('utf8');
|
|
82
|
+
아이.stderr?.setEncoding('utf8');
|
|
83
|
+
아이.stdout?.on('data', (조각) => {
|
|
84
|
+
밖 += 조각;
|
|
85
|
+
/*
|
|
86
|
+
* spawnSync 는 이 자리에서 ENOBUFS 를 냈다. 조용히 잘라 버리면 사람
|
|
87
|
+
* 눈에는 '결과가 적다' 로만 보인다 — 오류도 안 나고 아무도 눈치 못 챈다.
|
|
88
|
+
* 같은 자리에서 같은 실패를 내야 부르는 쪽이 예전 길로 내려간다.
|
|
89
|
+
*/
|
|
90
|
+
if (밖.length > maxBuffer) {
|
|
91
|
+
try { 아이.kill(); } catch { /* 이미 죽었다 */ }
|
|
92
|
+
끝내기({ error: new Error('결과가 너무 많습니다'), status: null, stdout: '', stderr: 탈 });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
아이.stderr?.on('data', (조각) => { if (탈.length < 65536) 탈 += 조각; });
|
|
96
|
+
아이.on('error', (탈난것) => 끝내기({ error: 탈난것, status: null, stdout: 밖, stderr: 탈 }));
|
|
97
|
+
아이.on('close', (코드) => 끝내기({ error: null, status: 코드, stdout: 밖, stderr: 탈 }));
|
|
98
|
+
});
|
|
99
|
+
}
|
package/src/tools/todo.js
CHANGED
|
@@ -95,6 +95,34 @@ export const TODO_TOOL = {
|
|
|
95
95
|
const 이전 = ctx.todos ?? [];
|
|
96
96
|
ctx.todos = items;
|
|
97
97
|
|
|
98
|
+
/*
|
|
99
|
+
* ── 똑같은 목록을 또 보냈으면 그렇다고 말한다 ────────────────────────
|
|
100
|
+
*
|
|
101
|
+
* 「폴더 정리 해줘」 에서 이 자리가 걸렸다. 모델이 파일을 옮긴 뒤
|
|
102
|
+
* 목록을 갱신하려 했는데, 끝난 줄을 done 으로 안 바꾸고 **글자 하나
|
|
103
|
+
* 안 틀린 같은 목록**을 다시 보냈다. 그러면 여기서는 성공으로 돌려주고
|
|
104
|
+
* 「3개 중 1개 완료」 라는 앞과 똑같은 글이 나간다. 모델 쪽에서 보면
|
|
105
|
+
* 갱신이 된 것이므로, 다음 걸음에 또 같은 것을 보낸다. 그렇게 세 번이면
|
|
106
|
+
* 헛돈다고 판정돼 턴이 죽었다.
|
|
107
|
+
*
|
|
108
|
+
* 오류로 만들지는 않는다. 목록은 실제로 저장됐고, 틀린 것을 한 것도
|
|
109
|
+
* 아니다. 다만 **아무 일도 안 일어났다는 사실**과 그럴 때 무엇을 해야
|
|
110
|
+
* 하는지를 그 자리에서 알려 준다.
|
|
111
|
+
*/
|
|
112
|
+
const 같은목록 = 이전.length === items.length
|
|
113
|
+
&& items.every((x, i) => 이전[i]?.text === x.text && 이전[i]?.state === x.state);
|
|
114
|
+
if (같은목록) {
|
|
115
|
+
return {
|
|
116
|
+
content: `${render(items)}\n\n`
|
|
117
|
+
+ '이 목록은 앞에 보낸 것과 글자 하나까지 같습니다 — 바뀐 것이 없습니다.\n'
|
|
118
|
+
+ '방금 끝낸 일이 있으면 그 줄을 done 으로 바꿔서 보내고,'
|
|
119
|
+
+ ' 없으면 목록을 다시 보내지 말고 다음 일을 하세요.',
|
|
120
|
+
summary: '그대로 (바뀐 것 없음)',
|
|
121
|
+
todos: items,
|
|
122
|
+
안바뀜: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
const 끝난것 = items.filter((x) => x.state === 'done').length;
|
|
99
127
|
const 새로끝난 = items.filter((x) =>
|
|
100
128
|
x.state === 'done' && !이전.some((y) => y.text === x.text && y.state === 'done'));
|
package/src/ui/inputbox.js
CHANGED
|
@@ -444,7 +444,15 @@ export class InputBox {
|
|
|
444
444
|
// 답이 흘러나오는 동안 상자가 답 줄에 얹히는 것을 여기서 막는다.
|
|
445
445
|
if (!this.일감 || this.줄중간) return;
|
|
446
446
|
const 지난 = Date.now() - this.일감.시작;
|
|
447
|
-
|
|
447
|
+
/*
|
|
448
|
+
* '멈춤' 은 '느긋' 을 이긴다.
|
|
449
|
+
*
|
|
450
|
+
* 45초가 넘으면 갈래를 느긋으로 갈아 주는데, 사람이 ESC 를 누르는 때가
|
|
451
|
+
* 하필 딱 그때다 — 오래 걸려서 누르는 것이다. 순서를 안 정해 두면
|
|
452
|
+
* 「멈추는 중」이 화면에 영영 안 뜨고, 사람은 안 먹었다고 여겨 또 누른다.
|
|
453
|
+
*/
|
|
454
|
+
const 멈추나 = this.일감.갈래 === '멈춤';
|
|
455
|
+
const 갈래 = (!멈추나 && 지난 >= 느긋해질때) ? '느긋' : this.일감.갈래;
|
|
448
456
|
const 회차 = Math.floor((this.틱 * 돌림주기) / 문구주기);
|
|
449
457
|
this.그리기(this.session, '', 0, {
|
|
450
458
|
// 갈래에 맞는 작은 그림. 무엇을 하는 중인지 글자와 그림이 같이 말한다.
|
|
@@ -459,7 +467,8 @@ export class InputBox {
|
|
|
459
467
|
this.일감.예약수 ? `${this.일감.예약수}건 예약됨` : '',
|
|
460
468
|
// 도는 동안 보이는 자리라 여기서는 ESC 만 말한다. Ctrl+C 도 되지만
|
|
461
469
|
// 두 번 누르면 끝나 버려서, 급히 멈추려는 사람에게 권할 것이 못 된다.
|
|
462
|
-
|
|
470
|
+
// 이미 멈추는 중이면 안 권한다 — 또 누르라는 말로 읽힌다.
|
|
471
|
+
멈추나 ? '' : 'ESC 중단',
|
|
463
472
|
].filter(Boolean).join(' · '),
|
|
464
473
|
대기: this.일감.대기,
|
|
465
474
|
});
|
package/src/ui/motion.js
CHANGED
|
@@ -163,6 +163,20 @@ export const 그림들 = {
|
|
|
163
163
|
['......', '....##', '....##', '......'],
|
|
164
164
|
],
|
|
165
165
|
|
|
166
|
+
/*
|
|
167
|
+
* 멈춤 — ESC 를 눌렀을 때. **조여든다.**
|
|
168
|
+
*
|
|
169
|
+
* 지나가지도 왕복하지도 않는다. 둘 다 '아직 하는 중' 으로 읽히기 때문이다.
|
|
170
|
+
* 여기서 말해야 하는 것은 하나다 — 눌린 것이 닿았고, 지금 거두는 중이다.
|
|
171
|
+
* 그렇다고 멈춰 세우면 화면이 얼어붙은 것과 구별이 안 된다.
|
|
172
|
+
*/
|
|
173
|
+
멈춤: [
|
|
174
|
+
['......', '######', '######', '......'],
|
|
175
|
+
['......', '.####.', '.####.', '......'],
|
|
176
|
+
['......', '..##..', '..##..', '......'],
|
|
177
|
+
['......', '.####.', '.####.', '......'],
|
|
178
|
+
],
|
|
179
|
+
|
|
166
180
|
/*
|
|
167
181
|
* 느긋 — 오래 걸릴 때. 왕복한다.
|
|
168
182
|
*
|
|
@@ -307,6 +321,14 @@ const 기사그림들 = {
|
|
|
307
321
|
['...##.......', '..####......', '..####......', '..#..#......'],
|
|
308
322
|
],
|
|
309
323
|
|
|
324
|
+
// 멈춤 — 걸음을 멈추고 팔을 든다. "그만" 이라는 손짓이다.
|
|
325
|
+
멈춤: [
|
|
326
|
+
['.##.........', '####........', '####........', '#..#........'],
|
|
327
|
+
['.##.#.......', '#####.......', '####........', '#..#........'],
|
|
328
|
+
['.##.#.......', '#####.......', '####........', '#..#........'],
|
|
329
|
+
['.##.........', '####........', '####........', '#..#........'],
|
|
330
|
+
],
|
|
331
|
+
|
|
310
332
|
/*
|
|
311
333
|
* 느긋 — **넘어진다.** 45초가 넘어가면 이 그림으로 갈린다.
|
|
312
334
|
*
|
|
@@ -417,6 +439,14 @@ const 동물그림들 = {
|
|
|
417
439
|
['..#..#......', '..####......', '.######.....', '..#..#......'],
|
|
418
440
|
],
|
|
419
441
|
|
|
442
|
+
// 멈춤 — 가던 걸음을 멈추고 다리를 모은다. 앉기 직전이다.
|
|
443
|
+
멈춤: [
|
|
444
|
+
['.#..#.......', '.####.......', '######......', '.#..#.......'],
|
|
445
|
+
['.#..#.......', '.####.......', '######......', '..##........'],
|
|
446
|
+
['.#..#.......', '.####.......', '######......', '..##........'],
|
|
447
|
+
['.#..#.......', '.####.......', '######......', '.#..#.......'],
|
|
448
|
+
],
|
|
449
|
+
|
|
420
450
|
/*
|
|
421
451
|
* 느긋 — 드러누워 잔다. 숨만 오르내린다.
|
|
422
452
|
*
|