@adhdev/daemon-core 0.6.56 → 0.6.58
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/dist/index.d.ts +23 -0
- package/dist/index.js +423 -95
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/providers/_builtin/cli/aider-cli/scripts/1.0/parse_output.js +51 -3
- package/providers/_builtin/cli/claude-cli/provider.json +18 -6
- package/providers/_builtin/cli/claude-cli/scripts/1.0/detect_status.js +68 -16
- package/providers/_builtin/cli/claude-cli/scripts/1.0/parse_approval.js +81 -22
- package/providers/_builtin/cli/claude-cli/scripts/1.0/parse_output.js +347 -94
- package/providers/_builtin/cli/codex-cli/provider.json +2 -0
- package/providers/_builtin/cli/codex-cli/scripts/1.0/detect_status.js +44 -10
- package/providers/_builtin/cli/codex-cli/scripts/1.0/parse_approval.js +83 -7
- package/providers/_builtin/cli/codex-cli/scripts/1.0/parse_output.js +501 -47
- package/providers/_builtin/cli/cursor-cli/scripts/1.0/parse_output.js +1 -1
- package/providers/_builtin/cli/github-copilot-cli/scripts/1.0/parse_output.js +1 -1
- package/providers/_builtin/cli/goose-cli/scripts/1.0/parse_output.js +1 -1
- package/providers/_builtin/cli/opencode-cli/scripts/1.0/parse_output.js +1 -1
- package/providers/_builtin/ide/vscode/provider.json +5 -1
- package/providers/_builtin/ide/vscode/scripts/1.0/focus_editor.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/list_models.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/list_sessions.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/new_session.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/open_panel.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/read_chat.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/resolve_action.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/scripts.js +25 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/send_message.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/set_model.js +1 -0
- package/providers/_builtin/ide/vscode/scripts/1.0/switch_session.js +1 -0
- package/providers/_builtin/registry.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +410 -65
- package/src/commands/chat-commands.ts +7 -1
- package/src/config/chat-history.ts +53 -1
- package/src/daemon/dev-server.ts +7 -9
- package/src/providers/cli-provider-instance.ts +10 -23
- package/src/providers/provider-instance.ts +1 -0
- package/src/providers/version-archive.ts +4 -1
|
@@ -1,156 +1,409 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Claude Code — parse_output
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* buffer: string, // Full ANSI-stripped accumulated PTY output
|
|
9
|
-
* rawBuffer: string, // Raw PTY output (with ANSI)
|
|
10
|
-
* recentBuffer: string, // Recent 1000 chars (ANSI-stripped)
|
|
11
|
-
* messages: Array, // Previously parsed messages (for delta)
|
|
12
|
-
* partialResponse: string, // Current partial response being generated
|
|
13
|
-
* }
|
|
14
|
-
*
|
|
15
|
-
* Output: ReadChatResult {
|
|
16
|
-
* messages: [{ id, role, content, index, kind?, meta? }],
|
|
17
|
-
* status: AgentStatus,
|
|
18
|
-
* activeModal?: ModalInfo | null,
|
|
19
|
-
* title?: string,
|
|
20
|
-
* }
|
|
4
|
+
* Reference implementation for CLI PTY parsing:
|
|
5
|
+
* - prefer the visible screen snapshot (`screenText`)
|
|
6
|
+
* - keep transcript state incrementally via `messages`
|
|
7
|
+
* - fall back to noisy rolling buffers when older runtimes do not provide screenText
|
|
21
8
|
*/
|
|
22
9
|
|
|
23
10
|
'use strict';
|
|
24
11
|
|
|
25
|
-
const detectStatus
|
|
12
|
+
const detectStatus = require('./detect_status.js');
|
|
26
13
|
const parseApproval = require('./parse_approval.js');
|
|
27
14
|
|
|
15
|
+
function splitLines(text) {
|
|
16
|
+
return String(text || '')
|
|
17
|
+
.replace(/\u0007/g, '')
|
|
18
|
+
.split(/\r\n|\n|\r/g)
|
|
19
|
+
.map(line => line.replace(/\s+$/, ''));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sanitizeLine(line) {
|
|
23
|
+
return String(line || '')
|
|
24
|
+
.replace(/\u0007/g, '')
|
|
25
|
+
.replace(/^\d+;/, '')
|
|
26
|
+
.replace(/\s+$/, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
28
29
|
function normalizeText(text) {
|
|
29
30
|
return String(text || '')
|
|
30
31
|
.replace(/\s+/g, ' ')
|
|
31
32
|
.trim();
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function
|
|
35
|
-
|
|
35
|
+
function looksLikeSamePrompt(left, right) {
|
|
36
|
+
const a = normalizeText(left);
|
|
37
|
+
const b = normalizeText(right);
|
|
38
|
+
if (!a || !b) return false;
|
|
39
|
+
if (a === b) return true;
|
|
40
|
+
const minLength = Math.min(a.length, b.length);
|
|
41
|
+
if (minLength < 24) return false;
|
|
42
|
+
return a.startsWith(b) || b.startsWith(a) || a.includes(b) || b.includes(a);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isSpinnerOnlyText(text) {
|
|
46
|
+
const lines = splitLines(text)
|
|
47
|
+
.map(line => stripAssistantPrefix(sanitizeLine(line).trim()))
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
if (lines.length === 0) return true;
|
|
50
|
+
return lines.every(line => isStatusLine(line) || /^(?:[A-Z][a-z]+ing\u2026?|[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+)$/.test(line));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function looksLikePromptEchoText(candidate, promptText, previousMessages) {
|
|
54
|
+
const normalizedCandidate = normalizeText(candidate);
|
|
55
|
+
if (!normalizedCandidate) return false;
|
|
56
|
+
if (promptText && looksLikeSamePrompt(normalizedCandidate, promptText)) return true;
|
|
57
|
+
|
|
58
|
+
const lastUser = [...(Array.isArray(previousMessages) ? previousMessages : [])]
|
|
59
|
+
.reverse()
|
|
60
|
+
.find(message => message?.role === 'user' && typeof message.content === 'string');
|
|
61
|
+
return !!lastUser && looksLikeSamePrompt(normalizedCandidate, lastUser.content);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parsePromptLine(line) {
|
|
65
|
+
const trimmed = sanitizeLine(line).trim();
|
|
66
|
+
const match = trimmed.match(/^[❯›>]\s*(.*)$/);
|
|
67
|
+
if (!match) return null;
|
|
68
|
+
const body = match[1].trim();
|
|
69
|
+
if (/^\d+[.)]\s+/.test(body)) return null;
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isBoxLine(trimmed) {
|
|
74
|
+
return /^[─═╭╮╰╯│┌┐└┘├┤┬┴┼]+$/.test(trimmed);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isFooterLine(trimmed) {
|
|
78
|
+
return /^➜\s+\S+/.test(trimmed)
|
|
79
|
+
|| /^Update available!/i.test(trimmed)
|
|
80
|
+
|| /Claude Code v\d/i.test(trimmed)
|
|
81
|
+
|| /^(Sonnet|Opus|Haiku)\b/i.test(trimmed)
|
|
82
|
+
|| /^[◐◑◒◓◴◵◶◷◸◹◺◿].*\/effort/i.test(trimmed)
|
|
83
|
+
|| /^⏵⏵\s+accept edits on/i.test(trimmed)
|
|
84
|
+
|| /^ctrl\+g to edit in VS Code/i.test(trimmed)
|
|
85
|
+
|| /^✳\s*Claude Code/i.test(trimmed)
|
|
86
|
+
|| /^[▗▖▘▝\s]+~\//.test(trimmed);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isStatusLine(trimmed) {
|
|
90
|
+
if (!trimmed) return true;
|
|
91
|
+
if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+$/.test(trimmed)) return true;
|
|
92
|
+
if (/^[⠂⠐⠒⠓⠦⠴⠶⠷⠿]\s+/.test(trimmed)) return true;
|
|
93
|
+
if (/esc to (cancel|interrupt|stop)/i.test(trimmed)) return true;
|
|
94
|
+
if (/(?:Finagling|Scurrying|Bloviating|Whatchamacallit(?:ing)?|Hatching|Tinkering|Thinking|Processing|Working|Analyzing|Planning|Drafting|Synthesizing|Inspecting|Reading|Searching)\u2026?$/i.test(trimmed)) return true;
|
|
95
|
+
if (/Allow\s*once|Always\s*allow|\(y\/n\)|\[Y\/n\]/i.test(trimmed)) return true;
|
|
96
|
+
return false;
|
|
36
97
|
}
|
|
37
98
|
|
|
38
99
|
function isNoiseLine(line) {
|
|
39
|
-
const trimmed = line.trim();
|
|
100
|
+
const trimmed = sanitizeLine(line).trim();
|
|
40
101
|
if (!trimmed) return true;
|
|
41
|
-
if (
|
|
102
|
+
if (/^…\s+\+\d+\s+lines\b/i.test(trimmed)) return true;
|
|
103
|
+
if (isBoxLine(trimmed)) return true;
|
|
104
|
+
if (isFooterLine(trimmed)) return true;
|
|
105
|
+
if (isStatusLine(trimmed)) return true;
|
|
42
106
|
if (/^Type your message/i.test(trimmed)) return true;
|
|
43
107
|
if (/^for\s*shortcuts/i.test(trimmed)) return true;
|
|
44
108
|
if (/^\? for help/i.test(trimmed)) return true;
|
|
45
109
|
if (/^Press enter/i.test(trimmed)) return true;
|
|
46
|
-
if (/^[\u2800-\u28ff]+$/.test(trimmed)) return true;
|
|
47
|
-
if (/^esc to (cancel|interrupt|stop)/i.test(trimmed)) return true;
|
|
48
|
-
if (/^Allow\s*once/i.test(trimmed)) return true;
|
|
49
|
-
if (/^Always\s*allow/i.test(trimmed)) return true;
|
|
50
|
-
if (/^\[(Y\/n|y\/n)\]$/i.test(trimmed)) return true;
|
|
51
110
|
return false;
|
|
52
111
|
}
|
|
53
112
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
113
|
+
function stripAssistantPrefix(trimmed) {
|
|
114
|
+
return trimmed
|
|
115
|
+
.replace(/^[⏺•]\s+/, '')
|
|
116
|
+
.replace(/^⎿\s+/, '')
|
|
117
|
+
.replace(/^[✻✶✳✢✽]\s+/, '')
|
|
118
|
+
.trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function collectMeaningfulLines(lines) {
|
|
122
|
+
const out = [];
|
|
123
|
+
let captureDetails = false;
|
|
124
|
+
for (let index = 0; index < lines.length; index++) {
|
|
125
|
+
const rawLine = lines[index];
|
|
126
|
+
const promptText = parsePromptLine(rawLine);
|
|
127
|
+
if (promptText !== null) continue;
|
|
128
|
+
|
|
129
|
+
const sanitized = sanitizeLine(rawLine);
|
|
130
|
+
const trimmed = sanitized.trim();
|
|
131
|
+
if (isNoiseLine(trimmed)) continue;
|
|
132
|
+
const nextTrimmed = sanitizeLine(lines[index + 1] || '').trim();
|
|
133
|
+
if (/\u2026\)$/.test(trimmed) && /^⎿\s+/.test(nextTrimmed)) continue;
|
|
134
|
+
|
|
135
|
+
const cleaned = stripAssistantPrefix(trimmed);
|
|
136
|
+
if (!cleaned) continue;
|
|
137
|
+
if (/^⏺\s+/.test(trimmed)) {
|
|
138
|
+
captureDetails = /^(?:Bash|Read|Task)\(/.test(cleaned)
|
|
139
|
+
|| /^(?:Exact output|Output|Result):/i.test(cleaned);
|
|
140
|
+
if (out[out.length - 1] !== cleaned) out.push(cleaned);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (/^⎿\s+/.test(trimmed)) {
|
|
145
|
+
if (!captureDetails || /^…\s+\+\d+\s+lines\b/i.test(cleaned)) continue;
|
|
146
|
+
if (out[out.length - 1] !== cleaned) out.push(cleaned);
|
|
147
|
+
continue;
|
|
60
148
|
}
|
|
149
|
+
|
|
150
|
+
if (!captureDetails && /^\d+\s+/.test(trimmed)) continue;
|
|
151
|
+
if (cleaned.length === 1 && /^[A-Za-z]$/.test(cleaned)) continue;
|
|
152
|
+
if (out[out.length - 1] !== cleaned) out.push(cleaned);
|
|
61
153
|
}
|
|
62
|
-
return
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function looksLikeStructuredDataLine(text) {
|
|
158
|
+
const line = stripAssistantPrefix(sanitizeLine(text).trim());
|
|
159
|
+
if (!line) return false;
|
|
160
|
+
return /^[{\[]/.test(line)
|
|
161
|
+
|| /^[A-Z0-9_]+=/.test(line)
|
|
162
|
+
|| /^\/[A-Za-z0-9._/-]+$/.test(line)
|
|
163
|
+
|| /^\d+$/.test(line);
|
|
63
164
|
}
|
|
64
165
|
|
|
65
|
-
function
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
166
|
+
function extractDenseOutputBlock(text) {
|
|
167
|
+
const lines = splitLines(text);
|
|
168
|
+
const blocks = [];
|
|
169
|
+
let current = [];
|
|
170
|
+
|
|
171
|
+
for (const rawLine of lines) {
|
|
172
|
+
const promptText = parsePromptLine(rawLine);
|
|
173
|
+
const sanitized = sanitizeLine(rawLine).trim();
|
|
174
|
+
if (promptText !== null || isNoiseLine(sanitized)) {
|
|
175
|
+
if (current.length > 0) {
|
|
176
|
+
blocks.push(current);
|
|
177
|
+
current = [];
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const cleaned = stripAssistantPrefix(sanitized);
|
|
183
|
+
if (!cleaned) {
|
|
184
|
+
if (current.length > 0) {
|
|
185
|
+
blocks.push(current);
|
|
186
|
+
current = [];
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
current.push(cleaned);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (current.length > 0) blocks.push(current);
|
|
194
|
+
|
|
195
|
+
const scored = blocks
|
|
196
|
+
.map(block => ({
|
|
197
|
+
block,
|
|
198
|
+
structured: block.filter(looksLikeStructuredDataLine).length,
|
|
199
|
+
}))
|
|
200
|
+
.filter(entry => entry.block.length >= 5 && entry.structured >= Math.max(3, Math.floor(entry.block.length * 0.6)));
|
|
201
|
+
|
|
202
|
+
const mergeBlocks = (existing, next) => {
|
|
203
|
+
if (existing.length === 0) return [...next];
|
|
204
|
+
const maxOverlap = Math.min(existing.length, next.length);
|
|
205
|
+
for (let overlap = maxOverlap; overlap >= 1; overlap--) {
|
|
206
|
+
let matches = true;
|
|
207
|
+
for (let i = 0; i < overlap; i++) {
|
|
208
|
+
if (existing[existing.length - overlap + i] !== next[i]) {
|
|
209
|
+
matches = false;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (matches) {
|
|
214
|
+
return existing.concat(next.slice(overlap));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return existing.concat(next);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const merged = scored.reduce((acc, entry) => mergeBlocks(acc, entry.block), []);
|
|
221
|
+
return merged.join('\n').trim();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function collectAssistantBlocks(lines) {
|
|
225
|
+
const blocks = [];
|
|
226
|
+
let current = null;
|
|
227
|
+
for (const rawLine of lines) {
|
|
228
|
+
const sanitized = sanitizeLine(rawLine);
|
|
229
|
+
const trimmed = sanitized.trim();
|
|
230
|
+
if (!trimmed || isNoiseLine(trimmed)) continue;
|
|
231
|
+
|
|
232
|
+
if (/^⏺\s+/.test(trimmed)) {
|
|
233
|
+
const title = stripAssistantPrefix(trimmed);
|
|
234
|
+
if (!title) {
|
|
235
|
+
current = null;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
current = {
|
|
239
|
+
title,
|
|
240
|
+
lines: [title],
|
|
241
|
+
isTool: /^(?:Bash|Read|Write|Edit|MultiEdit|Task|Glob|Grep|LS|NotebookEdit)\(/.test(title),
|
|
242
|
+
};
|
|
243
|
+
blocks.push(current);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!current) continue;
|
|
248
|
+
const cleaned = stripAssistantPrefix(trimmed);
|
|
249
|
+
if (!cleaned || /^…\s+\+\d+\s+lines\b/i.test(cleaned)) continue;
|
|
250
|
+
current.lines.push(cleaned);
|
|
251
|
+
}
|
|
252
|
+
return blocks;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function extractVisibleTurn(text, previousMessages) {
|
|
256
|
+
const lines = splitLines(text);
|
|
257
|
+
const emptyPromptIndex = (() => {
|
|
258
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
259
|
+
if (parsePromptLine(lines[i]) === '') return i;
|
|
260
|
+
}
|
|
261
|
+
return -1;
|
|
262
|
+
})();
|
|
263
|
+
|
|
264
|
+
const userPrompt = (() => {
|
|
265
|
+
const upperBound = emptyPromptIndex >= 0 ? emptyPromptIndex - 1 : lines.length - 1;
|
|
266
|
+
for (let i = upperBound; i >= 0; i--) {
|
|
267
|
+
const parsed = parsePromptLine(lines[i]);
|
|
268
|
+
if (parsed) return { index: i, text: parsed };
|
|
269
|
+
}
|
|
270
|
+
return { index: -1, text: '' };
|
|
271
|
+
})();
|
|
272
|
+
|
|
273
|
+
const promptLines = [];
|
|
274
|
+
let assistantStart = userPrompt.index >= 0 ? userPrompt.index + 1 : 0;
|
|
275
|
+
if (userPrompt.index >= 0) {
|
|
276
|
+
promptLines.push(userPrompt.text);
|
|
277
|
+
for (let i = userPrompt.index + 1; i < lines.length; i++) {
|
|
278
|
+
const trimmed = sanitizeLine(lines[i]).trim();
|
|
279
|
+
if (!trimmed) {
|
|
280
|
+
assistantStart = i + 1;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if (/^[⏺•]/.test(trimmed) || isBoxLine(trimmed) || isFooterLine(trimmed) || isStatusLine(trimmed) || /^([❯›>]\s*)?\d+[.)]\s+/.test(trimmed)) {
|
|
284
|
+
assistantStart = i;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
promptLines.push(trimmed);
|
|
288
|
+
assistantStart = i + 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const end = emptyPromptIndex >= 0 ? emptyPromptIndex : lines.length;
|
|
293
|
+
const assistantWindow = lines.slice(assistantStart, end);
|
|
294
|
+
const blocks = collectAssistantBlocks(assistantWindow);
|
|
295
|
+
const lastNarrativeBlock = [...blocks].reverse().find(block => !block.isTool);
|
|
296
|
+
let assistantLines = lastNarrativeBlock
|
|
297
|
+
? lastNarrativeBlock.lines
|
|
298
|
+
: collectMeaningfulLines(assistantWindow);
|
|
299
|
+
|
|
300
|
+
if (assistantLines.length === 0 && Array.isArray(previousMessages) && previousMessages.length > 0) {
|
|
301
|
+
assistantLines = collectMeaningfulLines(lines);
|
|
302
|
+
}
|
|
303
|
+
|
|
71
304
|
return {
|
|
72
|
-
promptText:
|
|
73
|
-
assistantText:
|
|
305
|
+
promptText: promptLines.join(' ').trim(),
|
|
306
|
+
assistantText: assistantLines.join('\n').trim(),
|
|
74
307
|
};
|
|
75
308
|
}
|
|
76
309
|
|
|
77
|
-
function
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
return slice.map((message, index) => ({
|
|
81
|
-
id: `msg_${index}`,
|
|
82
|
-
role: message.role,
|
|
83
|
-
content: typeof message.content === 'string' && message.content.length > 6000
|
|
84
|
-
? message.content.slice(0, 6000) + '\n[... truncated]'
|
|
85
|
-
: message.content,
|
|
86
|
-
index,
|
|
87
|
-
kind: 'standard',
|
|
88
|
-
...(status === 'generating' && index === slice.length - 1 && message.role === 'assistant'
|
|
89
|
-
? { meta: { streaming: true } }
|
|
90
|
-
: {}),
|
|
91
|
-
}));
|
|
310
|
+
function extractPartialAssistant(text) {
|
|
311
|
+
const meaningful = collectMeaningfulLines(splitLines(text));
|
|
312
|
+
return meaningful.join('\n').trim();
|
|
92
313
|
}
|
|
93
314
|
|
|
94
|
-
function buildMessages(previousMessages, promptText, assistantText,
|
|
315
|
+
function buildMessages(previousMessages, promptText, assistantText, partialText) {
|
|
95
316
|
const base = Array.isArray(previousMessages)
|
|
96
|
-
? previousMessages
|
|
97
|
-
role
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
317
|
+
? previousMessages
|
|
318
|
+
.filter(message => message && (message.role === 'user' || message.role === 'assistant'))
|
|
319
|
+
.map(message => ({
|
|
320
|
+
role: message.role,
|
|
321
|
+
content: typeof message.content === 'string' ? message.content : String(message.content || ''),
|
|
322
|
+
timestamp: message.timestamp,
|
|
323
|
+
}))
|
|
101
324
|
: [];
|
|
102
325
|
|
|
103
|
-
|
|
326
|
+
if (!promptText && base.length === 0) {
|
|
327
|
+
return base;
|
|
328
|
+
}
|
|
329
|
+
|
|
104
330
|
if (promptText) {
|
|
105
331
|
const normalizedPrompt = normalizeText(promptText);
|
|
106
|
-
|
|
332
|
+
const last = base[base.length - 1];
|
|
333
|
+
const previousUser = last?.role === 'assistant' ? base[base.length - 2] : last;
|
|
334
|
+
if (!previousUser || previousUser.role !== 'user' || !looksLikeSamePrompt(previousUser.content, normalizedPrompt)) {
|
|
107
335
|
base.push({ role: 'user', content: promptText });
|
|
108
336
|
}
|
|
109
337
|
}
|
|
110
338
|
|
|
111
|
-
const candidateAssistant =
|
|
112
|
-
if (candidateAssistant)
|
|
113
|
-
const normalizedAssistant = normalizeText(candidateAssistant);
|
|
114
|
-
const lastMsg = base[base.length - 1];
|
|
115
|
-
if (lastMsg && lastMsg.role === 'assistant') {
|
|
116
|
-
const existing = normalizeText(lastMsg.content);
|
|
117
|
-
if (normalizedAssistant !== existing) {
|
|
118
|
-
lastMsg.content = candidateAssistant;
|
|
119
|
-
}
|
|
120
|
-
} else {
|
|
121
|
-
base.push({ role: 'assistant', content: candidateAssistant });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
339
|
+
const candidateAssistant = assistantText || partialText;
|
|
340
|
+
if (!candidateAssistant) return base;
|
|
124
341
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
342
|
+
const normalizedAssistant = normalizeText(candidateAssistant);
|
|
343
|
+
if (!normalizedAssistant) return base;
|
|
344
|
+
if (looksLikePromptEchoText(candidateAssistant, promptText, previousMessages)) return base;
|
|
345
|
+
if (!assistantText && isSpinnerOnlyText(candidateAssistant)) return base;
|
|
346
|
+
|
|
347
|
+
const last = base[base.length - 1];
|
|
348
|
+
if (last && last.role === 'assistant') {
|
|
349
|
+
if (normalizeText(last.content) !== normalizedAssistant) {
|
|
350
|
+
last.content = candidateAssistant;
|
|
132
351
|
}
|
|
352
|
+
} else {
|
|
353
|
+
base.push({ role: 'assistant', content: candidateAssistant });
|
|
133
354
|
}
|
|
134
355
|
|
|
135
356
|
return base;
|
|
136
357
|
}
|
|
137
358
|
|
|
359
|
+
function toMessageObjects(messages, status) {
|
|
360
|
+
return messages.slice(-50).map((message, index, slice) => ({
|
|
361
|
+
id: `msg_${index}`,
|
|
362
|
+
role: message.role,
|
|
363
|
+
content: typeof message.content === 'string' && message.content.length > 6000
|
|
364
|
+
? message.content.slice(0, 6000) + '\n[... truncated]'
|
|
365
|
+
: message.content,
|
|
366
|
+
index,
|
|
367
|
+
kind: 'standard',
|
|
368
|
+
...(status === 'generating' && index === slice.length - 1 && message.role === 'assistant'
|
|
369
|
+
? { meta: { streaming: true } }
|
|
370
|
+
: {}),
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
|
|
138
374
|
module.exports = function parseOutput(input) {
|
|
139
|
-
const
|
|
140
|
-
const
|
|
375
|
+
const screenText = String(input?.screenText || '');
|
|
376
|
+
const buffer = String(input?.buffer || '');
|
|
377
|
+
const terminalHistory = String(input?.terminalHistory || '');
|
|
378
|
+
const tail = String(input?.recentBuffer || (screenText || buffer).slice(-500));
|
|
379
|
+
const previousMessages = Array.isArray(input?.messages) ? input.messages : [];
|
|
380
|
+
const transcriptSource = screenText || buffer;
|
|
141
381
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
382
|
+
const status = detectStatus({
|
|
383
|
+
tail,
|
|
384
|
+
screenText,
|
|
385
|
+
rawBuffer: input?.rawBuffer || '',
|
|
386
|
+
});
|
|
145
387
|
|
|
146
|
-
// Modal
|
|
147
388
|
const activeModal = status === 'waiting_approval'
|
|
148
|
-
? parseApproval({ buffer:
|
|
389
|
+
? parseApproval({ buffer: screenText || buffer, rawBuffer: input?.rawBuffer || '', tail })
|
|
149
390
|
: null;
|
|
150
391
|
|
|
151
|
-
const { promptText, assistantText } =
|
|
392
|
+
const { promptText, assistantText: visibleAssistantText } = status === 'waiting_approval'
|
|
393
|
+
? { promptText: '', assistantText: '' }
|
|
394
|
+
: extractVisibleTurn(transcriptSource, previousMessages);
|
|
395
|
+
const denseTerminalOutput = extractDenseOutputBlock(terminalHistory || buffer);
|
|
396
|
+
const assistantText = denseTerminalOutput || visibleAssistantText;
|
|
397
|
+
const rawPartialText = status === 'generating'
|
|
398
|
+
? extractPartialAssistant(input?.partialResponse || '')
|
|
399
|
+
: '';
|
|
400
|
+
const partialText = (!rawPartialText
|
|
401
|
+
|| isSpinnerOnlyText(rawPartialText)
|
|
402
|
+
|| looksLikePromptEchoText(rawPartialText, promptText, previousMessages))
|
|
403
|
+
? ''
|
|
404
|
+
: rawPartialText;
|
|
152
405
|
const messages = toMessageObjects(
|
|
153
|
-
buildMessages(previousMessages, promptText, assistantText,
|
|
406
|
+
buildMessages(previousMessages, promptText, assistantText, partialText),
|
|
154
407
|
status
|
|
155
408
|
);
|
|
156
409
|
|
|
@@ -1,20 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codex CLI — detect_status
|
|
3
|
-
* OpenAI Codex CLI uses a different TUI pattern.
|
|
4
3
|
*/
|
|
5
4
|
'use strict';
|
|
5
|
+
|
|
6
|
+
function sourceText(input) {
|
|
7
|
+
return `${String(input?.screenText || '')}\n${String(input?.tail || '')}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasWelcomeScreen(text) {
|
|
11
|
+
return /OpenAI Codex/i.test(text)
|
|
12
|
+
&& /To get started, describe a task/i.test(text);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasStartupApproval(text) {
|
|
16
|
+
return /You are running Codex in/i.test(text)
|
|
17
|
+
&& /Press Enter to continue/i.test(text)
|
|
18
|
+
&& /(?:^|\n)[▌> \t]*1\.\s+/m.test(text)
|
|
19
|
+
&& /(?:^|\n)[▌> \t]*2\.\s+/m.test(text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasCommandApproval(text) {
|
|
23
|
+
const hasAllowPrompt = /Allow Codex to (?:run|apply)/i.test(text)
|
|
24
|
+
|| /Allow command\?/i.test(text);
|
|
25
|
+
const hasButtons = /Approve and run now/i.test(text)
|
|
26
|
+
|| /Always approve this session/i.test(text)
|
|
27
|
+
|| /(?:^|\n)[▌> \t]*1\.\s+.*(?:approve|allow|run)/im.test(text)
|
|
28
|
+
|| /(?:^|\n)[▌> \t]*2\.\s+.*(?:always|session)/im.test(text);
|
|
29
|
+
const hasFooter = /Press Enter to confirm/i.test(text)
|
|
30
|
+
|| /Esc to cancel/i.test(text);
|
|
31
|
+
return hasAllowPrompt
|
|
32
|
+
|| (hasButtons && hasFooter)
|
|
33
|
+
|| /Approve and run now/i.test(text)
|
|
34
|
+
|| /Always approve this session/i.test(text)
|
|
35
|
+
|| /(?:^|\n)[▌> \t]*1\.\s+.*(?:approve|allow|run)/im.test(text);
|
|
36
|
+
}
|
|
37
|
+
|
|
6
38
|
module.exports = function detectStatus(input) {
|
|
7
|
-
const
|
|
8
|
-
if (!
|
|
39
|
+
const text = sourceText(input);
|
|
40
|
+
if (!text.trim()) return 'idle';
|
|
41
|
+
|
|
42
|
+
if (hasStartupApproval(text) || hasCommandApproval(text)) return 'waiting_approval';
|
|
9
43
|
|
|
10
|
-
|
|
11
|
-
if (/
|
|
12
|
-
|
|
44
|
+
if (/Esc to interrupt/i.test(text)) return 'generating';
|
|
45
|
+
if (/(?:Thinking|Planning|Searching|Reading|Working|Analyzing|Inspecting|Responding|Following instructions clearly)[^\n]*\(\d+s\b/i.test(text)) {
|
|
46
|
+
return 'generating';
|
|
47
|
+
}
|
|
48
|
+
if (/[⠁-⣿]/.test(text) && /(?:Working|Thinking|Esc to interrupt)/i.test(text)) return 'generating';
|
|
13
49
|
|
|
14
|
-
|
|
15
|
-
if (/
|
|
16
|
-
if (/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/.test(tail)) return 'generating';
|
|
17
|
-
if (/thinking|processing|running/i.test(tail)) return 'generating';
|
|
50
|
+
if (hasWelcomeScreen(text)) return 'idle';
|
|
51
|
+
if (/⏎\s+send/i.test(text)) return 'idle';
|
|
18
52
|
|
|
19
53
|
return 'idle';
|
|
20
54
|
};
|
|
@@ -2,14 +2,90 @@
|
|
|
2
2
|
* Codex CLI — parse_approval
|
|
3
3
|
*/
|
|
4
4
|
'use strict';
|
|
5
|
+
|
|
6
|
+
function splitLines(text) {
|
|
7
|
+
return String(text || '')
|
|
8
|
+
.replace(/\u0007/g, '')
|
|
9
|
+
.split(/\r\n|\n|\r/g)
|
|
10
|
+
.map(line => line.replace(/\s+$/, ''));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalize(line) {
|
|
14
|
+
return String(line || '')
|
|
15
|
+
.replace(/\u0007/g, '')
|
|
16
|
+
.replace(/^\d+;/, '')
|
|
17
|
+
.replace(/\s+/g, ' ')
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isBoxLine(line) {
|
|
22
|
+
return /^[─═╭╮╰╯│┌┐└┘├┤┬┴┼]+$/.test(line);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isFooterLine(line) {
|
|
26
|
+
return /⏎\s+send/i.test(line)
|
|
27
|
+
|| /⌃J\s+newline/i.test(line)
|
|
28
|
+
|| /⌃T\s+transcript/i.test(line)
|
|
29
|
+
|| /⌃C\s+quit/i.test(line)
|
|
30
|
+
|| /Press Enter to (?:continue|confirm)/i.test(line)
|
|
31
|
+
|| /Esc to cancel/i.test(line);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stripLeadingMarkers(s) {
|
|
35
|
+
return s.replace(/^(?:[▌>]\s*)+/, '').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeButton(line) {
|
|
39
|
+
return stripLeadingMarkers(normalize(line))
|
|
40
|
+
.replace(/^\d+\.\s+/, '')
|
|
41
|
+
.replace(/\s{2,}\([A-Za-z]\)\s.*$/, '')
|
|
42
|
+
.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isButtonLine(line) {
|
|
46
|
+
return /^\d+\.\s+/.test(stripLeadingMarkers(normalize(line)));
|
|
47
|
+
}
|
|
48
|
+
|
|
5
49
|
module.exports = function parseApproval(input) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
50
|
+
const text = String(input?.buffer || input?.tail || '');
|
|
51
|
+
const lines = splitLines(text);
|
|
52
|
+
if (lines.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
const buttons = [];
|
|
55
|
+
let currentButton = '';
|
|
56
|
+
const recentLines = lines.slice(-60);
|
|
57
|
+
for (const rawLine of recentLines) {
|
|
58
|
+
const line = normalize(rawLine);
|
|
59
|
+
if (!line) continue;
|
|
60
|
+
if (isButtonLine(rawLine)) {
|
|
61
|
+
if (currentButton && !buttons.includes(currentButton)) buttons.push(currentButton);
|
|
62
|
+
currentButton = normalizeButton(rawLine);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!currentButton) continue;
|
|
66
|
+
if (isFooterLine(line) || isBoxLine(line)) continue;
|
|
67
|
+
if (/^(?:model|directory):/i.test(line)) continue;
|
|
68
|
+
const continuation = stripLeadingMarkers(line);
|
|
69
|
+
if (!continuation) continue;
|
|
70
|
+
currentButton = `${currentButton} ${continuation}`.replace(/\s+/g, ' ').trim();
|
|
71
|
+
}
|
|
72
|
+
if (currentButton && !buttons.includes(currentButton)) buttons.push(currentButton);
|
|
73
|
+
|
|
74
|
+
const approvalText = lines
|
|
75
|
+
.map(normalize)
|
|
76
|
+
.filter(line => line && !isBoxLine(line) && !isButtonLine(line) && !isFooterLine(line))
|
|
77
|
+
.filter(line => !/^OpenAI Codex\b/i.test(line))
|
|
78
|
+
.filter(line => !/^model:/i.test(line))
|
|
79
|
+
.filter(line => !/^directory:/i.test(line));
|
|
80
|
+
|
|
81
|
+
const hasApproval = /You are running Codex in/i.test(text)
|
|
82
|
+
|| /Allow Codex to (?:run|apply)/i.test(text)
|
|
83
|
+
|| /Allow command\?/i.test(text)
|
|
84
|
+
|| buttons.length > 0;
|
|
85
|
+
if (!hasApproval) return null;
|
|
86
|
+
|
|
11
87
|
return {
|
|
12
|
-
message:
|
|
13
|
-
buttons: ['Approve', 'Deny'],
|
|
88
|
+
message: approvalText.slice(-3).join(' ').slice(0, 240) || 'Codex approval required',
|
|
89
|
+
buttons: buttons.length > 0 ? buttons : ['Approve', 'Deny'],
|
|
14
90
|
};
|
|
15
91
|
};
|