botmux 2.2.7 → 2.3.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/README.en.md +63 -13
- package/README.md +52 -14
- package/dist/adapters/backend/tmux-backend.d.ts +8 -0
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +18 -0
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/cli/aiden.d.ts.map +1 -1
- package/dist/adapters/cli/aiden.js +0 -40
- package/dist/adapters/cli/aiden.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +21 -67
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.d.ts.map +1 -1
- package/dist/adapters/cli/coco.js +0 -33
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +0 -27
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/gemini.d.ts.map +1 -1
- package/dist/adapters/cli/gemini.js +1 -29
- package/dist/adapters/cli/gemini.js.map +1 -1
- package/dist/adapters/cli/opencode.d.ts.map +1 -1
- package/dist/adapters/cli/opencode.js +1 -44
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +11 -8
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/cli.js +737 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +8 -4
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/scheduler.d.ts +38 -16
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +335 -149
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/session-manager.d.ts +6 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +105 -16
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +26 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +15 -3
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +233 -31
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +49 -10
- package/dist/daemon.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +29 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +241 -55
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +1 -0
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +195 -40
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/services/schedule-store.d.ts +20 -3
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +140 -16
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/skills/definitions.d.ts +17 -0
- package/dist/skills/definitions.d.ts.map +1 -0
- package/dist/skills/definitions.js +254 -0
- package/dist/skills/definitions.js.map +1 -0
- package/dist/skills/installer.d.ts +9 -0
- package/dist/skills/installer.d.ts.map +1 -0
- package/dist/skills/installer.js +42 -0
- package/dist/skills/installer.js.map +1 -0
- package/dist/types.d.ts +84 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -5
- package/dist/types.js.map +1 -1
- package/dist/utils/lark-upload.d.ts +2 -0
- package/dist/utils/lark-upload.d.ts.map +1 -0
- package/dist/utils/lark-upload.js +27 -0
- package/dist/utils/lark-upload.js.map +1 -0
- package/dist/utils/screen-analyzer.d.ts +67 -0
- package/dist/utils/screen-analyzer.d.ts.map +1 -0
- package/dist/utils/screen-analyzer.js +279 -0
- package/dist/utils/screen-analyzer.js.map +1 -0
- package/dist/utils/screenshot-renderer.d.ts +11 -0
- package/dist/utils/screenshot-renderer.d.ts.map +1 -0
- package/dist/utils/screenshot-renderer.js +225 -0
- package/dist/utils/screenshot-renderer.js.map +1 -0
- package/dist/utils/terminal-renderer.d.ts +30 -0
- package/dist/utils/terminal-renderer.d.ts.map +1 -1
- package/dist/utils/terminal-renderer.js +25 -0
- package/dist/utils/terminal-renderer.js.map +1 -1
- package/dist/worker.js +372 -14
- package/dist/worker.js.map +1 -1
- package/package.json +2 -5
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +0 -1
- package/dist/server.d.ts +0 -3
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -133
- package/dist/server.js.map +0 -1
- package/dist/tools/get-thread-messages.d.ts +0 -26
- package/dist/tools/get-thread-messages.d.ts.map +0 -1
- package/dist/tools/get-thread-messages.js +0 -38
- package/dist/tools/get-thread-messages.js.map +0 -1
- package/dist/tools/index.d.ts +0 -9
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -10
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/list-bots.d.ts +0 -40
- package/dist/tools/list-bots.d.ts.map +0 -1
- package/dist/tools/list-bots.js +0 -77
- package/dist/tools/list-bots.js.map +0 -1
- package/dist/tools/send-to-thread.d.ts +0 -46
- package/dist/tools/send-to-thread.d.ts.map +0 -1
- package/dist/tools/send-to-thread.js +0 -275
- package/dist/tools/send-to-thread.js.map +0 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-powered screen analyzer — detects interactive TUI prompts in terminal output
|
|
3
|
+
* by sending snapshots to a lightweight LLM and parsing structured responses.
|
|
4
|
+
*
|
|
5
|
+
* Token-saving protocol:
|
|
6
|
+
* 1. Text diff — skip if snapshot unchanged
|
|
7
|
+
* 2. Cumulative stability — require N consecutive unchanged snapshots
|
|
8
|
+
* 3. AI-driven cooldown — AI returns checkAgainWhen to control re-call timing
|
|
9
|
+
* 4. Prompt-active guard — stop calling AI once a prompt is reported, until resolved
|
|
10
|
+
*/
|
|
11
|
+
const SYSTEM_PROMPT = `You are a terminal screen analyzer. Analyze the terminal screenshot and determine if the CLI is showing a **blocking interactive prompt** that requires the user to make a selection before the CLI can proceed.
|
|
12
|
+
|
|
13
|
+
A BLOCKING interactive prompt looks like:
|
|
14
|
+
- A modal dialog with numbered options and a cursor (❯ or >) pointing to the selected option
|
|
15
|
+
- Examples: "Resume from summary / Resume full session / Don't ask me again", "Yes / No / Cancel"
|
|
16
|
+
- The CLI cannot proceed until the user selects an option
|
|
17
|
+
|
|
18
|
+
The following are NOT interactive prompts (return needsInteraction=false):
|
|
19
|
+
- Status bar text like "bypass permissions (shift+tab to cycle)" — this is a persistent status indicator, not a blocking prompt
|
|
20
|
+
- CLI idle state showing an input cursor (❯) waiting for the user to type a message — this is normal operation
|
|
21
|
+
- Progress indicators, spinners, or loading animations
|
|
22
|
+
- Error messages or informational output
|
|
23
|
+
- Any text that is part of the CLI's normal UI chrome (toolbars, status bars, mode indicators)
|
|
24
|
+
|
|
25
|
+
Return ONLY valid JSON (no markdown, no extra text):
|
|
26
|
+
{
|
|
27
|
+
"needsInteraction": boolean,
|
|
28
|
+
"description": "what is being asked",
|
|
29
|
+
"options": [{"label": "1", "text": "option text", "type": "select", "index": 0}],
|
|
30
|
+
"multiSelect": false,
|
|
31
|
+
"toggleKey": "Space",
|
|
32
|
+
"confirmKey": "Enter",
|
|
33
|
+
"checkAgainWhen": "content_changed" | "after_5s" | "after_10s" | "not_needed"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
For each option:
|
|
37
|
+
- label: exact label shown in TUI. If none, use sequential numbers.
|
|
38
|
+
- text: full option text
|
|
39
|
+
- type: "select" (single pick), "toggle" (multi-select checkbox), "confirm" (submit/next/done), "input" (free text like "Type something")
|
|
40
|
+
- index: the 0-based position of this option in the list (0 = first navigable item, 1 = second, etc.). Count ALL navigable items from top to bottom in order, including submit/next buttons. DO NOT count non-interactive lines like descriptions or separators.
|
|
41
|
+
|
|
42
|
+
toggleKey: the key used to toggle a checkbox item. Read the hint line at the bottom (e.g. "Space to toggle", "Enter to select"). Default "Space".
|
|
43
|
+
confirmKey: the key used to confirm/submit. Usually "Enter".
|
|
44
|
+
multiSelect: true if checkboxes ([ ]/[✓]/[✗], "可多选"), false for single-select (❯ cursor).
|
|
45
|
+
|
|
46
|
+
IMPORTANT for multiSelect prompts:
|
|
47
|
+
- There is almost always a Submit/Next/Done button. Look for items like "Submit", "Next", "Submit answers", "Done" — they are navigable items with type "confirm".
|
|
48
|
+
- "Submit" may appear AFTER "Type something" as a sub-item or on the next line. It is still a separate navigable option — include it with its own index.
|
|
49
|
+
- If the cursor (❯) is pointing at "Submit" or "Next", that is a confirm-type option.
|
|
50
|
+
- Count its index correctly — it is a navigable position between "Type something" and "Chat about this".
|
|
51
|
+
|
|
52
|
+
Important: "index" is the COUNT of ↓ presses needed to reach this option from the FIRST option (index 0). The first option is always index 0.
|
|
53
|
+
|
|
54
|
+
Rules for checkAgainWhen:
|
|
55
|
+
- "content_changed": call again when content changes
|
|
56
|
+
- "after_5s" or "after_10s": check back after a delay
|
|
57
|
+
- "not_needed": CLI is working normally or idle — don't call until content changes substantially
|
|
58
|
+
|
|
59
|
+
If needsInteraction is false, omit description and options fields.`;
|
|
60
|
+
export class ScreenAnalyzer {
|
|
61
|
+
config;
|
|
62
|
+
callbacks;
|
|
63
|
+
timer = null;
|
|
64
|
+
// Stability tracking
|
|
65
|
+
lastSnapshot = '';
|
|
66
|
+
stableCount = 0;
|
|
67
|
+
// AI-driven cooldown
|
|
68
|
+
lastAnalyzedSnapshot = '';
|
|
69
|
+
waitingForContentChange = false;
|
|
70
|
+
timerCooldownUntil = 0;
|
|
71
|
+
// Prompt state
|
|
72
|
+
promptActive = false;
|
|
73
|
+
// Concurrency guard — prevent overlapping AI calls
|
|
74
|
+
_analyzing = false;
|
|
75
|
+
// Disposed flag
|
|
76
|
+
disposed = false;
|
|
77
|
+
constructor(config, callbacks) {
|
|
78
|
+
this.config = config;
|
|
79
|
+
this.callbacks = callbacks;
|
|
80
|
+
}
|
|
81
|
+
/** Whether an AI call is currently in flight */
|
|
82
|
+
get isAnalyzing() { return this._analyzing; }
|
|
83
|
+
start() {
|
|
84
|
+
if (this.timer)
|
|
85
|
+
return;
|
|
86
|
+
this.timer = setInterval(() => this.tick(), this.config.intervalMs);
|
|
87
|
+
this.callbacks.log('ScreenAnalyzer started');
|
|
88
|
+
}
|
|
89
|
+
dispose() {
|
|
90
|
+
this.disposed = true;
|
|
91
|
+
if (this.timer) {
|
|
92
|
+
clearInterval(this.timer);
|
|
93
|
+
this.timer = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Called externally when user has made a selection via card click */
|
|
97
|
+
notifySelection(selectedText) {
|
|
98
|
+
this.promptActive = false;
|
|
99
|
+
// Reset all cooldowns so we check again soon to confirm prompt is gone
|
|
100
|
+
this.waitingForContentChange = false;
|
|
101
|
+
this.timerCooldownUntil = 0;
|
|
102
|
+
this.stableCount = 0;
|
|
103
|
+
this.lastSnapshot = '';
|
|
104
|
+
this.callbacks.log(`ScreenAnalyzer: selection made — "${selectedText}"`);
|
|
105
|
+
}
|
|
106
|
+
async tick() {
|
|
107
|
+
if (this.disposed)
|
|
108
|
+
return;
|
|
109
|
+
// Don't call AI while a prompt is active — we already notified Daemon.
|
|
110
|
+
if (this.promptActive)
|
|
111
|
+
return;
|
|
112
|
+
// Don't overlap AI calls
|
|
113
|
+
if (this._analyzing)
|
|
114
|
+
return;
|
|
115
|
+
// Layer 1: Text diff — get current snapshot
|
|
116
|
+
const snapshot = this.callbacks.getSnapshot();
|
|
117
|
+
if (!snapshot)
|
|
118
|
+
return;
|
|
119
|
+
const truncated = snapshot.length > this.config.snapshotMaxChars
|
|
120
|
+
? snapshot.slice(-this.config.snapshotMaxChars)
|
|
121
|
+
: snapshot;
|
|
122
|
+
if (truncated === this.lastSnapshot) {
|
|
123
|
+
this.stableCount++;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.stableCount = 1;
|
|
127
|
+
this.lastSnapshot = truncated;
|
|
128
|
+
if (this.waitingForContentChange) {
|
|
129
|
+
this.waitingForContentChange = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Layer 2: Cumulative stability — require N consecutive unchanged snapshots
|
|
133
|
+
if (this.stableCount < this.config.stableCount)
|
|
134
|
+
return;
|
|
135
|
+
// Layer 3: AI-driven cooldown
|
|
136
|
+
if (this.waitingForContentChange && truncated === this.lastAnalyzedSnapshot)
|
|
137
|
+
return;
|
|
138
|
+
if (this.timerCooldownUntil > Date.now())
|
|
139
|
+
return;
|
|
140
|
+
// All layers passed — call AI
|
|
141
|
+
this._analyzing = true;
|
|
142
|
+
this.callbacks.onAnalyzing();
|
|
143
|
+
try {
|
|
144
|
+
await this.analyze(truncated);
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
this._analyzing = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async analyze(snapshot) {
|
|
151
|
+
this.lastAnalyzedSnapshot = snapshot;
|
|
152
|
+
let analysis;
|
|
153
|
+
try {
|
|
154
|
+
analysis = await this.callAI(snapshot);
|
|
155
|
+
if (analysis.needsInteraction) {
|
|
156
|
+
this.callbacks.log(`ScreenAnalyzer AI input:\n${snapshot.slice(-1500)}`);
|
|
157
|
+
this.callbacks.log(`ScreenAnalyzer AI response: ${JSON.stringify(analysis)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
this.callbacks.log(`ScreenAnalyzer AI call failed: ${err.message}`);
|
|
162
|
+
this.waitingForContentChange = true;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// Apply checkAgainWhen
|
|
166
|
+
switch (analysis.checkAgainWhen) {
|
|
167
|
+
case 'content_changed':
|
|
168
|
+
case 'not_needed':
|
|
169
|
+
this.waitingForContentChange = true;
|
|
170
|
+
break;
|
|
171
|
+
case 'after_5s':
|
|
172
|
+
this.timerCooldownUntil = Date.now() + 5_000;
|
|
173
|
+
this.waitingForContentChange = false;
|
|
174
|
+
break;
|
|
175
|
+
case 'after_10s':
|
|
176
|
+
this.timerCooldownUntil = Date.now() + 10_000;
|
|
177
|
+
this.waitingForContentChange = false;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (analysis.needsInteraction && analysis.options && analysis.options.length > 0) {
|
|
181
|
+
// Generate keys deterministically in code, using AI-provided index + toggleKey/confirmKey
|
|
182
|
+
const toggleKey = analysis.toggleKey || 'Space';
|
|
183
|
+
const confirmKey = analysis.confirmKey || 'Enter';
|
|
184
|
+
// Fallback: if multiSelect but AI didn't return any confirm option,
|
|
185
|
+
// add a synthetic "Submit" confirm at the end (uses Enter at current position)
|
|
186
|
+
if (analysis.multiSelect && !analysis.options.some(o => o.type === 'confirm')) {
|
|
187
|
+
const maxIndex = Math.max(...analysis.options.map(o => o.index));
|
|
188
|
+
analysis.options.push({
|
|
189
|
+
label: '✅',
|
|
190
|
+
text: 'Submit',
|
|
191
|
+
selected: false,
|
|
192
|
+
type: 'confirm',
|
|
193
|
+
index: maxIndex + 1,
|
|
194
|
+
});
|
|
195
|
+
this.callbacks.log('ScreenAnalyzer: auto-added fallback confirm button');
|
|
196
|
+
}
|
|
197
|
+
for (const opt of analysis.options) {
|
|
198
|
+
const keys = [];
|
|
199
|
+
for (let i = 0; i < opt.index; i++)
|
|
200
|
+
keys.push('Down');
|
|
201
|
+
if (opt.type === 'toggle') {
|
|
202
|
+
keys.push(toggleKey);
|
|
203
|
+
// Return to top for next toggle
|
|
204
|
+
for (let i = 0; i < opt.index; i++)
|
|
205
|
+
keys.push('Up');
|
|
206
|
+
}
|
|
207
|
+
else if (opt.type === 'select' || opt.type === 'confirm') {
|
|
208
|
+
keys.push(confirmKey);
|
|
209
|
+
}
|
|
210
|
+
else if (opt.type === 'input') {
|
|
211
|
+
keys.push(confirmKey); // select the input option
|
|
212
|
+
}
|
|
213
|
+
opt.keys = keys;
|
|
214
|
+
}
|
|
215
|
+
this.promptActive = true;
|
|
216
|
+
this.callbacks.onTuiPrompt(analysis.description ?? 'CLI needs your selection', analysis.options, !!analysis.multiSelect);
|
|
217
|
+
this.callbacks.log(`ScreenAnalyzer: TUI prompt detected — ${analysis.description}${analysis.multiSelect ? ' (multi-select)' : ''} [toggleKey=${toggleKey}, confirmKey=${confirmKey}]`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async callAI(snapshot) {
|
|
221
|
+
const url = `${this.config.baseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
222
|
+
const body = {
|
|
223
|
+
model: this.config.model,
|
|
224
|
+
messages: [
|
|
225
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
226
|
+
{ role: 'user', content: snapshot },
|
|
227
|
+
],
|
|
228
|
+
temperature: 0,
|
|
229
|
+
max_tokens: 2048,
|
|
230
|
+
// Extra body params from config (e.g. { thinking: { type: "disabled" } } for Ark)
|
|
231
|
+
...(this.config.extraBody ?? {}),
|
|
232
|
+
};
|
|
233
|
+
const resp = await fetch(url, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: {
|
|
236
|
+
'Content-Type': 'application/json',
|
|
237
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
238
|
+
...(this.config.extraHeaders ?? {}),
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify(body),
|
|
241
|
+
signal: AbortSignal.timeout(15_000),
|
|
242
|
+
});
|
|
243
|
+
if (!resp.ok) {
|
|
244
|
+
const text = await resp.text().catch(() => '');
|
|
245
|
+
throw new Error(`AI API ${resp.status}: ${text.slice(0, 200)}`);
|
|
246
|
+
}
|
|
247
|
+
const data = await resp.json();
|
|
248
|
+
const content = data?.choices?.[0]?.message?.content ?? '';
|
|
249
|
+
// Parse JSON from response — handle potential markdown wrapping
|
|
250
|
+
const jsonStr = content.replace(/^```json?\s*/, '').replace(/\s*```$/, '').trim();
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(jsonStr);
|
|
253
|
+
return {
|
|
254
|
+
needsInteraction: !!parsed.needsInteraction,
|
|
255
|
+
description: parsed.description,
|
|
256
|
+
multiSelect: !!parsed.multiSelect,
|
|
257
|
+
toggleKey: parsed.toggleKey || 'Space',
|
|
258
|
+
confirmKey: parsed.confirmKey || 'Enter',
|
|
259
|
+
options: Array.isArray(parsed.options)
|
|
260
|
+
? parsed.options.map((o, i) => ({
|
|
261
|
+
label: o.label || String(i + 1),
|
|
262
|
+
// Clean text: collapse newlines/multi-line descriptions into single line
|
|
263
|
+
text: (o.text || '').replace(/\n+/g, ' ').trim(),
|
|
264
|
+
selected: !!o.selected,
|
|
265
|
+
type: (['select', 'toggle', 'confirm', 'input'].includes(o.type) ? o.type : 'select'),
|
|
266
|
+
index: typeof o.index === 'number' ? o.index : i,
|
|
267
|
+
}))
|
|
268
|
+
: undefined,
|
|
269
|
+
checkAgainWhen: ['content_changed', 'after_5s', 'after_10s', 'not_needed'].includes(parsed.checkAgainWhen)
|
|
270
|
+
? parsed.checkAgainWhen
|
|
271
|
+
: 'content_changed',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
throw new Error(`Failed to parse AI response as JSON: ${jsonStr.slice(0, 200)}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=screen-analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screen-analyzer.js","sourceRoot":"","sources":["../../src/utils/screen-analyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import xtermHeadless from '@xterm/headless';
|
|
2
|
+
type Terminal = InstanceType<typeof xtermHeadless.Terminal>;
|
|
3
|
+
export interface CaptureOpts {
|
|
4
|
+
cols: number;
|
|
5
|
+
rows: number;
|
|
6
|
+
startY: number;
|
|
7
|
+
}
|
|
8
|
+
/** Capture a section of the terminal buffer to a PNG buffer. */
|
|
9
|
+
export declare function captureToPng(terminal: Terminal, opts: CaptureOpts): Buffer;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=screenshot-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot-renderer.d.ts","sourceRoot":"","sources":["../../src/utils/screenshot-renderer.ts"],"names":[],"mappings":"AASA,OAAO,aAAa,MAAM,iBAAiB,CAAC;AAC5C,KAAK,QAAQ,GAAG,YAAY,CAAC,OAAO,aAAa,CAAC,QAAQ,CAAC,CAAC;AAuJ5D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,CAqE1E"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders an xterm-headless buffer to a PNG buffer using @napi-rs/canvas.
|
|
3
|
+
* ANSI 256-color palette + RGB true color, bold weight, inverse, default fg/bg.
|
|
4
|
+
* Tokyo Night theme (matches src/worker.ts web terminal).
|
|
5
|
+
*/
|
|
6
|
+
import { createCanvas, GlobalFonts } from '@napi-rs/canvas';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
// ─── Font registration ──────────────────────────────────────────────────────
|
|
11
|
+
// @napi-rs/canvas does NOT auto-discover system fonts on Linux; we must
|
|
12
|
+
// explicitly register a path. We build an ordered fallback chain so missing
|
|
13
|
+
// glyphs in the primary font (e.g. emoji, dingbats) get picked up by a later
|
|
14
|
+
// font in the chain (skia walks the family list per glyph).
|
|
15
|
+
const fontFamilyChain = [];
|
|
16
|
+
let fontInitialized = false;
|
|
17
|
+
function tryRegister(path, alias) {
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return false;
|
|
20
|
+
try {
|
|
21
|
+
GlobalFonts.registerFromPath(path, alias);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function ensureFontRegistered() {
|
|
29
|
+
if (fontInitialized)
|
|
30
|
+
return;
|
|
31
|
+
fontInitialized = true;
|
|
32
|
+
// 1. Project-bundled font(s) under assets/fonts/ — drop a TTF here to override defaults.
|
|
33
|
+
const projectFontDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'assets', 'fonts');
|
|
34
|
+
for (const fname of ['BotmuxMono-Regular.ttf', 'BotmuxMono.ttf', 'BotmuxMono-Regular.otf']) {
|
|
35
|
+
if (tryRegister(join(projectFontDir, fname), 'BotmuxMono')) {
|
|
36
|
+
tryRegister(join(projectFontDir, 'BotmuxMono-Bold.ttf'), 'BotmuxMono');
|
|
37
|
+
fontFamilyChain.push('BotmuxMono');
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 2. CJK monospace — primary for Latin + Han glyphs.
|
|
42
|
+
const cjkRegular = [
|
|
43
|
+
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
|
|
44
|
+
'/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
|
|
45
|
+
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
|
|
46
|
+
'/usr/share/fonts/opentype/noto/NotoSansMonoCJK-Regular.ttc',
|
|
47
|
+
];
|
|
48
|
+
const cjkBold = [
|
|
49
|
+
'/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',
|
|
50
|
+
'/usr/share/fonts/noto-cjk/NotoSansCJK-Bold.ttc',
|
|
51
|
+
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Bold.ttc',
|
|
52
|
+
'/usr/share/fonts/opentype/noto/NotoSansMonoCJK-Bold.ttc',
|
|
53
|
+
];
|
|
54
|
+
for (const p of cjkRegular) {
|
|
55
|
+
if (tryRegister(p)) {
|
|
56
|
+
for (const bp of cjkBold)
|
|
57
|
+
if (tryRegister(bp))
|
|
58
|
+
break;
|
|
59
|
+
fontFamilyChain.push('Noto Sans Mono CJK SC');
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// 3. Latin monospace — DejaVu/Liberation cover dingbats (❯, ✓, etc.) and
|
|
64
|
+
// most box-drawing/geometric symbols not in CJK font.
|
|
65
|
+
const latinCandidates = [
|
|
66
|
+
['/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', 'DejaVu Sans Mono'],
|
|
67
|
+
['/usr/share/fonts/dejavu/DejaVuSansMono.ttf', 'DejaVu Sans Mono'],
|
|
68
|
+
['/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf', 'Liberation Mono'],
|
|
69
|
+
['/usr/share/fonts/liberation/LiberationMono-Regular.ttf', 'Liberation Mono'],
|
|
70
|
+
];
|
|
71
|
+
for (const [p, name] of latinCandidates) {
|
|
72
|
+
if (tryRegister(p)) {
|
|
73
|
+
// Best-effort bold companion
|
|
74
|
+
tryRegister(p.replace(/Regular\.ttf$/, 'Bold.ttf'));
|
|
75
|
+
tryRegister(p.replace(/SansMono\.ttf$/, 'SansMono-Bold.ttf'));
|
|
76
|
+
fontFamilyChain.push(name);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 4. Color emoji — Noto Color Emoji on Linux, Apple Color Emoji on macOS.
|
|
81
|
+
// skia handles CBDT/CBLC + COLR/CPAL color formats.
|
|
82
|
+
const emojiCandidates = [
|
|
83
|
+
['/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf', 'Noto Color Emoji'],
|
|
84
|
+
['/usr/share/fonts/noto/NotoColorEmoji.ttf', 'Noto Color Emoji'],
|
|
85
|
+
['/usr/share/fonts/google-noto-emoji/NotoColorEmoji.ttf', 'Noto Color Emoji'],
|
|
86
|
+
['/Library/Fonts/Apple Color Emoji.ttc', 'Apple Color Emoji'],
|
|
87
|
+
['/System/Library/Fonts/Apple Color Emoji.ttc', 'Apple Color Emoji'],
|
|
88
|
+
];
|
|
89
|
+
for (const [p, name] of emojiCandidates) {
|
|
90
|
+
if (tryRegister(p)) {
|
|
91
|
+
fontFamilyChain.push(name);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (fontFamilyChain.length === 0)
|
|
96
|
+
fontFamilyChain.push('monospace');
|
|
97
|
+
}
|
|
98
|
+
function fontSpec(bold) {
|
|
99
|
+
const families = fontFamilyChain.map(f => `"${f}"`).join(', ');
|
|
100
|
+
return `${bold ? 'bold ' : ''}${FONT_SIZE}px ${families}, monospace`;
|
|
101
|
+
}
|
|
102
|
+
/** Detect whether the leading codepoint is an emoji/symbol pictograph that
|
|
103
|
+
* uses bitmap glyph metrics — these need vertical centering, not top-align. */
|
|
104
|
+
function isPictograph(ch) {
|
|
105
|
+
if (!ch)
|
|
106
|
+
return false;
|
|
107
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
108
|
+
return ((cp >= 0x1F300 && cp <= 0x1FAFF) || // emoji + extended pictographs
|
|
109
|
+
(cp >= 0x1F900 && cp <= 0x1F9FF) || // supplemental symbols
|
|
110
|
+
(cp >= 0x1F1E6 && cp <= 0x1F1FF) || // regional indicators (flags)
|
|
111
|
+
(cp >= 0x2600 && cp <= 0x27BF) || // misc symbols + dingbats (✓✗★⚠ etc.)
|
|
112
|
+
cp === 0x231A || cp === 0x231B || // ⌚ ⌛
|
|
113
|
+
cp === 0x23E9 || cp === 0x23EA || cp === 0x23EB || cp === 0x23EC ||
|
|
114
|
+
cp === 0x23F0 || cp === 0x23F3 || // ⏰ ⏳
|
|
115
|
+
cp === 0x25FD || cp === 0x25FE || // ◽ ◾
|
|
116
|
+
cp === 0x2B50 || cp === 0x2B55 || // ⭐ ⭕
|
|
117
|
+
cp === 0x303D || cp === 0x3297 || cp === 0x3299);
|
|
118
|
+
}
|
|
119
|
+
const BG = '#1a1b26';
|
|
120
|
+
const FG = '#a9b1d6';
|
|
121
|
+
const ANSI16 = [
|
|
122
|
+
'#15161e', '#f7768e', '#9ece6a', '#e0af68', '#7aa2f7', '#bb9af7', '#7dcfff', '#a9b1d6',
|
|
123
|
+
'#414868', '#ff7a93', '#b9f27c', '#ff9e64', '#7da6ff', '#bb9af7', '#0db9d7', '#c0caf5',
|
|
124
|
+
];
|
|
125
|
+
const PALETTE_256 = (() => {
|
|
126
|
+
const p = [...ANSI16];
|
|
127
|
+
const ramp = [0, 95, 135, 175, 215, 255];
|
|
128
|
+
for (let r = 0; r < 6; r++) {
|
|
129
|
+
for (let g = 0; g < 6; g++) {
|
|
130
|
+
for (let b = 0; b < 6; b++) {
|
|
131
|
+
p.push(`#${ramp[r].toString(16).padStart(2, '0')}${ramp[g].toString(16).padStart(2, '0')}${ramp[b].toString(16).padStart(2, '0')}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < 24; i++) {
|
|
136
|
+
const v = (8 + i * 10).toString(16).padStart(2, '0');
|
|
137
|
+
p.push(`#${v}${v}${v}`);
|
|
138
|
+
}
|
|
139
|
+
return p;
|
|
140
|
+
})();
|
|
141
|
+
const FONT_SIZE = 14;
|
|
142
|
+
const CELL_W = 8.4;
|
|
143
|
+
const CELL_H = 18;
|
|
144
|
+
const PADDING = 8;
|
|
145
|
+
function colorOf(cell, isBg) {
|
|
146
|
+
const isDefault = isBg ? cell.isBgDefault() : cell.isFgDefault();
|
|
147
|
+
if (isDefault)
|
|
148
|
+
return null;
|
|
149
|
+
const isRGB = isBg ? cell.isBgRGB() : cell.isFgRGB();
|
|
150
|
+
const isPalette = isBg ? cell.isBgPalette() : cell.isFgPalette();
|
|
151
|
+
const v = isBg ? cell.getBgColor() : cell.getFgColor();
|
|
152
|
+
if (isRGB)
|
|
153
|
+
return `#${v.toString(16).padStart(6, '0')}`;
|
|
154
|
+
if (isPalette)
|
|
155
|
+
return PALETTE_256[v] ?? null;
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
/** Capture a section of the terminal buffer to a PNG buffer. */
|
|
159
|
+
export function captureToPng(terminal, opts) {
|
|
160
|
+
ensureFontRegistered();
|
|
161
|
+
const { cols, rows, startY } = opts;
|
|
162
|
+
const buffer = terminal.buffer.active;
|
|
163
|
+
const W = Math.ceil(PADDING * 2 + cols * CELL_W);
|
|
164
|
+
const H = PADDING * 2 + rows * CELL_H;
|
|
165
|
+
const canvas = createCanvas(W, H);
|
|
166
|
+
const ctx = canvas.getContext('2d');
|
|
167
|
+
ctx.fillStyle = BG;
|
|
168
|
+
ctx.fillRect(0, 0, W, H);
|
|
169
|
+
ctx.textBaseline = 'top';
|
|
170
|
+
ctx.font = fontSpec(false);
|
|
171
|
+
let currentBold = false;
|
|
172
|
+
for (let row = 0; row < rows; row++) {
|
|
173
|
+
const line = buffer.getLine(startY + row);
|
|
174
|
+
if (!line)
|
|
175
|
+
continue;
|
|
176
|
+
let col = 0;
|
|
177
|
+
while (col < cols) {
|
|
178
|
+
const cell = line.getCell(col);
|
|
179
|
+
if (!cell) {
|
|
180
|
+
col++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const w = cell.getWidth();
|
|
184
|
+
if (w === 0) {
|
|
185
|
+
col++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const ch = cell.getChars();
|
|
189
|
+
const x = PADDING + col * CELL_W;
|
|
190
|
+
const y = PADDING + row * CELL_H;
|
|
191
|
+
let fg = colorOf(cell, false) ?? FG;
|
|
192
|
+
let bg = colorOf(cell, true);
|
|
193
|
+
if (cell.isInverse()) {
|
|
194
|
+
const tmp = bg ?? BG;
|
|
195
|
+
bg = fg;
|
|
196
|
+
fg = tmp;
|
|
197
|
+
}
|
|
198
|
+
if (bg) {
|
|
199
|
+
ctx.fillStyle = bg;
|
|
200
|
+
ctx.fillRect(x, y, CELL_W * w, CELL_H);
|
|
201
|
+
}
|
|
202
|
+
if (ch && ch !== ' ') {
|
|
203
|
+
const bold = !!cell.isBold();
|
|
204
|
+
if (bold !== currentBold) {
|
|
205
|
+
currentBold = bold;
|
|
206
|
+
ctx.font = fontSpec(bold);
|
|
207
|
+
}
|
|
208
|
+
ctx.fillStyle = fg;
|
|
209
|
+
// Pictographs (emoji + dingbats) use bitmap metrics that don't align with
|
|
210
|
+
// text top-baseline — center them vertically in the cell instead.
|
|
211
|
+
if (isPictograph(ch)) {
|
|
212
|
+
ctx.textBaseline = 'middle';
|
|
213
|
+
ctx.fillText(ch, x, y + CELL_H / 2);
|
|
214
|
+
ctx.textBaseline = 'top';
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
ctx.fillText(ch, x, y + 2);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
col += w;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return canvas.toBuffer('image/png');
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=screenshot-renderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshot-renderer.js","sourceRoot":"","sources":["../../src/utils/screenshot-renderer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless terminal renderer: feeds PTY data into an xterm-headless instance
|
|
3
|
+
* and periodically snapshots the rendered screen for Feishu card updates.
|
|
4
|
+
*
|
|
5
|
+
* Filters out TUI chrome and preamble (logo, version, prompt echo, system
|
|
6
|
+
* instructions) so only the CLI's actual work output appears in the card.
|
|
7
|
+
*
|
|
8
|
+
* Supports two rendering styles:
|
|
9
|
+
* - CLI-style (Claude Code, Aiden): output appends to scrollback. Baseline
|
|
10
|
+
* tracking isolates current-turn content. Phase 1 (OUTPUT_MARKER_RE) gates
|
|
11
|
+
* content detection.
|
|
12
|
+
* - TUI-style (CoCo, Codex): cursor-positioned full-screen UI. Content can
|
|
13
|
+
* be overwritten when the TUI redraws (e.g. response → idle prompt).
|
|
14
|
+
* Viewport fallback + peak retention ensure response content is captured.
|
|
15
|
+
*
|
|
16
|
+
* Timer overlay avoidance: The PTY is intentionally wider than normal so that
|
|
17
|
+
* right-aligned TUI overlays (elapsed time, timeout counters) are rendered
|
|
18
|
+
* far to the right. Snapshots only read the first `contentCols` columns,
|
|
19
|
+
* cleanly excluding the overlay area — no fragile regex stripping needed.
|
|
20
|
+
*/
|
|
21
|
+
import xtermHeadless from '@xterm/headless';
|
|
22
|
+
declare const Terminal: typeof xtermHeadless.Terminal;
|
|
1
23
|
export declare class TerminalRenderer {
|
|
2
24
|
private terminal;
|
|
3
25
|
private lastHash;
|
|
@@ -48,7 +70,15 @@ export declare class TerminalRenderer {
|
|
|
48
70
|
* Used for TUI viewport fallback where output markers differ.
|
|
49
71
|
*/
|
|
50
72
|
private extractContent;
|
|
73
|
+
/**
|
|
74
|
+
* Raw viewport snapshot — no filtering, no phase gating, no chrome removal.
|
|
75
|
+
* Used by ScreenAnalyzer which needs the full screen including ❯ cursor lines.
|
|
76
|
+
*/
|
|
77
|
+
rawSnapshot(): string;
|
|
51
78
|
resize(cols: number, rows: number): void;
|
|
79
|
+
/** Expose the underlying xterm-headless instance for screenshot rendering. */
|
|
80
|
+
get xterm(): InstanceType<typeof Terminal>;
|
|
52
81
|
dispose(): void;
|
|
53
82
|
}
|
|
83
|
+
export {};
|
|
54
84
|
//# sourceMappingURL=terminal-renderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terminal-renderer.d.ts","sourceRoot":"","sources":["../../src/utils/terminal-renderer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"terminal-renderer.d.ts","sourceRoot":"","sources":["../../src/utils/terminal-renderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,aAAa,MAAM,iBAAiB,CAAC;AAC5C,QAAA,MAAQ,QAAQ,+BAAkB,CAAC;AA8DnC,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,QAAQ,CAAM;IACtB,yDAAyD;IACzD,OAAO,CAAC,aAAa,CAAK;IAC1B,6EAA6E;IAC7E,OAAO,CAAC,WAAW,CAAM;IACzB;;;2DAGuD;IACvD,OAAO,CAAC,gBAAgB,CAAS;IACjC;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB,CAAQ;gBAEpB,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAItC,mDAAmD;IACnD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAWzB;;;;OAIG;IACH,WAAW,IAAI,IAAI;IAOnB;;;;;;;;;OASG;IACH,QAAQ,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE;IA4DjD;;;;OAIG;IACH,OAAO,CAAC,cAAc;IA2CtB;;;OAGG;IACH,WAAW,IAAI,MAAM;IAsBrB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIxC,8EAA8E;IAC9E,IAAI,KAAK,IAAI,YAAY,CAAC,OAAO,QAAQ,CAAC,CAA0B;IAEpE,OAAO,IAAI,IAAI;CAGhB"}
|
|
@@ -221,9 +221,34 @@ export class TerminalRenderer {
|
|
|
221
221
|
}
|
|
222
222
|
return filtered.join('\n');
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Raw viewport snapshot — no filtering, no phase gating, no chrome removal.
|
|
226
|
+
* Used by ScreenAnalyzer which needs the full screen including ❯ cursor lines.
|
|
227
|
+
*/
|
|
228
|
+
rawSnapshot() {
|
|
229
|
+
const buffer = this.terminal.buffer.active;
|
|
230
|
+
const baseY = buffer.baseY;
|
|
231
|
+
const rows = this.terminal.rows;
|
|
232
|
+
const readCols = Math.min(SNAPSHOT_COLS, this.terminal.cols);
|
|
233
|
+
const endY = baseY + rows;
|
|
234
|
+
const lines = [];
|
|
235
|
+
for (let y = baseY; y < endY; y++) {
|
|
236
|
+
const line = buffer.getLine(y);
|
|
237
|
+
if (!line)
|
|
238
|
+
continue;
|
|
239
|
+
lines.push(cleanBoxDrawing(line.translateToString(true, 0, readCols)));
|
|
240
|
+
}
|
|
241
|
+
// Trim trailing blank lines only
|
|
242
|
+
while (lines.length > 0 && BLANK_RE.test(lines[lines.length - 1])) {
|
|
243
|
+
lines.pop();
|
|
244
|
+
}
|
|
245
|
+
return lines.join('\n');
|
|
246
|
+
}
|
|
224
247
|
resize(cols, rows) {
|
|
225
248
|
this.terminal.resize(cols, rows);
|
|
226
249
|
}
|
|
250
|
+
/** Expose the underlying xterm-headless instance for screenshot rendering. */
|
|
251
|
+
get xterm() { return this.terminal; }
|
|
227
252
|
dispose() {
|
|
228
253
|
this.terminal.dispose();
|
|
229
254
|
}
|