shellbase 0.13.0 → 0.13.1
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/dist/agent.js +55 -16
- package/dist/browse.js +22 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -15,7 +15,7 @@ import { encryptFrame, decryptFrame, generateFrameKey } from './crypto.js';
|
|
|
15
15
|
import { registerDevice, heartbeatDevice, unregisterDevice, listOnlineNames, renameDevice, markSessionDone, keepDevice, } from './devices.js';
|
|
16
16
|
import { promptApproval } from './prompt.js';
|
|
17
17
|
import { touchRecentDir } from './recent-dirs.js';
|
|
18
|
-
import { listDirs, deleteEntry, renameEntry } from './browse.js';
|
|
18
|
+
import { listDirs, createEntry, deleteEntry, renameEntry } from './browse.js';
|
|
19
19
|
import { readTextFile, writeTextFile, chunkContent } from './files.js';
|
|
20
20
|
import { AUDIO_CHUNK_TTL_MS, MAX_AUDIO_BASE64, STT_SETUP_HINT, findSttServer, transcribe, } from './stt.js';
|
|
21
21
|
import { loadOpenSessions, saveOpenSessions } from './open-sessions.js';
|
|
@@ -34,8 +34,14 @@ const IMAGE_SWEEP_MS = 6 * 60 * 60 * 1000;
|
|
|
34
34
|
const APPROVAL_TIMEOUT_MS = 30_000;
|
|
35
35
|
// 복원해서 보낼 때 함께 보낼 이전 줄 수 (화면 위로 올려볼 수 있는 분량)
|
|
36
36
|
const RESTORE_SCROLLBACK_LINES = 300;
|
|
37
|
-
// 프레임 하나에 들어갈 수 있는 크기가 제한돼 있어서(
|
|
37
|
+
// 프레임 하나에 들어갈 수 있는 크기가 제한돼 있어서(카테고리 한도 64KB, 암호화하며 더 커짐) 나눠 보낸다.
|
|
38
|
+
// 화면 복원뿐 아니라 평소 출력도 반드시 이 크기로 나눠야 한다 — 한 덩이로 보내면 출력이 조금만 몰려도
|
|
39
|
+
// (빌드 로그·Claude Code 전체 다시 그리기 등) 프레임이 한도를 넘어 통째로 거부되고, 그 세션은 그 뒤로
|
|
40
|
+
// 아무 화면도 못 받는다. 폰에는 "검은 화면에 글자 하나 없는" 상태로 보인다 (제보).
|
|
38
41
|
const RESTORE_CHUNK = 12 * 1024;
|
|
42
|
+
// 전송이 막혀 밀린 출력이 이만큼을 넘으면, 밀린 것을 하나하나 따라 보내는 대신 지금 화면을 통째로
|
|
43
|
+
// 다시 보낸다 — 사용자가 보게 될 최종 화면은 어차피 같고, 훨씬 빨리 따라잡는다.
|
|
44
|
+
const MAX_PENDING_OUTPUT = 256 * 1024;
|
|
39
45
|
// 출력 전송 최소 간격. "마지막 전송 후 이 간격이 지났으면 바로 보내고, 아니면 그만큼만 기다렸다 모아서
|
|
40
46
|
// 보낸다"(leading-edge 스로틀). 예전엔 문서의 "최소 100ms" 를 보수적으로 잡아 120ms 로 뒀는데, 실측해보니
|
|
41
47
|
// 서버가 40회 연속 전송도 거부 없이 받아줬고 이 값이 타이핑 체감 지연의 주범이었음(입력·출력 양쪽에서
|
|
@@ -184,25 +190,48 @@ export async function runAgent(options) {
|
|
|
184
190
|
};
|
|
185
191
|
const flush = async (session) => {
|
|
186
192
|
session.pendingFlush = null;
|
|
187
|
-
if (!session.outBuffer)
|
|
193
|
+
if (session.flushing || !session.outBuffer)
|
|
188
194
|
return;
|
|
189
195
|
const chunk = session.outBuffer;
|
|
190
196
|
session.outBuffer = '';
|
|
197
|
+
// 전송이 계속 막혀 밀린 양이 감당 못 할 만큼 커졌으면, 밀린 것을 하나하나 따라 보내는 대신
|
|
198
|
+
// 지금 화면을 통째로 다시 보낸다. 화면 상태는 이쪽(session.screen)에 그대로 들고 있다.
|
|
199
|
+
if (chunk.length > MAX_PENDING_OUTPUT) {
|
|
200
|
+
console.error(`밀린 출력이 너무 많아요(${Math.round(chunk.length / 1024)}KB) — 화면을 통째로 다시 보낼게요.`);
|
|
201
|
+
if (!session.closing)
|
|
202
|
+
void sendRestore(session);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
session.flushing = true;
|
|
191
206
|
session.lastSentAt = Date.now();
|
|
192
207
|
await enqueueSend(session, async () => {
|
|
193
208
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
// 프레임 한도를 넘지 않도록 나눠 보낸다 (한 덩이로 보내면 통째로 거부돼 화면이 멈춘다)
|
|
210
|
+
for (let i = 0; i < chunk.length; i += RESTORE_CHUNK) {
|
|
211
|
+
const part = chunk.slice(i, i + RESTORE_CHUNK);
|
|
212
|
+
try {
|
|
213
|
+
await channel.send({
|
|
214
|
+
kind: 'output',
|
|
215
|
+
to: session.deviceId,
|
|
216
|
+
data: encryptFrame(part, session.frameKey),
|
|
217
|
+
}, { includeSelf: false });
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
// 실패한 출력을 그냥 버리면 폰 화면이 깨진 채로 남으니, 실패한 자리부터 뒤쪽 전부를
|
|
221
|
+
// 다음 전송 앞에 다시 붙여 순서대로 재시도한다
|
|
222
|
+
session.outBuffer = chunk.slice(i) + session.outBuffer;
|
|
223
|
+
session.sendPenaltyUntil = Date.now() + SEND_PENALTY_MS;
|
|
224
|
+
console.error('출력 전송 실패, 다시 시도해요:', err.message);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
199
228
|
}
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
session.
|
|
204
|
-
|
|
205
|
-
if (!session.closing)
|
|
229
|
+
finally {
|
|
230
|
+
// 무슨 일이 있어도 표시는 반드시 푼다 — 여기 걸린 채로 남으면 그 세션은 그 뒤로
|
|
231
|
+
// 아무 화면도 못 보낸다 (지금 고치려는 바로 그 증상)
|
|
232
|
+
session.flushing = false;
|
|
233
|
+
// 남은 것(재시도분·보내는 사이에 새로 나온 것)이 있으면 이어서 보낸다
|
|
234
|
+
if (session.outBuffer && !session.closing)
|
|
206
235
|
scheduleFlush(session);
|
|
207
236
|
}
|
|
208
237
|
});
|
|
@@ -231,6 +260,7 @@ export async function runAgent(options) {
|
|
|
231
260
|
session.pendingFlush = null;
|
|
232
261
|
}
|
|
233
262
|
session.outBuffer = '';
|
|
263
|
+
session.flushing = false;
|
|
234
264
|
let snapshot;
|
|
235
265
|
try {
|
|
236
266
|
snapshot = session.serializer.serialize({ scrollback: RESTORE_SCROLLBACK_LINES });
|
|
@@ -256,7 +286,7 @@ export async function runAgent(options) {
|
|
|
256
286
|
}
|
|
257
287
|
};
|
|
258
288
|
const scheduleFlush = (session) => {
|
|
259
|
-
if (session.pendingFlush !== null)
|
|
289
|
+
if (session.pendingFlush !== null || session.flushing)
|
|
260
290
|
return;
|
|
261
291
|
const interval = Date.now() < session.sendPenaltyUntil ? PENALTY_INTERVAL_MS : MIN_SEND_INTERVAL_MS;
|
|
262
292
|
const wait = Math.max(0, interval - (Date.now() - session.lastSentAt));
|
|
@@ -377,6 +407,7 @@ export async function runAgent(options) {
|
|
|
377
407
|
inputApproved: false,
|
|
378
408
|
approvalInFlight: false,
|
|
379
409
|
outBuffer: '',
|
|
410
|
+
flushing: false,
|
|
380
411
|
sendChain: Promise.resolve(),
|
|
381
412
|
restoring: false,
|
|
382
413
|
restoreAgain: false,
|
|
@@ -792,7 +823,7 @@ export async function runAgent(options) {
|
|
|
792
823
|
});
|
|
793
824
|
return;
|
|
794
825
|
}
|
|
795
|
-
// 폴더 찾기에서 이름 바꾸기 / 지우기.
|
|
826
|
+
// 폴더 찾기에서 새로 만들기 / 이름 바꾸기 / 지우기. 무엇이든 끝나면 **새 목록**을 돌려줘서
|
|
796
827
|
// 폰 화면이 곧바로 최신 상태가 되게 한다 (실패 사유도 그 목록에 실어 보낸다).
|
|
797
828
|
if (frame.kind === 'path_action') {
|
|
798
829
|
const control = readControl(frame.data, session.frameKey);
|
|
@@ -810,6 +841,14 @@ export async function runAgent(options) {
|
|
|
810
841
|
if (!failure)
|
|
811
842
|
console.log(`📱 폰에서 이름을 바꿨어요: ${name} → ${to} (${dir})`);
|
|
812
843
|
}
|
|
844
|
+
else if (control.op === 'create') {
|
|
845
|
+
// 「+ 새로 만들기」 — 빈 파일이나 빈 폴더 하나. 이미 있는 이름이면 만들지 않는다.
|
|
846
|
+
const kind = control.kind === 'folder' ? 'folder' : 'file';
|
|
847
|
+
failure = createEntry(dir, name, kind).error;
|
|
848
|
+
if (!failure) {
|
|
849
|
+
console.log(`📱 폰에서 ${kind === 'folder' ? '폴더' : '파일'}를 만들었어요: ${name} (${dir})`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
813
852
|
else if (control.op === 'delete') {
|
|
814
853
|
const recursive = control.recursive === true;
|
|
815
854
|
const result = deleteEntry(dir, name, { recursive, protect: liveCwd(session) });
|
package/dist/browse.js
CHANGED
|
@@ -36,6 +36,28 @@ export function renameEntry(dir, name, to) {
|
|
|
36
36
|
return { error: `이름을 바꾸지 못했어요: ${err.message}` };
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
// 새로 만들기 — 지금 보고 있는 폴더 안에 빈 파일이나 빈 폴더를 하나 만든다.
|
|
40
|
+
// 이미 있는 이름은 절대 덮어쓰지 않는다 (내용이 날아가면 되돌릴 수 없다).
|
|
41
|
+
export function createEntry(dir, name, kind) {
|
|
42
|
+
const target = childPath(dir, name);
|
|
43
|
+
if (!target)
|
|
44
|
+
return { error: '이름에 「/」 나 「..」 은 쓸 수 없어요.' };
|
|
45
|
+
if (fs.existsSync(target))
|
|
46
|
+
return { error: `"${name}" 는 이미 있어요.` };
|
|
47
|
+
try {
|
|
48
|
+
if (kind === 'folder') {
|
|
49
|
+
fs.mkdirSync(target);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// wx = 이미 있으면 실패 — 위에서 한 번 봤지만 그 사이에 생겼을 수도 있어서 한 겹 더 막는다
|
|
53
|
+
fs.writeFileSync(target, '', { flag: 'wx' });
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return { error: `만들지 못했어요: ${err.message}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
39
61
|
// 지우기 — 파일은 바로, 폴더는 **비어 있을 때만**.
|
|
40
62
|
// 폰에서 한 번 잘못 누르면 되돌릴 수 없으므로, 안에 든 것까지 통째로 지우는 일은 하지 않는다
|
|
41
63
|
// (정말 필요하면 터미널에서 직접 지우면 된다).
|