@seamnet/client 0.20.2 → 0.20.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/lib/tmux-utils.cjs +99 -9
- package/package.json +1 -1
package/lib/tmux-utils.cjs
CHANGED
|
@@ -18,21 +18,28 @@ const EXEC_OPTS = { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pi
|
|
|
18
18
|
// kind → 启动命令族。声明式数据小表,不是 adapter。新增 kind 加一行即可。
|
|
19
19
|
// - fresh:全新会话的启动命令
|
|
20
20
|
// - resume:续上一次会话的命令(restart 默认走这个;--no-continue 走 fresh)
|
|
21
|
-
// -
|
|
22
|
-
// - exit
|
|
21
|
+
// - busyPatterns/idleWaitMs/idleSettleMs:restart 先等 TUI 真空闲并短暂复核稳定;超时才退回 resetKeys 强推
|
|
22
|
+
// - resetKeys/resetWaitMs:仅兜底路径用;发 exit 前尝试把 TUI 从忙窗口里拉回可退出态
|
|
23
|
+
// - exit:TUI 内的优雅退出命令(空闲时直接发它 + Enter)
|
|
23
24
|
// - downTimeout:发了 exit 后等进程退出的轮询超时(ms)
|
|
24
25
|
const KIND_CMD = {
|
|
25
26
|
cc: {
|
|
26
27
|
fresh: 'claude --dangerously-skip-permissions',
|
|
27
28
|
resume: 'claude --dangerously-skip-permissions --continue',
|
|
29
|
+
busyPatterns: [/esc to interrupt/i],
|
|
30
|
+
idleWaitMs: 120000,
|
|
31
|
+
idleSettleMs: 500,
|
|
28
32
|
resetKeys: ['Escape'],
|
|
29
33
|
resetWaitMs: 1000,
|
|
30
34
|
exit: '/exit',
|
|
31
35
|
downTimeout: 120000, // cc 退出跑 Stop hook(auto-save / semantic_memory)要 ~90s
|
|
32
36
|
},
|
|
33
37
|
codex: {
|
|
34
|
-
fresh: 'codex
|
|
38
|
+
fresh: 'codex',
|
|
35
39
|
resume: 'codex resume --last',
|
|
40
|
+
busyPatterns: [/esc to interrupt/i, /Working \(/i],
|
|
41
|
+
idleWaitMs: 60000,
|
|
42
|
+
idleSettleMs: 1000,
|
|
36
43
|
resetKeys: ['Escape', 'Escape'],
|
|
37
44
|
resetWaitMs: 1000,
|
|
38
45
|
exit: '/exit',
|
|
@@ -137,6 +144,59 @@ function readTerm(session, { lines = 20, full = false, socketPath } = {}) {
|
|
|
137
144
|
return raw.replace(/\s+$/, '').split('\n').slice(-n).join('\n');
|
|
138
145
|
}
|
|
139
146
|
|
|
147
|
+
function readViewport(session, { lines = 80, socketPath } = {}) {
|
|
148
|
+
const tmux = tmuxCmd(socketPath);
|
|
149
|
+
const raw = execSync(
|
|
150
|
+
`${tmux} capture-pane -t "${session}" -p`,
|
|
151
|
+
{ ...EXEC_OPTS, maxBuffer: 4 * 1024 * 1024 }
|
|
152
|
+
);
|
|
153
|
+
const n = parseInt(lines, 10) || 80;
|
|
154
|
+
return raw.replace(/\s+$/, '').split('\n').slice(-n).join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function matchesBusy(screenText, busyPatterns = []) {
|
|
158
|
+
return busyPatterns.some((pattern) => pattern.test(screenText));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function waitForIdle(
|
|
162
|
+
session,
|
|
163
|
+
socketPath,
|
|
164
|
+
{ targetKind, busyPatterns = [], idleWaitMs = 0, idleSettleMs = 0 }
|
|
165
|
+
) {
|
|
166
|
+
if (!(idleWaitMs > 0) || busyPatterns.length === 0) {
|
|
167
|
+
return { ok: true, lastScreen: '' };
|
|
168
|
+
}
|
|
169
|
+
const deadline = Date.now() + idleWaitMs;
|
|
170
|
+
let lastScreen = '';
|
|
171
|
+
let idleSince = null;
|
|
172
|
+
while (true) {
|
|
173
|
+
if (detectKind(session, socketPath) !== targetKind) {
|
|
174
|
+
return { ok: true, kindChanged: true, lastScreen };
|
|
175
|
+
}
|
|
176
|
+
lastScreen = readViewport(session, { socketPath });
|
|
177
|
+
if (!matchesBusy(lastScreen, busyPatterns)) {
|
|
178
|
+
if (!(idleSettleMs > 0)) {
|
|
179
|
+
return { ok: true, lastScreen };
|
|
180
|
+
}
|
|
181
|
+
if (idleSince == null) idleSince = Date.now();
|
|
182
|
+
if (Date.now() - idleSince >= idleSettleMs) {
|
|
183
|
+
return { ok: true, lastScreen };
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
idleSince = null;
|
|
187
|
+
}
|
|
188
|
+
if (Date.now() >= deadline) {
|
|
189
|
+
return { ok: false, lastScreen };
|
|
190
|
+
}
|
|
191
|
+
await sleep(1000);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function appendWarning(existing, next) {
|
|
196
|
+
if (!existing) return next;
|
|
197
|
+
return `${existing};${next}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
140
200
|
/**
|
|
141
201
|
* 给 session 发文本和/或命名按键。
|
|
142
202
|
* - text:字面文本(-l)。默认末尾自动 Enter;noEnter 或同时有 keys 时抑制自动 Enter。
|
|
@@ -204,21 +264,47 @@ function startTerm({ kind, cmd, session, dir, socketPath } = {}) {
|
|
|
204
264
|
async function restartSkeleton(
|
|
205
265
|
session,
|
|
206
266
|
socketPath,
|
|
207
|
-
{
|
|
267
|
+
{
|
|
268
|
+
targetKind,
|
|
269
|
+
busyPatterns = [],
|
|
270
|
+
idleWaitMs = 0,
|
|
271
|
+
idleSettleMs = 0,
|
|
272
|
+
resetAction = async () => {},
|
|
273
|
+
stopAction,
|
|
274
|
+
downTimeout,
|
|
275
|
+
startCmd,
|
|
276
|
+
}
|
|
208
277
|
) {
|
|
209
278
|
const tmux = tmuxCmd(socketPath);
|
|
210
279
|
let warning = null;
|
|
211
280
|
|
|
212
|
-
// 1.
|
|
213
|
-
await
|
|
214
|
-
|
|
281
|
+
// 1. 先等真空闲;整段预算都没等到,才退回 resetKey 强推
|
|
282
|
+
const idleState = await waitForIdle(session, socketPath, {
|
|
283
|
+
targetKind,
|
|
284
|
+
busyPatterns,
|
|
285
|
+
idleWaitMs,
|
|
286
|
+
idleSettleMs,
|
|
287
|
+
});
|
|
288
|
+
if (!idleState.ok) {
|
|
289
|
+
warning = appendWarning(
|
|
290
|
+
warning,
|
|
291
|
+
'未等到空闲窗口,已改用中断后退出;可能打断记忆保存'
|
|
292
|
+
);
|
|
293
|
+
await resetAction();
|
|
294
|
+
}
|
|
295
|
+
if (detectKind(session, socketPath) === targetKind) {
|
|
296
|
+
stopAction();
|
|
297
|
+
}
|
|
215
298
|
|
|
216
299
|
// 2. poll-down:轮询直到 kind 不再是 targetKind;超时强制 kill 兜底
|
|
217
300
|
const downDeadline = Date.now() + downTimeout;
|
|
218
301
|
while (detectKind(session, socketPath) === targetKind) {
|
|
219
302
|
if (Date.now() >= downDeadline) {
|
|
220
|
-
warning =
|
|
221
|
-
|
|
303
|
+
warning = appendWarning(
|
|
304
|
+
warning,
|
|
305
|
+
`${targetKind} 未在 ${Math.round(downTimeout / 1000)}s 内优雅退出,已强制终止;`
|
|
306
|
+
+ (targetKind === 'codex' ? 'resume 的会话可能不完整' : '会话状态可能不完整')
|
|
307
|
+
);
|
|
222
308
|
try { execSync(`${tmux} send-keys -t "${session}" C-c`, EXEC_OPTS); } catch {}
|
|
223
309
|
await sleep(2000);
|
|
224
310
|
if (detectKind(session, socketPath) === targetKind) {
|
|
@@ -292,6 +378,9 @@ async function restartTerm(session, { socketPath, continueSession = true } = {})
|
|
|
292
378
|
const startCmd = continueSession ? spec.resume : spec.fresh;
|
|
293
379
|
return restartSkeleton(session, socketPath, {
|
|
294
380
|
targetKind: kind,
|
|
381
|
+
busyPatterns: spec.busyPatterns,
|
|
382
|
+
idleWaitMs: spec.idleWaitMs,
|
|
383
|
+
idleSettleMs: spec.idleSettleMs,
|
|
295
384
|
resetAction: async () => {
|
|
296
385
|
if (Array.isArray(spec.resetKeys) && spec.resetKeys.length > 0) {
|
|
297
386
|
for (const key of spec.resetKeys) {
|
|
@@ -322,4 +411,5 @@ function stopTerm(session, { socketPath } = {}) {
|
|
|
322
411
|
module.exports = {
|
|
323
412
|
tmuxCmd, detectKind, listTerms, readTerm, sendTerm,
|
|
324
413
|
startTerm, restartTerm, stopTerm, KIND_CMD,
|
|
414
|
+
readViewport, matchesBusy, waitForIdle,
|
|
325
415
|
};
|