clideck 1.27.0 → 1.29.0
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.md +7 -0
- package/agent-presets.json +7 -9
- package/bin/claude-hook.js +31 -0
- package/bin/gemini-hook.js +31 -0
- package/bin/notify-helper.js +9 -2
- package/codex-config.js +77 -0
- package/config.js +2 -0
- package/handlers.js +164 -56
- package/package.json +1 -2
- package/plugin-loader.js +176 -50
- package/plugins/autopilot/clideck-plugin.json +5 -3
- package/plugins/autopilot/index.js +24 -30
- package/plugins/autopilot/package.json +7 -0
- package/plugins/trim-clip/clideck-plugin.json +1 -0
- package/plugins/voice-input/clideck-plugin.json +1 -0
- package/public/index.html +2 -4
- package/public/js/app.js +62 -5
- package/public/js/creator.js +14 -2
- package/public/js/settings.js +8 -6
- package/public/js/terminals.js +126 -26
- package/public/js/toast.js +2 -17
- package/public/js/utils.js +9 -0
- package/public/tailwind.css +1 -1
- package/server.js +87 -21
- package/sessions.js +41 -8
- package/telemetry-receiver.js +83 -80
- package/tools/merge-jsonl-roles.mjs +182 -0
- package/transcript-builder.js +53 -0
- package/transcript-parser.js +135 -0
- package/transcript.js +115 -181
package/transcript.js
CHANGED
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
const { appendFile, writeFileSync, mkdirSync, existsSync, readdirSync, readFileSync, unlinkSync } = require('fs');
|
|
2
2
|
const { join, basename } = require('path');
|
|
3
3
|
const { DATA_DIR } = require('./paths');
|
|
4
|
+
const builder = require('./transcript-builder');
|
|
5
|
+
const parser = require('./transcript-parser');
|
|
4
6
|
|
|
5
7
|
const DIR = join(DATA_DIR, 'transcripts');
|
|
6
8
|
const ANSI_RE = /\x1b[\[\]()#;?]*[0-9;]*[a-zA-Z@`~]|\x1b\].*?(?:\x07|\x1b\\)|\x1b.|\r|\x07/g;
|
|
7
9
|
const MAX_CACHE = 50 * 1024;
|
|
10
|
+
const LEGACY_SUFFIXES = ['-parsed.jsonl', '.screen'];
|
|
8
11
|
|
|
9
12
|
const inputBuf = {};
|
|
10
13
|
const outputBuf = {};
|
|
11
14
|
const cache = {};
|
|
12
15
|
const prefixes = {};
|
|
16
|
+
const entriesById = {};
|
|
13
17
|
const userTexts = {}; // sessionId → [text, ...] — user prompts for parser matching
|
|
18
|
+
const finalizePreset = {};
|
|
19
|
+
const lastAgentText = {};
|
|
14
20
|
let broadcast = null;
|
|
15
21
|
let notifyPlugin = null;
|
|
16
22
|
|
|
23
|
+
function tlog(id, msg) {
|
|
24
|
+
// console.log(`[transcript:${id.slice(0,8)}] ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clog(id, msg) {
|
|
28
|
+
if (finalizePreset[id] !== 'claude-code') return;
|
|
29
|
+
// console.log(`[claude:transcript:${id.slice(0,8)}] ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
function init(bc, validIds, pluginNotify) {
|
|
18
33
|
broadcast = bc;
|
|
19
34
|
notifyPlugin = pluginNotify || null;
|
|
20
35
|
if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true });
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const id = basename(file, '.screen');
|
|
24
|
-
if (!existsSync(fpath(id)) && (!validIds || !validIds.has(id))) { try { unlinkSync(join(DIR, file)); } catch {} }
|
|
36
|
+
for (const file of readdirSync(DIR).filter(f => LEGACY_SUFFIXES.some(s => f.endsWith(s)))) {
|
|
37
|
+
try { unlinkSync(join(DIR, file)); } catch {}
|
|
25
38
|
}
|
|
26
39
|
for (const file of readdirSync(DIR).filter(f => f.endsWith('.jsonl'))) {
|
|
27
40
|
const id = basename(file, '.jsonl');
|
|
28
41
|
if (validIds && !validIds.has(id)) { try { unlinkSync(join(DIR, file)); } catch {} continue; }
|
|
29
42
|
try {
|
|
30
|
-
const lines = readFileSync(join(DIR, file), 'utf8').trim().split('\n');
|
|
31
|
-
|
|
43
|
+
const lines = readFileSync(join(DIR, file), 'utf8').trim().split('\n').filter(Boolean);
|
|
44
|
+
entriesById[id] = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
45
|
+
cache[id] = entriesById[id].map(e => e.text).join('\n');
|
|
32
46
|
if (cache[id].length > MAX_CACHE) cache[id] = cache[id].slice(-MAX_CACHE);
|
|
33
47
|
} catch {}
|
|
34
48
|
}
|
|
@@ -36,13 +50,36 @@ function init(bc, validIds, pluginNotify) {
|
|
|
36
50
|
|
|
37
51
|
function fpath(id) { return join(DIR, `${id}.jsonl`); }
|
|
38
52
|
function setPrefix(id, prefix) { prefixes[id] = prefix; }
|
|
53
|
+
function setFinalizeOnIdle(id, presetId) {
|
|
54
|
+
if (!presetId) { delete finalizePreset[id]; return; }
|
|
55
|
+
finalizePreset[id] = presetId;
|
|
56
|
+
entriesById[id] = builder.compactEntries(entriesById[id], presetId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rewrite(id) {
|
|
60
|
+
const entries = entriesById[id] || [];
|
|
61
|
+
writeFileSync(fpath(id), entries.map(e => JSON.stringify(e)).join('\n') + (entries.length ? '\n' : ''));
|
|
62
|
+
cache[id] = entries.map(e => e.text).join('\n');
|
|
63
|
+
if (cache[id].length > MAX_CACHE) cache[id] = cache[id].slice(-MAX_CACHE);
|
|
64
|
+
tlog(id, `rewrite entries=${entries.length} last=${entries.length ? entries[entries.length - 1].role : 'none'}`);
|
|
65
|
+
clog(id, `rewrite entries=${entries.length} last=${entries.length ? entries[entries.length - 1].role : 'none'}`);
|
|
66
|
+
}
|
|
39
67
|
|
|
40
68
|
function store(id, role, text) {
|
|
41
69
|
const prefix = prefixes[id] || '';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
70
|
+
if (finalizePreset[id]) {
|
|
71
|
+
if (!entriesById[id]) entriesById[id] = [];
|
|
72
|
+
const entry = { ts: Date.now(), role, text, ...(prefix && { prefix }) };
|
|
73
|
+
tlog(id, `store role=${role} finalize=${finalizePreset[id]} raw=${JSON.stringify(String(text).slice(0, 160))}`);
|
|
74
|
+
builder.addEntry(entriesById[id], entry, finalizePreset[id]);
|
|
75
|
+
rewrite(id);
|
|
76
|
+
} else {
|
|
77
|
+
tlog(id, `store role=${role} append raw=${JSON.stringify(String(text).slice(0, 160))}`);
|
|
78
|
+
appendFile(fpath(id), JSON.stringify({ ts: Date.now(), role, text, ...(prefix && { prefix }) }) + '\n', () => {});
|
|
79
|
+
if (!cache[id]) cache[id] = '';
|
|
80
|
+
cache[id] += '\n' + text;
|
|
81
|
+
if (cache[id].length > MAX_CACHE) cache[id] = cache[id].slice(-MAX_CACHE);
|
|
82
|
+
}
|
|
46
83
|
if (broadcast) broadcast({ type: 'transcript.append', id, role, text });
|
|
47
84
|
if (notifyPlugin) notifyPlugin(id, role, text);
|
|
48
85
|
}
|
|
@@ -72,7 +109,12 @@ function trackInput(id, data) {
|
|
|
72
109
|
}
|
|
73
110
|
if (ch === '\r' || ch === '\n') {
|
|
74
111
|
const line = buf.text.trim();
|
|
75
|
-
if (line) {
|
|
112
|
+
if (line) {
|
|
113
|
+
delete lastAgentText[id];
|
|
114
|
+
store(id, 'user', line);
|
|
115
|
+
if (!userTexts[id]) userTexts[id] = [];
|
|
116
|
+
userTexts[id].push(line);
|
|
117
|
+
}
|
|
76
118
|
buf.text = '';
|
|
77
119
|
} else if (ch === '\x7f' || ch === '\x08') {
|
|
78
120
|
const chars = Array.from(buf.text);
|
|
@@ -84,8 +126,20 @@ function trackInput(id, data) {
|
|
|
84
126
|
}
|
|
85
127
|
}
|
|
86
128
|
|
|
129
|
+
function recordInjectedInput(id, text) {
|
|
130
|
+
delete lastAgentText[id];
|
|
131
|
+
for (const raw of String(text).split(/\r?\n/)) {
|
|
132
|
+
const line = raw.trim();
|
|
133
|
+
if (!line) continue;
|
|
134
|
+
store(id, 'user', line);
|
|
135
|
+
if (!userTexts[id]) userTexts[id] = [];
|
|
136
|
+
userTexts[id].push(line);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
87
140
|
// Server-side fallback: captures raw PTY output (noisy but always available)
|
|
88
141
|
function trackOutput(id, data) {
|
|
142
|
+
if (finalizePreset[id]) return;
|
|
89
143
|
if (!outputBuf[id]) outputBuf[id] = { text: '', timer: null };
|
|
90
144
|
const buf = outputBuf[id];
|
|
91
145
|
buf.text += data;
|
|
@@ -102,180 +156,46 @@ function flush(id) {
|
|
|
102
156
|
if (lines.length) store(id, 'agent', lines.join('\n'));
|
|
103
157
|
}
|
|
104
158
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|| /^[─━═\u2500-\u257f]+$/.test(t) // box-drawing horizontal lines
|
|
111
|
-
|| /^[▀▄█▌▐░▒▓╭╮╰╯│╔╗╚╝║]+$/.test(t) // block elements, box corners, vertical bars
|
|
112
|
-
|| (/[█▀▄▌▐░▒▓]/.test(t) && /^[█▀▄▌▐░▒▓\s]+$/.test(t)) // ASCII art (blocks + whitespace, e.g. logos)
|
|
113
|
-
|| /^[❯>$%#]\s*$/.test(t) // bare prompt markers
|
|
114
|
-
|| /^(esc to interrupt|\? for shortcuts)$/i.test(t); // Claude Code chrome
|
|
115
|
-
const filtered = lines.filter(l => !isChrome(l.trim()));
|
|
116
|
-
while (filtered.length && !filtered[filtered.length - 1].trim()) filtered.pop();
|
|
117
|
-
const screenPath = join(DIR, `${id}.screen`);
|
|
118
|
-
if (filtered.length) writeFileSync(screenPath, filtered.join('\n'));
|
|
119
|
-
else try { unlinkSync(screenPath); } catch {}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Read the clean screen snapshot for a session (if available).
|
|
123
|
-
function getScreen(id) {
|
|
124
|
-
const file = join(DIR, `${id}.screen`);
|
|
125
|
-
if (!existsSync(file)) return null;
|
|
126
|
-
try { return readFileSync(file, 'utf8'); } catch { return null; }
|
|
159
|
+
function parseTurnsFromLines(id, agent, lines, opts) {
|
|
160
|
+
const turns = parser.parseTurns(agent, lines, getUsers(id));
|
|
161
|
+
if (!opts?.raw && turns?.length && turns[turns.length - 1].role === 'user') turns.pop();
|
|
162
|
+
tlog(id, `parse agent=${agent} lines=${lines?.length || 0} turns=${turns?.map(t => t.role).join(',') || 'none'}`);
|
|
163
|
+
return turns?.length >= 2 ? turns : null;
|
|
127
164
|
}
|
|
128
165
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
145
|
-
current = { role: 'user', text: userM[1] };
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
if (!current) continue;
|
|
149
|
-
let cont = line;
|
|
150
|
-
if (cont.startsWith('│ ')) cont = cont.slice(2);
|
|
151
|
-
else if (cont.startsWith(' ')) cont = cont.slice(2);
|
|
152
|
-
current.text += '\n' + cont;
|
|
153
|
-
}
|
|
154
|
-
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
155
|
-
return turns.length >= 2 ? turns : null;
|
|
156
|
-
},
|
|
157
|
-
'codex': (lines, id) => {
|
|
158
|
-
const known = userTexts[id]?.length ? new Set(userTexts[id]) : null;
|
|
159
|
-
const turns = [];
|
|
160
|
-
let current = null;
|
|
161
|
-
for (const line of lines) {
|
|
162
|
-
const agent = line.match(/^(?:│\s*)?•\s(.*)$/);
|
|
163
|
-
if (agent) {
|
|
164
|
-
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
165
|
-
current = { role: 'agent', text: agent[1] };
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
const userM = line.match(/^(?:│\s*)?›\s(.*)$/);
|
|
169
|
-
if (userM && (known ? known.has(userM[1].trim()) : true)) {
|
|
170
|
-
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
171
|
-
current = { role: 'user', text: userM[1] };
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (!current) continue;
|
|
175
|
-
let cont = line;
|
|
176
|
-
if (cont.startsWith('│ ')) cont = cont.slice(2);
|
|
177
|
-
else if (cont.startsWith(' ')) cont = cont.slice(2);
|
|
178
|
-
current.text += '\n' + cont;
|
|
179
|
-
}
|
|
180
|
-
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
181
|
-
return turns.length >= 2 ? turns : null;
|
|
182
|
-
},
|
|
183
|
-
'gemini-cli': (lines, id) => {
|
|
184
|
-
const known = userTexts[id]?.length ? new Set(userTexts[id]) : null;
|
|
185
|
-
const geminiChrome = t => {
|
|
186
|
-
const s = t.trim();
|
|
187
|
-
return /^shift\+tab to accept/i.test(s)
|
|
188
|
-
|| /^(Type your message|@path\/to\/)/i.test(s)
|
|
189
|
-
|| /^(\/\w+ |no sandbox|\/model )/i.test(s)
|
|
190
|
-
|| /^[~\/\\].*\(main[*]?\)\s*$/i.test(s)
|
|
191
|
-
|| /^(Logged in with|Plan:|Tips for getting started)/i.test(s)
|
|
192
|
-
|| /^\d+\.\s+(Ask questions|Be specific|Create GEMINI)/i.test(s)
|
|
193
|
-
|| /^ℹ\s/.test(s);
|
|
194
|
-
};
|
|
195
|
-
const turns = [];
|
|
196
|
-
let current = null;
|
|
197
|
-
for (const line of lines) {
|
|
198
|
-
if (geminiChrome(line)) continue;
|
|
199
|
-
const isAgent = line.startsWith('✦ ');
|
|
200
|
-
const userM = line.startsWith(' > ') ? line.slice(3) : null;
|
|
201
|
-
const isUser = userM && (known ? known.has(userM.trim()) : true);
|
|
202
|
-
if (isUser || isAgent) {
|
|
203
|
-
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
204
|
-
current = { role: isUser ? 'user' : 'agent', text: isUser ? userM : line.slice(2) };
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (!current) continue;
|
|
208
|
-
current.text += '\n' + line;
|
|
209
|
-
}
|
|
210
|
-
if (current) { current.text = current.text.replace(/\n+$/, ''); turns.push(current); }
|
|
211
|
-
return turns.length >= 2 ? turns : null;
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Fallback anchor parser for agents without a dedicated parser.
|
|
216
|
-
// Uses JSONL user inputs to find prompt lines in the screen.
|
|
217
|
-
function anchorParse(id, lines) {
|
|
218
|
-
const file = fpath(id);
|
|
219
|
-
if (!existsSync(file)) return null;
|
|
220
|
-
const users = [];
|
|
221
|
-
try {
|
|
222
|
-
for (const l of readFileSync(file, 'utf8').trim().split('\n')) {
|
|
223
|
-
try { const e = JSON.parse(l); if (e.role === 'user') users.push(e.text); } catch {}
|
|
224
|
-
}
|
|
225
|
-
} catch { return null; }
|
|
226
|
-
if (!users.length) return null;
|
|
227
|
-
|
|
228
|
-
const isPrompt = (line, text) => { const t = line.trim(); return t.endsWith(text) && t.length - text.length <= 6; };
|
|
229
|
-
|
|
230
|
-
const lastUser = users[users.length - 1];
|
|
231
|
-
let lastIdx = -1;
|
|
232
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
233
|
-
if (isPrompt(lines[i], lastUser)) { lastIdx = i; break; }
|
|
234
|
-
}
|
|
235
|
-
if (lastIdx < 0) return null;
|
|
236
|
-
|
|
237
|
-
const anchors = [{ idx: lastIdx, text: lastUser }];
|
|
238
|
-
for (let u = users.length - 2; u >= 0 && anchors.length < 3; u--) {
|
|
239
|
-
for (let i = anchors[anchors.length - 1].idx - 1; i >= 0; i--) {
|
|
240
|
-
if (isPrompt(lines[i], users[u])) { anchors.push({ idx: i, text: users[u] }); break; }
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
anchors.reverse();
|
|
244
|
-
|
|
245
|
-
const turns = [];
|
|
246
|
-
for (let p = 0; p < anchors.length; p++) {
|
|
247
|
-
turns.push({ role: 'user', text: anchors[p].text });
|
|
248
|
-
const start = anchors[p].idx + 1;
|
|
249
|
-
const end = p + 1 < anchors.length ? anchors[p + 1].idx : lines.length;
|
|
250
|
-
const agentLines = lines.slice(start, end).filter(l => l.trim());
|
|
251
|
-
if (agentLines.length) turns.push({ role: 'agent', text: agentLines.join('\n') });
|
|
252
|
-
}
|
|
253
|
-
if (turns.length < 2 || turns[turns.length - 1].role !== 'agent') return null;
|
|
254
|
-
return turns;
|
|
166
|
+
function captureAgentTurn(id, agent, lines) {
|
|
167
|
+
if (!finalizePreset[id]) return;
|
|
168
|
+
const turns = parseTurnsFromLines(id, agent, lines);
|
|
169
|
+
// Finalized capture should not depend on the viewport still containing the
|
|
170
|
+
// matching user prompt line; if the full turn structure is gone, fall back to
|
|
171
|
+
// the last visible agent block alone.
|
|
172
|
+
const last = turns?.length ? turns[turns.length - 1] : parser.parseLastAgentOnly(agent, lines);
|
|
173
|
+
if (last) tlog(id, `capture lastRole=${last.role} raw=${JSON.stringify(String(last.text).slice(0, 200))}`);
|
|
174
|
+
if (last) clog(id, `capture lastRole=${last.role} raw=${JSON.stringify(String(last.text).slice(0, 200))}`);
|
|
175
|
+
const text = last?.role === 'agent' ? builder.cleanAgentText(finalizePreset[id], last.text) : '';
|
|
176
|
+
tlog(id, `capture cleaned=${JSON.stringify(String(text).slice(0, 200))}`);
|
|
177
|
+
if (!text) { tlog(id, 'capture skip empty-clean'); clog(id, 'capture skip empty-clean'); return; }
|
|
178
|
+
if (text === lastAgentText[id]) { tlog(id, 'capture skip duplicate-clean'); clog(id, 'capture skip duplicate-clean'); return; }
|
|
179
|
+
lastAgentText[id] = text;
|
|
180
|
+
store(id, 'agent', text);
|
|
255
181
|
}
|
|
256
182
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
return
|
|
183
|
+
function captureFinalAgentText(id, presetId, text) {
|
|
184
|
+
if (!finalizePreset[id]) return;
|
|
185
|
+
const clean = builder.cleanAgentText(presetId || finalizePreset[id], text);
|
|
186
|
+
tlog(id, `captureFinal raw=${JSON.stringify(String(text).slice(0, 200))}`);
|
|
187
|
+
tlog(id, `captureFinal cleaned=${JSON.stringify(String(clean).slice(0, 200))}`);
|
|
188
|
+
clog(id, `captureFinal raw=${JSON.stringify(String(text).slice(0, 200))}`);
|
|
189
|
+
clog(id, `captureFinal cleaned=${JSON.stringify(String(clean).slice(0, 200))}`);
|
|
190
|
+
if (!clean) { tlog(id, 'captureFinal skip empty-clean'); clog(id, 'captureFinal skip empty-clean'); return; }
|
|
191
|
+
if (clean === lastAgentText[id]) { tlog(id, 'captureFinal skip duplicate-clean'); clog(id, 'captureFinal skip duplicate-clean'); return; }
|
|
192
|
+
lastAgentText[id] = clean;
|
|
193
|
+
store(id, 'agent', clean);
|
|
266
194
|
}
|
|
267
195
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const screen = getScreen(id);
|
|
272
|
-
if (!screen) return null;
|
|
273
|
-
const lines = screen.split('\n');
|
|
274
|
-
const parser = agentParsers[agent];
|
|
275
|
-
const turns = collapseAgentTurns(parser ? parser(lines, id) : anchorParse(id, lines));
|
|
276
|
-
// Drop trailing user turn — it's the empty prompt or unanswered input
|
|
277
|
-
if (!opts?.raw && turns?.length && turns[turns.length - 1].role === 'user') turns.pop();
|
|
278
|
-
return turns?.length >= 2 ? turns : null;
|
|
196
|
+
function getUsers(id) {
|
|
197
|
+
if (userTexts[id]?.length) return userTexts[id];
|
|
198
|
+
return (entriesById[id] || []).filter(e => e.role === 'user').map(e => e.text);
|
|
279
199
|
}
|
|
280
200
|
|
|
281
201
|
function getLastTurns(id, n) {
|
|
@@ -301,6 +221,18 @@ function getLastTurns(id, n) {
|
|
|
301
221
|
|
|
302
222
|
function getCache() { return { ...cache }; }
|
|
303
223
|
|
|
224
|
+
function getReplayText(id, presetId) {
|
|
225
|
+
const entries = builder.compactEntries(entriesById[id], presetId);
|
|
226
|
+
if (!entries?.length) return '';
|
|
227
|
+
const marks = {
|
|
228
|
+
'claude-code': { user: '❯', agent: '⏺' },
|
|
229
|
+
codex: { user: '›', agent: '•' },
|
|
230
|
+
'gemini-cli': { user: '>', agent: '✦' },
|
|
231
|
+
opencode: { user: '›', agent: '•' },
|
|
232
|
+
}[presetId] || { user: '›', agent: '•' };
|
|
233
|
+
return entries.map(e => `${e.role === 'user' ? marks.user : marks.agent} ${e.text}`).join('\n\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
304
236
|
function clear(id) {
|
|
305
237
|
flush(id);
|
|
306
238
|
delete inputBuf[id];
|
|
@@ -309,9 +241,11 @@ function clear(id) {
|
|
|
309
241
|
delete outputBuf[id];
|
|
310
242
|
}
|
|
311
243
|
delete cache[id];
|
|
244
|
+
delete entriesById[id];
|
|
312
245
|
delete prefixes[id];
|
|
313
246
|
delete userTexts[id];
|
|
314
|
-
|
|
247
|
+
delete lastAgentText[id];
|
|
248
|
+
delete finalizePreset[id];
|
|
315
249
|
}
|
|
316
250
|
|
|
317
251
|
// Detect interactive menus from raw screen lines. Returns [{value, label, selected}] or null.
|
|
@@ -340,4 +274,4 @@ function detectMenu(lines, presetId) {
|
|
|
340
274
|
return choices.length ? choices : null;
|
|
341
275
|
}
|
|
342
276
|
|
|
343
|
-
module.exports = { init, trackInput, trackOutput,
|
|
277
|
+
module.exports = { init, trackInput, recordInjectedInput, trackOutput, captureAgentTurn, captureFinalAgentText, parseTurnsFromLines, getLastTurns, getCache, getReplayText, clear, setPrefix, setFinalizeOnIdle, detectMenu };
|