botmux 2.24.4 → 2.24.6
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 +4 -2
- package/README.md +4 -2
- package/dist/adapters/backend/session-backend-selector.d.ts +11 -0
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -0
- package/dist/adapters/backend/session-backend-selector.js +26 -0
- package/dist/adapters/backend/session-backend-selector.js.map +1 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +55 -15
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +163 -21
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +91 -15
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/shared-hints.js +2 -2
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/cli/arg-utils.d.ts +11 -0
- package/dist/cli/arg-utils.d.ts.map +1 -0
- package/dist/cli/arg-utils.js +25 -0
- package/dist/cli/arg-utils.js.map +1 -0
- package/dist/cli/quoted-render.d.ts +30 -0
- package/dist/cli/quoted-render.d.ts.map +1 -0
- package/dist/cli/quoted-render.js +29 -0
- package/dist/cli/quoted-render.js.map +1 -0
- package/dist/cli.js +65 -16
- package/dist/cli.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +34 -19
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +13 -4
- package/dist/daemon.js.map +1 -1
- package/dist/im/lark/client.d.ts +7 -7
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +14 -15
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/md-card.d.ts +21 -0
- package/dist/im/lark/md-card.d.ts.map +1 -1
- package/dist/im/lark/md-card.js +63 -0
- package/dist/im/lark/md-card.js.map +1 -1
- package/dist/im/lark/message-parser.d.ts +9 -3
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +13 -5
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/quote-hint.d.ts +18 -0
- package/dist/im/lark/quote-hint.d.ts.map +1 -0
- package/dist/im/lark/quote-hint.js +23 -0
- package/dist/im/lark/quote-hint.js.map +1 -0
- package/dist/services/coco-transcript.d.ts +11 -8
- package/dist/services/coco-transcript.d.ts.map +1 -1
- package/dist/services/coco-transcript.js +77 -40
- package/dist/services/coco-transcript.js.map +1 -1
- package/dist/services/codex-transcript.d.ts +26 -10
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js +95 -30
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/skills/definitions.d.ts +4 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +69 -12
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/installer.d.ts +3 -1
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +18 -3
- package/dist/skills/installer.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.js +1 -1
- package/dist/utils/screenshot-renderer.d.ts.map +1 -1
- package/dist/utils/screenshot-renderer.js +63 -26
- package/dist/utils/screenshot-renderer.js.map +1 -1
- package/dist/utils/terminal-renderer.d.ts +16 -0
- package/dist/utils/terminal-renderer.d.ts.map +1 -1
- package/dist/utils/terminal-renderer.js +35 -21
- package/dist/utils/terminal-renderer.js.map +1 -1
- package/dist/utils/transient-snapshot.d.ts +28 -0
- package/dist/utils/transient-snapshot.d.ts.map +1 -0
- package/dist/utils/transient-snapshot.js +96 -0
- package/dist/utils/transient-snapshot.js.map +1 -0
- package/dist/worker.js +172 -71
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/worker.js
CHANGED
|
@@ -20,7 +20,7 @@ import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './ser
|
|
|
20
20
|
import { shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
|
|
21
21
|
import { shouldRunQuietRotation, evaluatePidResolverPullback, decideFingerprintSwitch, sessionIdFromJsonlPath, SESSION_ID_FILENAME_RE, } from './services/bridge-rotation-policy.js';
|
|
22
22
|
import { CodexBridgeQueue } from './services/codex-bridge-queue.js';
|
|
23
|
-
import { drainCodexRollout, findCodexRolloutBySessionId, findCodexRolloutByPid, splitCodexEventsByCutoff } from './services/codex-transcript.js';
|
|
23
|
+
import { drainCodexRollout, findCodexRolloutBySessionId, findCodexRolloutByPid, splitCodexEventsByCutoff, extractLastCodexTurn } from './services/codex-transcript.js';
|
|
24
24
|
import { cocoEventsPathForSession, drainCocoEvents, findCocoSessionByPid } from './services/coco-transcript.js';
|
|
25
25
|
import { dirname } from 'node:path';
|
|
26
26
|
import { createServer as createHttpServer } from 'node:http';
|
|
@@ -29,13 +29,14 @@ import { TerminalRenderer } from './utils/terminal-renderer.js';
|
|
|
29
29
|
import { DEFAULT_RENDER_COLS, DEFAULT_RENDER_ROWS, MAX_RENDER_COLS, MAX_RENDER_ROWS, MIN_RENDER_COLS, MIN_RENDER_ROWS, clamp, resolveRenderDimensions, } from './utils/render-dimensions.js';
|
|
30
30
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
31
31
|
import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds } from './adapters/cli/claude-code.js';
|
|
32
|
-
import { PtyBackend } from './adapters/backend/pty-backend.js';
|
|
33
32
|
import { TmuxBackend } from './adapters/backend/tmux-backend.js';
|
|
34
33
|
import { TmuxPipeBackend } from './adapters/backend/tmux-pipe-backend.js';
|
|
34
|
+
import { selectSessionBackend } from './adapters/backend/session-backend-selector.js';
|
|
35
35
|
import { tmuxEnv } from './setup/ensure-tmux.js';
|
|
36
36
|
import { IdleDetector } from './utils/idle-detector.js';
|
|
37
37
|
import { ScreenAnalyzer } from './utils/screen-analyzer.js';
|
|
38
38
|
import { captureToPng } from './utils/screenshot-renderer.js';
|
|
39
|
+
import { snapshotToPng, snapshotToText } from './utils/transient-snapshot.js';
|
|
39
40
|
import { uploadImageBuffer } from './utils/lark-upload.js';
|
|
40
41
|
import { config } from './config.js';
|
|
41
42
|
import * as sessionStore from './services/session-store.js';
|
|
@@ -164,6 +165,11 @@ let codexAdoptPendingPid;
|
|
|
164
165
|
* but BEFORE the rollout was located still reach the Lark thread. 5s
|
|
165
166
|
* skew tolerance is applied on top, mirroring the Lark/Claude bridges. */
|
|
166
167
|
let codexAdoptStartMs;
|
|
168
|
+
/** Adopt-only: 一次性发送的 "/adopt 前最后一轮" preamble 是否已经触发过。
|
|
169
|
+
* codexBridgeAttach 在 split-live 分支会查 history 取最后一对 user/assistant
|
|
170
|
+
* 发给 daemon —— late-attach poller 也会反复走这条分支(每秒一次),所以
|
|
171
|
+
* 必须有标志位防重发。镜像 claude 那套 bridgePreambleSent 的角色。 */
|
|
172
|
+
let codexBridgePreambleSent = false;
|
|
167
173
|
/** Cap the preamble text so an extremely long previous turn doesn't blow
|
|
168
174
|
* past Lark's per-message limit. The user only needs enough to recall
|
|
169
175
|
* context, not the entire transcript. */
|
|
@@ -179,41 +185,23 @@ function truncatePreambleText(text, max) {
|
|
|
179
185
|
return text;
|
|
180
186
|
return text.slice(0, max) + '…';
|
|
181
187
|
}
|
|
182
|
-
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
|
|
187
|
-
function formatLocalTurnContent(userText, assistantText) {
|
|
188
|
+
/** Prepare a local-turn `final_output` payload. The daemon owns the card
|
|
189
|
+
* chrome (label/quote/markdown body), so we ship the user prompt and
|
|
190
|
+
* assistant text as separate fields — see card-builder `buildContextualReplyCard`.
|
|
191
|
+
* Returns null when both sides are empty so the caller can skip the emit. */
|
|
192
|
+
function formatLocalTurnFields(userText, assistantText) {
|
|
188
193
|
const u = truncatePreambleText(userText.trim(), LOCAL_TURN_USER_MAX);
|
|
189
194
|
const a = truncatePreambleText(assistantText.trim(), LOCAL_TURN_ASSISTANT_MAX);
|
|
190
195
|
if (!u && !a)
|
|
191
196
|
return null;
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
'',
|
|
198
|
-
`🤖 ${cliName()}:`,
|
|
199
|
-
a || '(空)',
|
|
200
|
-
].join('\n');
|
|
201
|
-
}
|
|
202
|
-
/** Compose a `final_output` payload for a HEADLESS local turn — assistant
|
|
203
|
-
* text arrived without a known user side. Typically: daemon restart cut
|
|
204
|
-
* off an in-flight model stream; the original user event was absorbed
|
|
205
|
-
* at baseline so we have no userUuid to resolve, but the rest of the
|
|
206
|
-
* reply still deserves to land in Lark. */
|
|
197
|
+
return { userText: u, content: a };
|
|
198
|
+
}
|
|
199
|
+
/** Same as `formatLocalTurnFields` but for HEADLESS local turns — daemon
|
|
200
|
+
* restart cut off an in-flight model stream so we have an assistant side
|
|
201
|
+
* with no resolvable user prompt. */
|
|
207
202
|
function formatHeadlessLocalTurnContent(assistantText) {
|
|
208
203
|
const a = truncatePreambleText(assistantText.trim(), LOCAL_TURN_ASSISTANT_MAX);
|
|
209
|
-
|
|
210
|
-
return null;
|
|
211
|
-
return [
|
|
212
|
-
'🖥️ 终端本地对话续传(daemon 重启时模型正在输出)',
|
|
213
|
-
'',
|
|
214
|
-
`🤖 ${cliName()}:`,
|
|
215
|
-
a,
|
|
216
|
-
].join('\n');
|
|
204
|
+
return a || null;
|
|
217
205
|
}
|
|
218
206
|
// ─── Bridge fallback marker (non-adopt) ────────────────────────────────────
|
|
219
207
|
//
|
|
@@ -290,6 +278,30 @@ function maybeEmitAdoptPreamble(events) {
|
|
|
290
278
|
});
|
|
291
279
|
log('Bridge adopt preamble emitted (last completed turn from baseline)');
|
|
292
280
|
}
|
|
281
|
+
/** Codex / CoCo 镜像版:split-live 攒齐 history 后挑最后一对 user/assistant_final
|
|
282
|
+
* 发回 daemon 渲染成 "📜 /adopt 前最后一轮" 卡片。语义、跳过条件、字数截断都
|
|
283
|
+
* 对齐 maybeEmitAdoptPreamble;区别只在事件取出方式(codex/coco 是结构化
|
|
284
|
+
* event,不需要走 claude 那套 jsonl turn assembly)。 */
|
|
285
|
+
function maybeEmitCodexAdoptPreamble(history) {
|
|
286
|
+
if (!lastInitConfig?.adoptMode)
|
|
287
|
+
return;
|
|
288
|
+
if (lastInitConfig?.adoptRestoredFromMetadata)
|
|
289
|
+
return;
|
|
290
|
+
if (codexBridgePreambleSent)
|
|
291
|
+
return;
|
|
292
|
+
const turn = extractLastCodexTurn(history);
|
|
293
|
+
if (!turn)
|
|
294
|
+
return;
|
|
295
|
+
if (!turn.userText.trim() && !turn.assistantText.trim())
|
|
296
|
+
return;
|
|
297
|
+
codexBridgePreambleSent = true;
|
|
298
|
+
send({
|
|
299
|
+
type: 'adopt_preamble',
|
|
300
|
+
userText: truncatePreambleText(turn.userText, PREAMBLE_USER_MAX),
|
|
301
|
+
assistantText: truncatePreambleText(turn.assistantText, PREAMBLE_ASSISTANT_MAX),
|
|
302
|
+
});
|
|
303
|
+
log('Codex bridge adopt preamble emitted (last completed turn from split-live history)');
|
|
304
|
+
}
|
|
293
305
|
/** Extract the sessionId from a Claude jsonl path and add it to the
|
|
294
306
|
* known-sid set. Validates the filename against Claude's UUID-shaped
|
|
295
307
|
* sessionId pattern so non-Claude jsonls in the project dir (accidental
|
|
@@ -1150,18 +1162,31 @@ function emitReadyTurns() {
|
|
|
1150
1162
|
// attachment.prompt) so type-ahead'd local input renders the same as
|
|
1151
1163
|
// a normally-typed pane prompt.
|
|
1152
1164
|
const userEv = drained.events.find(e => e.uuid === turn.userUuid);
|
|
1153
|
-
const
|
|
1154
|
-
const
|
|
1155
|
-
if (!
|
|
1165
|
+
const rawUserText = userEv ? extractTurnStartText(userEv) : '';
|
|
1166
|
+
const fields = formatLocalTurnFields(rawUserText, assistantText);
|
|
1167
|
+
if (!fields)
|
|
1156
1168
|
continue;
|
|
1157
|
-
send({
|
|
1169
|
+
send({
|
|
1170
|
+
type: 'final_output',
|
|
1171
|
+
content: fields.content,
|
|
1172
|
+
lastUuid,
|
|
1173
|
+
turnId: turn.turnId,
|
|
1174
|
+
kind: 'local-turn',
|
|
1175
|
+
userText: fields.userText,
|
|
1176
|
+
});
|
|
1158
1177
|
continue;
|
|
1159
1178
|
}
|
|
1160
1179
|
// Headless local turn — see formatHeadlessLocalTurnContent for context.
|
|
1161
1180
|
const headlessContent = formatHeadlessLocalTurnContent(assistantText);
|
|
1162
1181
|
if (!headlessContent)
|
|
1163
1182
|
continue;
|
|
1164
|
-
send({
|
|
1183
|
+
send({
|
|
1184
|
+
type: 'final_output',
|
|
1185
|
+
content: headlessContent,
|
|
1186
|
+
lastUuid,
|
|
1187
|
+
turnId: turn.turnId,
|
|
1188
|
+
kind: 'local-turn-headless',
|
|
1189
|
+
});
|
|
1165
1190
|
continue;
|
|
1166
1191
|
}
|
|
1167
1192
|
send({ type: 'final_output', content: assistantText, lastUuid, turnId: turn.turnId });
|
|
@@ -1301,6 +1326,7 @@ function codexBridgeAttach(rolloutPath, mode) {
|
|
|
1301
1326
|
codexBridgePendingTail = result.pendingTail;
|
|
1302
1327
|
codexBridgeBaselineDone = true;
|
|
1303
1328
|
log(`Codex bridge split-live: ${rolloutPath} (history=${history.length}, live=${live.length}, cutoff=${cutoff}, offset=${codexBridgeOffset})`);
|
|
1329
|
+
maybeEmitCodexAdoptPreamble(history);
|
|
1304
1330
|
}
|
|
1305
1331
|
else if (mode === 'split-live') {
|
|
1306
1332
|
// split-live requested but file missing — degrade to fresh: the file
|
|
@@ -1341,6 +1367,12 @@ function codexBridgeAttach(rolloutPath, mode) {
|
|
|
1341
1367
|
catch (err) {
|
|
1342
1368
|
log(`Codex bridge fs.watch unavailable (${err.message}); relying on poller`);
|
|
1343
1369
|
}
|
|
1370
|
+
// macOS 上 fs.watch 对 codex/coco 的外部进程追加 rollout / events.jsonl
|
|
1371
|
+
// 经常静默丢事件(FSEvents 跨进程不可靠),所以无论 watcher 是否 attach
|
|
1372
|
+
// 成功,都必须起 1s poller 兜底 —— 不然 split-live 成功的 adopt session
|
|
1373
|
+
// 在 macOS 上会卡死,永远收不到模型回复。Linux 上 poller 多 tick 也无害
|
|
1374
|
+
// (codexBridgeIngest 在 offset 未推进时是 no-op)。
|
|
1375
|
+
codexBridgeStartTimer();
|
|
1344
1376
|
}
|
|
1345
1377
|
/** Called from flushPending after writeInput first returns a cliSessionId.
|
|
1346
1378
|
* Tries to locate the rollout file immediately; if it's not on disk yet,
|
|
@@ -1421,12 +1453,19 @@ function emitReadyCodexTurns() {
|
|
|
1421
1453
|
if (turn.isLocal) {
|
|
1422
1454
|
// Local turn (adopt only): user typed in iTerm. Surface both sides
|
|
1423
1455
|
// so the Lark thread sees a complete exchange instead of an orphan
|
|
1424
|
-
// reply.
|
|
1425
|
-
// Lark's per-message limit.
|
|
1426
|
-
const
|
|
1427
|
-
if (!
|
|
1456
|
+
// reply. formatLocalTurnFields caps both texts to keep within
|
|
1457
|
+
// Lark's per-message limit; daemon owns the card chrome.
|
|
1458
|
+
const fields = formatLocalTurnFields(turn.userText ?? '', turn.finalText);
|
|
1459
|
+
if (!fields)
|
|
1428
1460
|
continue;
|
|
1429
|
-
send({
|
|
1461
|
+
send({
|
|
1462
|
+
type: 'final_output',
|
|
1463
|
+
content: fields.content,
|
|
1464
|
+
lastUuid: turn.turnId,
|
|
1465
|
+
turnId: turn.turnId,
|
|
1466
|
+
kind: 'local-turn',
|
|
1467
|
+
userText: fields.userText,
|
|
1468
|
+
});
|
|
1430
1469
|
continue;
|
|
1431
1470
|
}
|
|
1432
1471
|
send({ type: 'final_output', content: turn.finalText, lastUuid: turn.turnId, turnId: turn.turnId });
|
|
@@ -1536,6 +1575,10 @@ let renderCols = PTY_COLS;
|
|
|
1536
1575
|
let renderRows = PTY_ROWS;
|
|
1537
1576
|
// ─── Headless Terminal for Screen Capture ────────────────────────────────────
|
|
1538
1577
|
let renderer = null;
|
|
1578
|
+
/** Most recent unfiltered viewport text — kept in sync by the screen_update
|
|
1579
|
+
* timer for pipe-pane backends so ScreenAnalyzer (which is synchronous) has
|
|
1580
|
+
* a fresh snapshot to read without needing its own tmux capture-pane call. */
|
|
1581
|
+
let lastAnalyzerSnapshot = '';
|
|
1539
1582
|
let screenUpdateTimer = null;
|
|
1540
1583
|
const SCREEN_UPDATE_INTERVAL_MS = 2_000;
|
|
1541
1584
|
// ─── Scrollback Buffer (replay to late-connecting WS clients) ───────────────
|
|
@@ -1569,7 +1612,14 @@ function startScreenAnalyzer() {
|
|
|
1569
1612
|
extraHeaders: sa.extraHeaders,
|
|
1570
1613
|
extraBody: sa.extraBody,
|
|
1571
1614
|
}, {
|
|
1572
|
-
getSnapshot: () =>
|
|
1615
|
+
getSnapshot: () => {
|
|
1616
|
+
// ScreenAnalyzer is called every ~5s for TUI-prompt detection. We
|
|
1617
|
+
// can't make this async without overhauling the analyzer, so cache
|
|
1618
|
+
// the last pipe-pane text snapshot here and refresh it eagerly.
|
|
1619
|
+
// For pipe-pane backends, the cache is repopulated by the screen
|
|
1620
|
+
// update timer; for others, fall through to the long-lived renderer.
|
|
1621
|
+
return lastAnalyzerSnapshot || renderer?.rawSnapshot() || '';
|
|
1622
|
+
},
|
|
1573
1623
|
onAnalyzing: () => { },
|
|
1574
1624
|
onTuiPrompt: (description, options, multiSelect) => {
|
|
1575
1625
|
tuiPromptBlocking = true;
|
|
@@ -1671,28 +1721,41 @@ async function captureAndUpload() {
|
|
|
1671
1721
|
logScreenshotSkip('awaitingFirstPrompt');
|
|
1672
1722
|
return;
|
|
1673
1723
|
}
|
|
1674
|
-
if (!renderer) {
|
|
1675
|
-
logScreenshotSkip('renderer=null');
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
1724
|
if (!larkAppIdForUpload || !larkAppSecretForUpload) {
|
|
1679
1725
|
logScreenshotSkip('lark credentials missing');
|
|
1680
1726
|
return;
|
|
1681
1727
|
}
|
|
1682
|
-
const term = renderer.xterm;
|
|
1683
|
-
const startY = term.buffer.active.baseY;
|
|
1684
|
-
// Hash dedup — same content → skip upload. Not logged: this is the expected
|
|
1685
|
-
// "nothing changed" path and would dominate the log signal.
|
|
1686
|
-
const snap = renderer.rawSnapshot();
|
|
1687
|
-
const hash = createHash('md5').update(snap).digest('hex');
|
|
1688
|
-
if (hash === lastShotHash)
|
|
1689
|
-
return;
|
|
1690
|
-
lastShotHash = hash;
|
|
1691
1728
|
let png;
|
|
1692
1729
|
try {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1730
|
+
// Preferred path: pipe-pane backends ask tmux for a fresh viewport
|
|
1731
|
+
// snapshot and render it through a transient xterm-headless. This
|
|
1732
|
+
// avoids the accumulated-buffer drift that produced duplicated /
|
|
1733
|
+
// staircase content under the legacy long-lived renderer.
|
|
1734
|
+
const pipeResult = await snapshotToPng(backend, renderCols, renderRows);
|
|
1735
|
+
if (pipeResult) {
|
|
1736
|
+
if (pipeResult.ansi === lastShotHash)
|
|
1737
|
+
return;
|
|
1738
|
+
lastShotHash = pipeResult.ansi;
|
|
1739
|
+
png = pipeResult.png;
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
// Fallback path: non-pipe backends (PtyBackend, legacy TmuxBackend)
|
|
1743
|
+
// still drive the long-lived renderer.
|
|
1744
|
+
if (!renderer) {
|
|
1745
|
+
logScreenshotSkip('renderer=null');
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const term = renderer.xterm;
|
|
1749
|
+
const startY = term.buffer.active.baseY;
|
|
1750
|
+
const snap = renderer.rawSnapshot();
|
|
1751
|
+
const hash = createHash('md5').update(snap).digest('hex');
|
|
1752
|
+
if (hash === lastShotHash)
|
|
1753
|
+
return;
|
|
1754
|
+
lastShotHash = hash;
|
|
1755
|
+
const shotCols = clamp(term.cols, MIN_RENDER_COLS, MAX_RENDER_COLS);
|
|
1756
|
+
const shotRows = clamp(term.rows, MIN_RENDER_ROWS, MAX_RENDER_ROWS);
|
|
1757
|
+
png = captureToPng(term, { cols: shotCols, rows: shotRows, startY });
|
|
1758
|
+
}
|
|
1696
1759
|
}
|
|
1697
1760
|
catch (err) {
|
|
1698
1761
|
logError(`Screenshot render failed: ${err?.message ?? err}`);
|
|
@@ -2149,18 +2212,46 @@ function startScreenUpdates() {
|
|
|
2149
2212
|
// content (the live failure that prompted this fix).
|
|
2150
2213
|
renderer = new TerminalRenderer(renderCols, renderRows);
|
|
2151
2214
|
let lastSentStatus;
|
|
2215
|
+
let lastTextSnapshotHash = '';
|
|
2152
2216
|
screenUpdateTimer = setInterval(() => {
|
|
2153
|
-
if (
|
|
2217
|
+
if (awaitingFirstPrompt)
|
|
2154
2218
|
return;
|
|
2155
|
-
const { content, changed } = renderer.snapshot();
|
|
2156
2219
|
let status = isPromptReady ? 'idle' : 'working';
|
|
2157
2220
|
if (screenAnalyzer?.isAnalyzing)
|
|
2158
2221
|
status = 'analyzing';
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2222
|
+
void (async () => {
|
|
2223
|
+
let content;
|
|
2224
|
+
let changed;
|
|
2225
|
+
// Preferred path: pipe-pane backends pull a fresh viewport snapshot
|
|
2226
|
+
// from tmux every tick. This eliminates the accumulated-buffer drift
|
|
2227
|
+
// that produced duplicated/staircase text in 'text' display mode.
|
|
2228
|
+
const pipeText = await snapshotToText(backend, renderCols, renderRows, { filter: true });
|
|
2229
|
+
if (pipeText) {
|
|
2230
|
+
content = pipeText.content;
|
|
2231
|
+
const hash = pipeText.ansi;
|
|
2232
|
+
changed = hash !== lastTextSnapshotHash;
|
|
2233
|
+
lastTextSnapshotHash = hash;
|
|
2234
|
+
// Refresh the unfiltered cache that ScreenAnalyzer reads from. Same
|
|
2235
|
+
// tmux call would otherwise need to fire twice per tick.
|
|
2236
|
+
if (changed) {
|
|
2237
|
+
const rawSnap = await snapshotToText(backend, renderCols, renderRows, { filter: false });
|
|
2238
|
+
if (rawSnap)
|
|
2239
|
+
lastAnalyzerSnapshot = rawSnap.content;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
else if (renderer) {
|
|
2243
|
+
const snap = renderer.snapshot();
|
|
2244
|
+
content = snap.content;
|
|
2245
|
+
changed = snap.changed;
|
|
2246
|
+
}
|
|
2247
|
+
else {
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
if (changed || status !== lastSentStatus) {
|
|
2251
|
+
lastSentStatus = status;
|
|
2252
|
+
send({ type: 'screen_update', content, status });
|
|
2253
|
+
}
|
|
2254
|
+
})();
|
|
2164
2255
|
}, SCREEN_UPDATE_INTERVAL_MS);
|
|
2165
2256
|
}
|
|
2166
2257
|
function stopScreenUpdates() {
|
|
@@ -2172,6 +2263,7 @@ function stopScreenUpdates() {
|
|
|
2172
2263
|
renderer.dispose();
|
|
2173
2264
|
renderer = null;
|
|
2174
2265
|
}
|
|
2266
|
+
lastAnalyzerSnapshot = '';
|
|
2175
2267
|
}
|
|
2176
2268
|
// ─── PTY Management ──────────────────────────────────────────────────────────
|
|
2177
2269
|
function spawnCli(cfg) {
|
|
@@ -2357,9 +2449,10 @@ function spawnCli(cfg) {
|
|
|
2357
2449
|
log('tmux backend requested but functional probe failed — falling back to PTY backend');
|
|
2358
2450
|
useTmux = false;
|
|
2359
2451
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2452
|
+
const selectedBackend = selectSessionBackend({ sessionId: cfg.sessionId, useTmux });
|
|
2453
|
+
isTmuxMode = selectedBackend.isTmuxMode;
|
|
2454
|
+
isPipeMode = selectedBackend.isPipeMode;
|
|
2455
|
+
backend = selectedBackend.backend;
|
|
2363
2456
|
// Claude Code appends a line to ~/.claude/projects/<cwd-hash>/<sid>.jsonl each
|
|
2364
2457
|
// time the user submits. The adapter uses this file to verify paste+Enter
|
|
2365
2458
|
// actually committed (rather than trusting a fixed sleep), so wire it up now.
|
|
@@ -2432,9 +2525,6 @@ function spawnCli(cfg) {
|
|
|
2432
2525
|
// On tmux re-attach, keep awaitingFirstPrompt = true so screen updates are
|
|
2433
2526
|
// suppressed until the idle detector fires markNewTurn() — this prevents the
|
|
2434
2527
|
// full tmux scrollback history from leaking into the streaming card.
|
|
2435
|
-
if (tmuxBe?.isReattach) {
|
|
2436
|
-
log('Re-attached to existing tmux session');
|
|
2437
|
-
}
|
|
2438
2528
|
// Bridge fallback: claude-code only. Tail Claude's transcript JSONL so a
|
|
2439
2529
|
// turn the model finishes WITHOUT calling `botmux send` still gets its
|
|
2440
2530
|
// assistant text forwarded to Lark (the gate in emitReadyTurns suppresses
|
|
@@ -2509,6 +2599,17 @@ function spawnCli(cfg) {
|
|
|
2509
2599
|
isPromptReady = false;
|
|
2510
2600
|
send({ type: 'claude_exit', code, signal });
|
|
2511
2601
|
});
|
|
2602
|
+
if (isPipeMode && backend instanceof TmuxPipeBackend && backend.isReattach) {
|
|
2603
|
+
log(`Re-attached to existing tmux session via pipe-pane: ${TmuxBackend.sessionName(cfg.sessionId)}`);
|
|
2604
|
+
try {
|
|
2605
|
+
const initial = backend.captureCurrentScreen();
|
|
2606
|
+
if (initial.length > 0)
|
|
2607
|
+
onPtyData(initial);
|
|
2608
|
+
}
|
|
2609
|
+
catch (err) {
|
|
2610
|
+
log(`captureCurrentScreen failed: ${err.message}`);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2512
2613
|
// Fallback: if the CLI takes too long to show its prompt (e.g. slow
|
|
2513
2614
|
// plugin init), unblock screen updates so the card doesn't stay at
|
|
2514
2615
|
// "启动中" forever. markNewTurn() sets a clean baseline at the current
|