@adhdev/daemon-core 0.5.7 → 0.5.16

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.
@@ -1,12 +1,15 @@
1
1
  /**
2
- * Cursor — read_chat
2
+ * Cursor — read_chat (v2 — thoughts, tools, terminal, DOM-ordered)
3
3
  *
4
4
  * DOM 구조 (v0.49):
5
5
  * 컴포저: [data-composer-id] + [data-composer-status]
6
6
  * 메시지 쌍: .composer-human-ai-pair-container
7
7
  * 사용자: .composer-human-message
8
- * AI: direct children after first (tool/assistant)
9
- * 입력: .aislash-editor-input[contenteditable="true"]
8
+ * AI 텍스트: .composer-rendered-message (assistant content)
9
+ * 사고: .ui-step-group-collapsible → header에 "Thought briefly"
10
+ * 도구: .composer-tool-former-message (file edits, reads)
11
+ * 코드블록: .composer-code-block-container (terminal commands)
12
+ * Diff: .composer-diff-block (code changes)
10
13
  * 승인: .run-command-review-active + button/cursor-pointer 요소
11
14
  *
12
15
  * → { id, status, title, messages[], inputContent, activeModal }
@@ -20,18 +23,12 @@
20
23
  if (rawStatus === 'thinking' || rawStatus === 'streaming') status = 'generating';
21
24
  else if (rawStatus === 'completed' || rawStatus === 'idle' || !rawStatus) status = 'idle';
22
25
 
23
- // Detect approval dialogs
26
+ // ─── Approval Detection ───
24
27
  let activeModal = null;
25
-
26
- // Primary signal: Cursor uses .run-command-review-active on conversations container
27
28
  const reviewActive = !!document.querySelector('.run-command-review-active');
28
-
29
- // Also check clickable elements (Cursor uses divs with cursor-pointer, not buttons)
30
- // Note: Cursor concatenates button text with shortcut key labels (e.g. "SkipEsc", "Run⏎")
31
29
  const clickableEls = [...document.querySelectorAll('button, [role="button"], .cursor-pointer')].filter(b =>
32
30
  b.offsetWidth > 0 && /^(accept|reject|approve|deny|run|skip|allow|cancel)/i.test((b.textContent || b.getAttribute('aria-label') || '').trim())
33
31
  );
34
-
35
32
  if (reviewActive || clickableEls.length > 0) {
36
33
  status = 'waiting_approval';
37
34
  const reviewContainer = document.querySelector('.run-command-review-active');
@@ -44,36 +41,278 @@
44
41
  };
45
42
  }
46
43
 
47
- const msgs = [];
48
- document.querySelectorAll('.composer-human-ai-pair-container').forEach((p, i) => {
49
- if (p.children.length === 0) return; // skip virtual-scroll placeholders
50
- const h = p.querySelector('.composer-human-message');
51
- if (h) {
52
- const userText = (h.innerText || '').trim().substring(0, 6000);
53
- if (userText) msgs.push({ role: 'user', content: userText, index: msgs.length });
54
- }
55
- // Iterate direct children after the first (user message block)
56
- for (let ci = 1; ci < p.children.length; ci++) {
57
- const b = p.children[ci];
58
- if ((b.className || '').includes('opacity-50')) continue;
59
- const t = (b.innerText || '').trim();
44
+ // ─── Message Collection ───
45
+ const collected = [];
46
+ const seenHashes = new Set();
47
+
48
+ // Markdown Extractor to preserve tables/lists structure properly instead of flat innerText
49
+ function extractMarkdown(node) {
50
+ if (!node) return '';
51
+ if (node.nodeType === 3) return node.textContent;
52
+ if (node.nodeType !== 1) return '';
53
+
54
+ const tag = node.tagName.toLowerCase();
55
+
56
+ // Stop extracting into nested block containers that we already process as separate items
57
+ if (node.classList && (
58
+ node.classList.contains('composer-tool-former-message') ||
59
+ node.classList.contains('composer-code-block-container') ||
60
+ node.classList.contains('composer-diff-block')
61
+ )) {
62
+ return '';
63
+ }
64
+
65
+ let res = '';
66
+ if (tag === 'p' || tag === 'div') {
67
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
68
+ return res + '\n\n';
69
+ }
70
+ if (/^h[1-6]$/.test(tag)) {
71
+ const level = parseInt(tag.charAt(1), 10);
72
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
73
+ return '#'.repeat(level) + ' ' + res.trim() + '\n\n';
74
+ }
75
+ if (tag === 'ul' || tag === 'ol') {
76
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
77
+ return res + '\n';
78
+ }
79
+ if (tag === 'li') {
80
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
81
+ return '- ' + res.trim() + '\n';
82
+ }
83
+ if (tag === 'strong' || tag === 'b') {
84
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
85
+ return '**' + res + '**';
86
+ }
87
+ if (tag === 'em' || tag === 'i') {
88
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
89
+ return '*' + res + '*';
90
+ }
91
+ if (tag === 'code') {
92
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
93
+ return '`' + res + '`';
94
+ }
95
+ if (tag === 'pre') {
96
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
97
+ return '\n```\n' + res.trim() + '\n```\n\n';
98
+ }
99
+ if (tag === 'a') {
100
+ let text = '';
101
+ for (let i = 0; i < node.childNodes.length; i++) text += extractMarkdown(node.childNodes[i]);
102
+ return '[' + text + '](' + (node.href || '') + ')';
103
+ }
104
+ if (tag === 'table') {
105
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
106
+ return '\n' + res.trim() + '\n\n';
107
+ }
108
+ if (tag === 'thead') {
109
+ const tr = node.querySelector('tr');
110
+ if (tr) {
111
+ const ths = Array.from(tr.querySelectorAll('th')).map(th => extractMarkdown(th).trim());
112
+ return '| ' + ths.join(' | ') + ' |\n| ' + ths.map(() => '---').join(' | ') + ' |\n';
113
+ }
114
+ return '';
115
+ }
116
+ if (tag === 'tbody') {
117
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
118
+ return res;
119
+ }
120
+ if (tag === 'tr' && (!node.parentElement || node.parentElement.tagName.toLowerCase() !== 'thead')) {
121
+ const tds = Array.from(node.querySelectorAll('td')).map(td => extractMarkdown(td).trim().replace(/\n/g, '<br>'));
122
+ return '| ' + tds.join(' | ') + ' |\n';
123
+ }
124
+
125
+ // Default
126
+ for (let i = 0; i < node.childNodes.length; i++) res += extractMarkdown(node.childNodes[i]);
127
+ return res;
128
+ }
129
+
130
+ document.querySelectorAll('.composer-human-ai-pair-container').forEach((pair) => {
131
+ if (pair.children.length === 0) return; // virtual-scroll placeholder
132
+
133
+ // ─── User Message ───
134
+ const humanEl = pair.querySelector('.composer-human-message');
135
+ if (humanEl) {
136
+ const userText = (humanEl.innerText || '').trim().substring(0, 6000);
137
+ if (userText) {
138
+ const hash = 'user:' + userText.slice(0, 200);
139
+ if (!seenHashes.has(hash)) {
140
+ seenHashes.add(hash);
141
+ collected.push({ role: 'user', text: userText, el: humanEl, kind: 'standard' });
142
+ }
143
+ }
144
+ }
145
+
146
+ // ─── AI Message Blocks ───
147
+ const aiBlocks = Array.from(pair.querySelectorAll('.composer-assistant-message, [data-message-role="ai"], [data-message-role="assistant"]'));
148
+ for (const block of aiBlocks) {
149
+
150
+ // ─── Thinking ("Thought briefly", etc) ───
151
+ const uiStepGroup = block.querySelectorAll('.ui-step-group-collapsible, [class*="collapsible"]');
152
+ if (uiStepGroup.length > 0) {
153
+ uiStepGroup.forEach(group => {
154
+ const collapsibleHeader = group.querySelector('.ui-collapsible-header');
155
+ const spans = collapsibleHeader?.querySelectorAll('span');
156
+ const headerText = spans && spans.length > 0
157
+ ? Array.from(spans).map(s => (s.textContent || '').trim()).filter(Boolean).join(' ')
158
+ : (collapsibleHeader?.innerText || collapsibleHeader?.textContent || '').trim();
159
+
160
+ // "Thought briefly", "Thought for Xs" — thinking content
161
+ if (headerText && /thought/i.test(headerText)) {
162
+ // Try to get expanded thought content
163
+ const contentEl = block.querySelector('.ui-collapsible-content, [class*="collapsible-content"]');
164
+ const thinkText = contentEl ? (contentEl.innerText || '').trim() : '';
165
+ const label = headerText;
166
+ const hash = 'think:' + (thinkText || headerText).slice(0, 200);
167
+
168
+ if (!seenHashes.has(hash)) {
169
+ seenHashes.add(hash);
170
+ // Prepend thinking into collected as a special block
171
+ let combined = `> **${label}**\n> \n`;
172
+ if (thinkText) {
173
+ combined += thinkText.split('\n').map(l => `> ${l}`).join('\n');
174
+ } else {
175
+ combined += '> ...\n'; // collapsed
176
+ }
177
+ collected.push({ role: 'assistant', text: combined, el: group, kind: 'thought' });
178
+ }
179
+ }
180
+ });
181
+ // Note: If the entire block is just a thought, maybe continue, but usually there's rendered text after.
182
+ }
183
+
184
+ // ─── Tools (Reads, Edits, etc) ───
185
+ const toolMsgs = block.querySelectorAll('.composer-tool-former-message');
186
+ if (toolMsgs.length > 0) {
187
+ toolMsgs.forEach(tm => {
188
+ let cleanText = '';
189
+ // For file modifications/reads, try to just grab the file path (first line)
190
+ // It usually looks like: "MachineDetail.tsx+3-1 Read file"
191
+ const rawText = (tm.innerText || '').trim();
192
+ if (rawText) {
193
+ cleanText = rawText.split('\n')[0].substring(0, 80)
194
+ .replace(/(?:Skip|Run|Accept|Reject)(?:Esc)?$/g, '').trim();
195
+ // If it looks like code/JSX, truncate further
196
+ if (/[<>{}()=]/.test(cleanText) && cleanText.length > 50) {
197
+ cleanText = cleanText.substring(0, 50) + '…';
198
+ }
199
+ }
200
+ if (!cleanText || cleanText.length < 2) return;
201
+ const hash = 'tool:' + cleanText;
202
+ if (!seenHashes.has(hash)) {
203
+ seenHashes.add(hash);
204
+ collected.push({ role: 'assistant', text: cleanText, el: tm, kind: 'tool' });
205
+ }
206
+ });
207
+ }
208
+
209
+ // ─── Code blocks / Terminal ───
210
+ const codeBlocks = block.querySelectorAll('.composer-code-block-container');
211
+ if (codeBlocks.length > 0) {
212
+ codeBlocks.forEach(cb => {
213
+ let codeText = (cb.textContent || '').trim().substring(0, 3000);
214
+ // Strip button text noise
215
+ codeText = codeText.replace(/(?:Skip|Run|Accept|Reject)(?:Esc|⏎|↵)?\s*$/g, '').trim();
216
+ codeText = codeText.replace(/\n(?:Skip|Run|Accept|Reject)(?:Esc|⏎|↵)?\s*/g, '\n').trim();
217
+ if (codeText.length < 2) return;
218
+ // Detect if it's a terminal command vs code block
219
+ const isTerminal = cb.querySelector('[class*="terminal"]') ||
220
+ cb.closest('.run-command-review-active') ||
221
+ /^\$\s/.test(codeText) ||
222
+ /^(?:cd |npm |node |npx |git |make |docker |curl |wget |ls |cat |mkdir |rm |mv |cp )/i.test(codeText);
223
+ const hash = (isTerminal ? 'term:' : 'code:') + codeText.slice(0, 200);
224
+ if (!seenHashes.has(hash)) {
225
+ seenHashes.add(hash);
226
+ // For terminal: extract first line as label
227
+ const label = isTerminal ? codeText.split('\n')[0].substring(0, 100) : undefined;
228
+ collected.push({
229
+ role: 'assistant',
230
+ text: codeText,
231
+ el: cb,
232
+ kind: isTerminal ? 'terminal' : 'code',
233
+ label
234
+ });
235
+ }
236
+ });
237
+ }
238
+
239
+ // ─── Diff Blocks ───
240
+ const diffBlocks = block.querySelectorAll('.composer-diff-block');
241
+ if (diffBlocks.length > 0) {
242
+ diffBlocks.forEach(db => {
243
+ const diffText = (db.textContent || '').trim().substring(0, 2000);
244
+ if (diffText.length < 5) return;
245
+ const hash = 'diff:' + diffText.slice(0, 200);
246
+ if (!seenHashes.has(hash)) {
247
+ seenHashes.add(hash);
248
+ collected.push({ role: 'assistant', text: diffText, el: db, kind: 'tool' });
249
+ }
250
+ });
251
+ }
252
+
253
+ // ─── Rendered assistant message ───
254
+ const rendered = block.classList.contains('composer-rendered-message') ? block : block.querySelector('.composer-rendered-message');
255
+ if (rendered) {
256
+ const t = extractMarkdown(rendered).replace(/\n{3,}/g, '\n\n').trim();
257
+ if (t.length >= 2) {
258
+ // Strip button noise from end
259
+ const clean = t.replace(/(?:Skip|Run|Accept|Reject)(?:Esc)?\s*$/g, '').trim();
260
+ if (clean.length >= 2) {
261
+ const hash = 'assistant:' + clean.slice(0, 200);
262
+ if (!seenHashes.has(hash)) {
263
+ seenHashes.add(hash);
264
+ collected.push({ role: 'assistant', text: clean.substring(0, 6000), el: rendered, kind: 'standard' });
265
+ }
266
+ }
267
+ }
268
+ continue;
269
+ }
270
+
271
+ // ─── Fallback: any remaining text block ───
272
+ const t = (block.innerText || '').trim();
60
273
  if (t.length < 2) continue;
61
- // Filter noise: "Thought for Xs", "Explored N files", step group timing headers (e.g. "Crafting a minimal response\n1s")
274
+ // Filter noise
62
275
  if (/^Thought\nfor \d+s?$/i.test(t) || /^Explored\n/i.test(t)) continue;
63
- if (/^.{1,80}\n\d+s$/.test(t) && !t.includes('\n\n')) continue; // step-group collapsible header with timing
64
- // Skip step-group collapsible headers (e.g. "Crafting a minimal response\n1s")
65
- if (b.querySelector('.ui-step-group-collapsible, .ui-collapsible-header') &&
66
- !b.querySelector('.composer-rendered-message, .composer-tool-former-message, .composer-diff-block')) continue;
67
- const hasTool = b.querySelector('.composer-tool-former-message, .composer-diff-block, .composer-code-block-container');
68
- msgs.push({ role: hasTool ? 'tool' : 'assistant', content: t.substring(0, 6000), index: msgs.length });
276
+ if (/^.{1,80}\n\d+s$/.test(t) && !t.includes('\n\n')) continue;
277
+
278
+ const hash = 'other:' + t.slice(0, 200);
279
+ if (!seenHashes.has(hash)) {
280
+ seenHashes.add(hash);
281
+ collected.push({ role: 'assistant', text: t.substring(0, 6000), el: block, kind: 'standard' });
282
+ }
69
283
  }
70
284
  });
285
+
286
+ // ─── DOM Order Sort ───
287
+ collected.sort((a, b) => {
288
+ const pos = a.el.compareDocumentPosition(b.el);
289
+ if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
290
+ if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
291
+ return 0;
292
+ });
293
+
294
+ // Keep last 50 messages
295
+ const trimmed = collected.length > 50 ? collected.slice(-50) : collected;
296
+
297
+ const messages = trimmed.map((m, i) => ({
298
+ id: 'msg_' + i,
299
+ role: m.role,
300
+ content: m.text.length > 6000 ? m.text.slice(0, 6000) + '\n[... truncated]' : m.text,
301
+ index: i,
302
+ kind: m.kind || 'standard',
303
+ meta: m.meta || undefined,
304
+ }));
305
+
306
+ // ─── Input Content ───
71
307
  const inputEl = document.querySelector('.aislash-editor-input[contenteditable="true"]');
72
308
  const inputContent = inputEl?.textContent?.trim() || '';
309
+
310
+ // ─── Title ───
73
311
  const titleParts = document.title.split(' — ');
74
312
  const projectTitle = (titleParts.length >= 2 ? titleParts[titleParts.length - 2] : titleParts[0] || '').trim();
75
- return JSON.stringify({ id, status, title: projectTitle, messages: msgs, inputContent, activeModal });
313
+
314
+ return JSON.stringify({ id, status, title: projectTitle, messages, inputContent, activeModal });
76
315
  } catch(e) {
77
- return JSON.stringify({ id: '', status: 'error', messages: [] });
316
+ return JSON.stringify({ id: '', status: 'error', error: e.message, messages: [] });
78
317
  }
79
318
  })()
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.03.19",
2
+ "version": "2026.03.21",
3
3
  "providers": {
4
4
  "agentpool-acp": {
5
5
  "providerVersion": "0.0.0",
@@ -186,12 +186,11 @@ function coercePatternArray(raw: unknown, fallbacks: RegExp[]): RegExp[] {
186
186
  const FALLBACK_PROMPT: RegExp[] = [
187
187
  /Type your message/i,
188
188
  /^>\s*$/m, // '>' alone on its own line
189
- /[›➤]\s*$/,
190
- /for shortcuts/i,
191
- /\?\s*for help/i,
189
+ /[›❯]\s*[\r\n]/, // prompt char followed by line ending (ANSI-stripped may not have $ at end)
190
+ /[›❯]\s*$/m, // prompt char at end of line (multiline)
191
+ /for\s*shortcuts/i, // Claude Code prompt (ANSI strip may remove spaces → 'forshortcuts')
192
+ /\?\s*for\s*help/i,
192
193
  /Press enter/i,
193
- // NOTE: removed /^[\s\u2500-\u257f]*>\s*$/m — the box-drawing char range is too wide and
194
- // can match dialog-clearing ANSI output, causing false prompt detection in approval state.
195
194
  ];
196
195
 
197
196
  const FALLBACK_GENERATING: RegExp[] = [
@@ -202,13 +201,14 @@ const FALLBACK_GENERATING: RegExp[] = [
202
201
  ];
203
202
 
204
203
  const FALLBACK_APPROVAL: RegExp[] = [
205
- /Allow\s+once/i,
206
- /Always\s+allow/i,
204
+ /Allow\s*once/i, // ANSI strip may remove spaces
205
+ /Always\s*allow/i,
207
206
  /\(y\/n\)/i,
208
207
  /\[Y\/n\]/i,
209
- /Run\s+this\s+command/i,
210
- // NOTE: removed /Do you want to (?:run|execute|allow)/i too broad, matches AI explanation
211
- // text like "Do you want to allow this feature?" causing false approval notifications.
208
+ /Run\s*this\s*command/i,
209
+ /Allow\s*tool/i, // Claude Code v2 approval
210
+ /Yes,?\s*don'?t\s*ask/i, // "Yes, don't ask again" (Claude Code)
211
+ /Deny/i, // Deny button presence = approval dialog
212
212
  ];
213
213
 
214
214
  function defaultCleanOutput(raw: string, _lastUserInput?: string): string {
@@ -268,9 +268,24 @@ export class ProviderCliAdapter implements CliAdapter {
268
268
  // Resize redraw suppression
269
269
  private resizeSuppressUntil: number = 0;
270
270
 
271
+ // Debug: status transition history
272
+ private statusHistory: { status: string; at: number; trigger?: string }[] = [];
273
+
274
+ private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
275
+ const prev = this.currentStatus;
276
+ if (prev === status) return;
277
+ this.currentStatus = status;
278
+ this.statusHistory.push({ status, at: Date.now(), trigger });
279
+ if (this.statusHistory.length > 50) this.statusHistory.shift();
280
+ LOG.info('CLI', `[${this.cliType}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
281
+ }
282
+
271
283
  // Resolved timeouts (provider defaults + overrides)
272
284
  private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
273
285
 
286
+ // Provider approval key mapping (e.g. { 0: '1', 1: '2', 2: '3' }) — loaded from provider.json
287
+ private readonly approvalKeys: Record<number, string>;
288
+
274
289
  constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
275
290
  this.provider = normalizeCliProviderForRuntime(provider);
276
291
  this.cliType = provider.type;
@@ -290,6 +305,10 @@ export class ProviderCliAdapter implements CliAdapter {
290
305
  maxResponse: t.maxResponse ?? 300000,
291
306
  shutdownGrace: t.shutdownGrace ?? 1000,
292
307
  };
308
+
309
+ // Load approval key mapping from provider (e.g. approvalKeys: {"0":"1","1":"2","2":"3"})
310
+ const rawKeys = (provider as any).approvalKeys;
311
+ this.approvalKeys = (rawKeys && typeof rawKeys === 'object') ? rawKeys : {};
293
312
  }
294
313
 
295
314
  // ─── Lifecycle ─────────────────────────────────
@@ -390,12 +409,12 @@ export class ProviderCliAdapter implements CliAdapter {
390
409
  this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
391
410
  LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
392
411
  this.ptyProcess = null;
393
- this.currentStatus = 'stopped';
412
+ this.setStatus('stopped', 'pty_exit');
394
413
  this.ready = false;
395
414
  this.onStatusChange?.();
396
415
  });
397
416
 
398
- this.currentStatus = 'starting';
417
+ this.setStatus('starting', 'spawn');
399
418
  this.onStatusChange?.();
400
419
  }
401
420
 
@@ -423,11 +442,15 @@ export class ProviderCliAdapter implements CliAdapter {
423
442
  // ─── Phase 1: Startup — ready status wait
424
443
  if (!this.ready) {
425
444
  this.startupBuffer += cleanData;
445
+ LOG.info('CLI', `[${this.cliType}] startup chunk (${cleanData.length} chars): ${cleanData.slice(0, 200).replace(/\n/g, '\\n')}`);
426
446
 
427
447
  // Startup dialog auto-proceed (Enter)
428
448
  const dialogPatterns = [
429
449
  /Do you want to connect/i,
430
450
  /Do you trust the files/i,
451
+ /Quick safety check/i,
452
+ /Is this a project/i,
453
+ /Enter to confirm/i,
431
454
  ];
432
455
  if (dialogPatterns.some(p => p.test(this.startupBuffer))) {
433
456
  setTimeout(() => this.ptyProcess?.write('\r'), this.timeouts.dialogAccept);
@@ -438,7 +461,7 @@ export class ProviderCliAdapter implements CliAdapter {
438
461
  // Prompt → ready
439
462
  if (patterns.prompt.some(p => p.test(this.startupBuffer))) {
440
463
  this.ready = true;
441
- this.currentStatus = 'idle';
464
+ this.setStatus('idle', 'prompt_matched');
442
465
  LOG.info('CLI', `[${this.cliType}] ✓ Ready`);
443
466
  this.onStatusChange?.();
444
467
  }
@@ -455,12 +478,14 @@ export class ProviderCliAdapter implements CliAdapter {
455
478
  .map(l => l.trim())
456
479
  .filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
457
480
  this.isWaitingForResponse = true;
458
- this.currentStatus = 'waiting_approval';
481
+ this.setStatus('waiting_approval', 'approval_pattern');
459
482
  this.recentOutputBuffer = '';
460
483
  this.approvalTransitionBuffer = '';
461
484
  this.activeModal = {
462
485
  message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
463
- buttons: ['Allow once', 'Always allow', 'Deny'],
486
+ buttons: this.cliType === 'claude-cli'
487
+ ? ['Yes (y)', 'Always allow (a)', 'Deny (Esc)']
488
+ : ['Allow once', 'Always allow', 'Deny'],
464
489
  };
465
490
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
466
491
  // Safety timeout — if stuck in waiting_approval, auto-exit after 60s
@@ -473,7 +498,7 @@ export class ProviderCliAdapter implements CliAdapter {
473
498
  this.recentOutputBuffer = '';
474
499
  this.approvalTransitionBuffer = '';
475
500
  this.approvalExitTimeout = null;
476
- this.currentStatus = this.isWaitingForResponse ? 'generating' : 'idle';
501
+ this.setStatus(this.isWaitingForResponse ? 'generating' : 'idle', 'approval_cleared');
477
502
  this.onStatusChange?.();
478
503
  }
479
504
  }, 60000);
@@ -490,7 +515,7 @@ export class ProviderCliAdapter implements CliAdapter {
490
515
  const promptResume = patterns.prompt.some(p => p.test(this.approvalTransitionBuffer));
491
516
  if (genResume) {
492
517
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
493
- this.currentStatus = 'generating';
518
+ this.setStatus('generating', 'approval_gen_resume');
494
519
  this.activeModal = null;
495
520
  this.recentOutputBuffer = '';
496
521
  this.approvalTransitionBuffer = '';
@@ -512,7 +537,7 @@ export class ProviderCliAdapter implements CliAdapter {
512
537
  if (patterns.generating.some(p => p.test(cleanData))) {
513
538
  this.isWaitingForResponse = true;
514
539
  this.responseBuffer = '';
515
- this.currentStatus = 'generating';
540
+ this.setStatus('generating', 'autonomous_gen');
516
541
  this.onStatusChange?.();
517
542
  }
518
543
  }
@@ -524,7 +549,7 @@ export class ProviderCliAdapter implements CliAdapter {
524
549
 
525
550
  const stillGenerating = patterns.generating.some(p => p.test(cleanData));
526
551
  if (stillGenerating) {
527
- this.currentStatus = 'generating';
552
+ this.setStatus('generating', 'still_generating');
528
553
  this.idleTimeout = setTimeout(() => {
529
554
  if (this.isWaitingForResponse) this.finishResponse();
530
555
  }, this.timeouts.generatingIdle);
@@ -575,7 +600,7 @@ export class ProviderCliAdapter implements CliAdapter {
575
600
  this.responseBuffer = '';
576
601
  this.isWaitingForResponse = false;
577
602
  this.activeModal = null;
578
- this.currentStatus = 'idle';
603
+ this.setStatus('idle', 'response_finished');
579
604
  this.onStatusChange?.();
580
605
  }
581
606
 
@@ -598,7 +623,7 @@ export class ProviderCliAdapter implements CliAdapter {
598
623
  this.messages.push({ role: 'user', content: text, timestamp: Date.now() });
599
624
  this.isWaitingForResponse = true;
600
625
  this.responseBuffer = '';
601
- this.currentStatus = 'generating';
626
+ this.setStatus('generating', 'sendMessage');
602
627
  this.onStatusChange?.();
603
628
 
604
629
  this.ptyProcess.write(text + '\r');
@@ -623,7 +648,7 @@ export class ProviderCliAdapter implements CliAdapter {
623
648
  setTimeout(() => {
624
649
  try { this.ptyProcess?.kill(); } catch { }
625
650
  this.ptyProcess = null;
626
- this.currentStatus = 'stopped';
651
+ this.setStatus('stopped', 'stop_cmd');
627
652
  this.ready = false;
628
653
  this.onStatusChange?.();
629
654
  }, this.timeouts.shutdownGrace);
@@ -649,9 +674,16 @@ export class ProviderCliAdapter implements CliAdapter {
649
674
  */
650
675
  resolveModal(buttonIndex: number): void {
651
676
  if (!this.ptyProcess || this.currentStatus !== 'waiting_approval') return;
652
- const DOWN = '\x1B[B'; // Arrow Down
653
- const keys = DOWN.repeat(Math.max(0, buttonIndex)) + '\r';
654
- this.ptyProcess.write(keys);
677
+ // Use provider-defined approval keys if available
678
+ // Each provider.json can define: { "approvalKeys": { "0": "1", "1": "2", "2": "3" } }
679
+ if (buttonIndex in this.approvalKeys) {
680
+ this.ptyProcess.write(this.approvalKeys[buttonIndex]);
681
+ } else {
682
+ // Generic fallback: Arrow Down to navigate + Enter to select
683
+ const DOWN = '\x1B[B';
684
+ const keys = DOWN.repeat(Math.max(0, buttonIndex)) + '\r';
685
+ this.ptyProcess.write(keys);
686
+ }
655
687
  }
656
688
 
657
689
  resize(cols: number, rows: number): void {
@@ -664,4 +696,43 @@ export class ProviderCliAdapter implements CliAdapter {
664
696
  } catch { }
665
697
  }
666
698
  }
699
+
700
+ /**
701
+ * Full debug state — exposes all internal buffers, status, and patterns for debugging.
702
+ * Used by DevServer /api/cli/debug endpoint.
703
+ */
704
+ getDebugState(): Record<string, any> {
705
+ return {
706
+ type: this.cliType,
707
+ name: this.cliName,
708
+ status: this.currentStatus,
709
+ ready: this.ready,
710
+ workingDir: this.workingDir,
711
+ messages: this.messages.slice(-20),
712
+ messageCount: this.messages.length,
713
+ // Buffers
714
+ startupBuffer: this.startupBuffer.slice(-500),
715
+ recentOutputBuffer: this.recentOutputBuffer.slice(-500),
716
+ responseBuffer: this.responseBuffer.slice(-500),
717
+ approvalTransitionBuffer: this.approvalTransitionBuffer.slice(-500),
718
+ // State
719
+ isWaitingForResponse: this.isWaitingForResponse,
720
+ activeModal: this.activeModal,
721
+ lastApprovalResolvedAt: this.lastApprovalResolvedAt,
722
+ resizeSuppressUntil: this.resizeSuppressUntil,
723
+ // Provider patterns (serialized)
724
+ patterns: {
725
+ prompt: this.provider.patterns.prompt.map(p => p.toString()),
726
+ generating: this.provider.patterns.generating.map(p => p.toString()),
727
+ approval: this.provider.patterns.approval.map(p => p.toString()),
728
+ ready: this.provider.patterns.ready.map(p => p.toString()),
729
+ },
730
+ // Status history
731
+ statusHistory: this.statusHistory.slice(-30),
732
+ // Timeouts config
733
+ timeouts: this.timeouts,
734
+ // PTY alive
735
+ ptyAlive: !!this.ptyProcess,
736
+ };
737
+ }
667
738
  }