botmux 2.24.5 → 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/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/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/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 +3 -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 +94 -31
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transient snapshot helper for tmux pipe-pane sessions.
|
|
3
|
+
*
|
|
4
|
+
* Instead of accumulating PTY data into a long-lived xterm-headless instance
|
|
5
|
+
* (the legacy renderer path), we ask tmux for a fresh ANSI snapshot of the
|
|
6
|
+
* current pane viewport and feed it to a throwaway xterm-headless that gets
|
|
7
|
+
* disposed immediately after we read it.
|
|
8
|
+
*
|
|
9
|
+
* Why: the long-lived buffer accumulates errors over time —
|
|
10
|
+
* - drift if the headless cols/rows don't match the real pane (web client
|
|
11
|
+
* resize changes the real pane via tmux resize-window, but the headless
|
|
12
|
+
* stays at spawn-time dimensions),
|
|
13
|
+
* - leftover history from before alt-buffer switches that the long buffer
|
|
14
|
+
* never strictly cleared,
|
|
15
|
+
* - cursor-positioning sequences emitted by the CLI landing at the wrong
|
|
16
|
+
* coordinates when the headless terminal is narrower than the real pane.
|
|
17
|
+
*
|
|
18
|
+
* Tmux's own state IS the authoritative current screen — capture-pane returns
|
|
19
|
+
* what the user actually sees in their terminal. Seeding a fresh xterm-headless
|
|
20
|
+
* with this snapshot every screenshot/screen-update means there is no
|
|
21
|
+
* accumulated state to drift.
|
|
22
|
+
*/
|
|
23
|
+
import xtermHeadless from '@xterm/headless';
|
|
24
|
+
const { Terminal } = xtermHeadless;
|
|
25
|
+
import { TmuxPipeBackend } from '../adapters/backend/tmux-pipe-backend.js';
|
|
26
|
+
import { readViewportText } from './terminal-renderer.js';
|
|
27
|
+
import { captureToPng } from './screenshot-renderer.js';
|
|
28
|
+
import { clamp, MIN_RENDER_COLS, MAX_RENDER_COLS, MIN_RENDER_ROWS, MAX_RENDER_ROWS } from './render-dimensions.js';
|
|
29
|
+
/** Attempt to capture a fresh ANSI snapshot from a TmuxPipeBackend. Returns
|
|
30
|
+
* null if the backend isn't a pipe backend, the pane has gone away, or
|
|
31
|
+
* tmux refuses to answer. Callers should fall back to the legacy renderer
|
|
32
|
+
* path on null. */
|
|
33
|
+
export function tryCapturePipeSnapshot(backend, fallbackCols, fallbackRows) {
|
|
34
|
+
if (!(backend instanceof TmuxPipeBackend))
|
|
35
|
+
return null;
|
|
36
|
+
const live = backend.getPaneSize();
|
|
37
|
+
const cols = clamp(live?.cols ?? fallbackCols, MIN_RENDER_COLS, MAX_RENDER_COLS);
|
|
38
|
+
const rows = clamp(live?.rows ?? fallbackRows, MIN_RENDER_ROWS, MAX_RENDER_ROWS);
|
|
39
|
+
// Viewport-only capture: same number of rows as the transient terminal,
|
|
40
|
+
// so the snapshot never overflows and triggers a normal-buffer scroll.
|
|
41
|
+
const ansi = backend.captureViewport();
|
|
42
|
+
if (!ansi)
|
|
43
|
+
return null;
|
|
44
|
+
return { cols, rows, ansi };
|
|
45
|
+
}
|
|
46
|
+
/** Feed an ANSI snapshot into a transient xterm-headless and yield the
|
|
47
|
+
* terminal once tmux's bytes have been consumed by the parser. Caller MUST
|
|
48
|
+
* dispose() the terminal when done. */
|
|
49
|
+
async function buildTransientTerminal(snap) {
|
|
50
|
+
const terminal = new Terminal({ cols: snap.cols, rows: snap.rows, allowProposedApi: true });
|
|
51
|
+
// xterm.write() with a callback resolves after the async parser has fully
|
|
52
|
+
// consumed the chunk. Without awaiting this, reading the buffer can race
|
|
53
|
+
// and pick up partial state mid-parse.
|
|
54
|
+
await new Promise(resolve => terminal.write(snap.ansi, () => resolve()));
|
|
55
|
+
return terminal;
|
|
56
|
+
}
|
|
57
|
+
/** Render a tmux pipe-pane snapshot to a PNG buffer. Returns null when the
|
|
58
|
+
* backend can't provide a snapshot — caller falls back to the legacy
|
|
59
|
+
* long-lived-renderer path. */
|
|
60
|
+
export async function snapshotToPng(backend, fallbackCols, fallbackRows) {
|
|
61
|
+
const snap = tryCapturePipeSnapshot(backend, fallbackCols, fallbackRows);
|
|
62
|
+
if (!snap)
|
|
63
|
+
return null;
|
|
64
|
+
const terminal = await buildTransientTerminal(snap);
|
|
65
|
+
try {
|
|
66
|
+
// Read the actual viewport — baseY may have shifted if the snapshot
|
|
67
|
+
// ended with a newline that triggered a normal-buffer scroll. We want
|
|
68
|
+
// the *current* visible rows, not buffer rows 0..N-1 which may be
|
|
69
|
+
// stale scrollback after that scroll.
|
|
70
|
+
const startY = terminal.buffer.active.baseY;
|
|
71
|
+
const png = captureToPng(terminal, { cols: snap.cols, rows: snap.rows, startY });
|
|
72
|
+
return { png, ansi: snap.ansi };
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
terminal.dispose();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Render a tmux pipe-pane snapshot to filtered text (drops the bare prompt
|
|
79
|
+
* + input echo lines, same rules as TerminalRenderer.snapshot()). Returns
|
|
80
|
+
* null when the backend can't provide a snapshot. */
|
|
81
|
+
export async function snapshotToText(backend, fallbackCols, fallbackRows, opts) {
|
|
82
|
+
const snap = tryCapturePipeSnapshot(backend, fallbackCols, fallbackRows);
|
|
83
|
+
if (!snap)
|
|
84
|
+
return null;
|
|
85
|
+
const terminal = await buildTransientTerminal(snap);
|
|
86
|
+
try {
|
|
87
|
+
// Read from the actual current viewport (see snapshotToPng comment).
|
|
88
|
+
const startY = terminal.buffer.active.baseY;
|
|
89
|
+
const content = readViewportText(terminal, { filter: opts.filter, startY, rows: snap.rows });
|
|
90
|
+
return { content, ansi: snap.ansi };
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
terminal.dispose();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=transient-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transient-snapshot.js","sourceRoot":"","sources":["../../src/utils/transient-snapshot.ts"],"names":[],"mappings":"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"}
|
package/dist/worker.js
CHANGED
|
@@ -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';
|
|
@@ -1574,6 +1575,10 @@ let renderCols = PTY_COLS;
|
|
|
1574
1575
|
let renderRows = PTY_ROWS;
|
|
1575
1576
|
// ─── Headless Terminal for Screen Capture ────────────────────────────────────
|
|
1576
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 = '';
|
|
1577
1582
|
let screenUpdateTimer = null;
|
|
1578
1583
|
const SCREEN_UPDATE_INTERVAL_MS = 2_000;
|
|
1579
1584
|
// ─── Scrollback Buffer (replay to late-connecting WS clients) ───────────────
|
|
@@ -1607,7 +1612,14 @@ function startScreenAnalyzer() {
|
|
|
1607
1612
|
extraHeaders: sa.extraHeaders,
|
|
1608
1613
|
extraBody: sa.extraBody,
|
|
1609
1614
|
}, {
|
|
1610
|
-
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
|
+
},
|
|
1611
1623
|
onAnalyzing: () => { },
|
|
1612
1624
|
onTuiPrompt: (description, options, multiSelect) => {
|
|
1613
1625
|
tuiPromptBlocking = true;
|
|
@@ -1709,28 +1721,41 @@ async function captureAndUpload() {
|
|
|
1709
1721
|
logScreenshotSkip('awaitingFirstPrompt');
|
|
1710
1722
|
return;
|
|
1711
1723
|
}
|
|
1712
|
-
if (!renderer) {
|
|
1713
|
-
logScreenshotSkip('renderer=null');
|
|
1714
|
-
return;
|
|
1715
|
-
}
|
|
1716
1724
|
if (!larkAppIdForUpload || !larkAppSecretForUpload) {
|
|
1717
1725
|
logScreenshotSkip('lark credentials missing');
|
|
1718
1726
|
return;
|
|
1719
1727
|
}
|
|
1720
|
-
const term = renderer.xterm;
|
|
1721
|
-
const startY = term.buffer.active.baseY;
|
|
1722
|
-
// Hash dedup — same content → skip upload. Not logged: this is the expected
|
|
1723
|
-
// "nothing changed" path and would dominate the log signal.
|
|
1724
|
-
const snap = renderer.rawSnapshot();
|
|
1725
|
-
const hash = createHash('md5').update(snap).digest('hex');
|
|
1726
|
-
if (hash === lastShotHash)
|
|
1727
|
-
return;
|
|
1728
|
-
lastShotHash = hash;
|
|
1729
1728
|
let png;
|
|
1730
1729
|
try {
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
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
|
+
}
|
|
1734
1759
|
}
|
|
1735
1760
|
catch (err) {
|
|
1736
1761
|
logError(`Screenshot render failed: ${err?.message ?? err}`);
|
|
@@ -2187,18 +2212,46 @@ function startScreenUpdates() {
|
|
|
2187
2212
|
// content (the live failure that prompted this fix).
|
|
2188
2213
|
renderer = new TerminalRenderer(renderCols, renderRows);
|
|
2189
2214
|
let lastSentStatus;
|
|
2215
|
+
let lastTextSnapshotHash = '';
|
|
2190
2216
|
screenUpdateTimer = setInterval(() => {
|
|
2191
|
-
if (
|
|
2217
|
+
if (awaitingFirstPrompt)
|
|
2192
2218
|
return;
|
|
2193
|
-
const { content, changed } = renderer.snapshot();
|
|
2194
2219
|
let status = isPromptReady ? 'idle' : 'working';
|
|
2195
2220
|
if (screenAnalyzer?.isAnalyzing)
|
|
2196
2221
|
status = 'analyzing';
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
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
|
+
})();
|
|
2202
2255
|
}, SCREEN_UPDATE_INTERVAL_MS);
|
|
2203
2256
|
}
|
|
2204
2257
|
function stopScreenUpdates() {
|
|
@@ -2210,6 +2263,7 @@ function stopScreenUpdates() {
|
|
|
2210
2263
|
renderer.dispose();
|
|
2211
2264
|
renderer = null;
|
|
2212
2265
|
}
|
|
2266
|
+
lastAnalyzerSnapshot = '';
|
|
2213
2267
|
}
|
|
2214
2268
|
// ─── PTY Management ──────────────────────────────────────────────────────────
|
|
2215
2269
|
function spawnCli(cfg) {
|
|
@@ -2395,9 +2449,10 @@ function spawnCli(cfg) {
|
|
|
2395
2449
|
log('tmux backend requested but functional probe failed — falling back to PTY backend');
|
|
2396
2450
|
useTmux = false;
|
|
2397
2451
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2452
|
+
const selectedBackend = selectSessionBackend({ sessionId: cfg.sessionId, useTmux });
|
|
2453
|
+
isTmuxMode = selectedBackend.isTmuxMode;
|
|
2454
|
+
isPipeMode = selectedBackend.isPipeMode;
|
|
2455
|
+
backend = selectedBackend.backend;
|
|
2401
2456
|
// Claude Code appends a line to ~/.claude/projects/<cwd-hash>/<sid>.jsonl each
|
|
2402
2457
|
// time the user submits. The adapter uses this file to verify paste+Enter
|
|
2403
2458
|
// actually committed (rather than trusting a fixed sleep), so wire it up now.
|
|
@@ -2470,9 +2525,6 @@ function spawnCli(cfg) {
|
|
|
2470
2525
|
// On tmux re-attach, keep awaitingFirstPrompt = true so screen updates are
|
|
2471
2526
|
// suppressed until the idle detector fires markNewTurn() — this prevents the
|
|
2472
2527
|
// full tmux scrollback history from leaking into the streaming card.
|
|
2473
|
-
if (tmuxBe?.isReattach) {
|
|
2474
|
-
log('Re-attached to existing tmux session');
|
|
2475
|
-
}
|
|
2476
2528
|
// Bridge fallback: claude-code only. Tail Claude's transcript JSONL so a
|
|
2477
2529
|
// turn the model finishes WITHOUT calling `botmux send` still gets its
|
|
2478
2530
|
// assistant text forwarded to Lark (the gate in emitReadyTurns suppresses
|
|
@@ -2547,6 +2599,17 @@ function spawnCli(cfg) {
|
|
|
2547
2599
|
isPromptReady = false;
|
|
2548
2600
|
send({ type: 'claude_exit', code, signal });
|
|
2549
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
|
+
}
|
|
2550
2613
|
// Fallback: if the CLI takes too long to show its prompt (e.g. slow
|
|
2551
2614
|
// plugin init), unblock screen updates so the card doesn't stay at
|
|
2552
2615
|
// "启动中" forever. markNewTurn() sets a clean baseline at the current
|