@adhdev/daemon-core 0.5.5 → 0.5.7
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/package.json
CHANGED
|
@@ -185,29 +185,30 @@ function coercePatternArray(raw: unknown, fallbacks: RegExp[]): RegExp[] {
|
|
|
185
185
|
/** Defaults tuned for Claude Code / similar agent CLIs when provider.json patterns are empty. */
|
|
186
186
|
const FALLBACK_PROMPT: RegExp[] = [
|
|
187
187
|
/Type your message/i,
|
|
188
|
-
|
|
188
|
+
/^>\s*$/m, // '>' alone on its own line
|
|
189
189
|
/[›➤]\s*$/,
|
|
190
190
|
/for shortcuts/i,
|
|
191
191
|
/\?\s*for help/i,
|
|
192
192
|
/Press enter/i,
|
|
193
|
-
/^[\s\u2500-\u257f]*>\s*$/m
|
|
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.
|
|
194
195
|
];
|
|
195
196
|
|
|
196
197
|
const FALLBACK_GENERATING: RegExp[] = [
|
|
197
|
-
/
|
|
198
|
-
/
|
|
199
|
-
/
|
|
200
|
-
/
|
|
201
|
-
/[\u2800-\u28ff]/, // Braille spinner blocks
|
|
198
|
+
/[\u2800-\u28ff]/, // Braille spinner blocks (universal TUI)
|
|
199
|
+
/esc to (cancel|interrupt|stop)/i, // Common TUI generation status line
|
|
200
|
+
/generating\.\.\./i,
|
|
201
|
+
/Claude is (?:thinking|processing|working)/i, // Specific Claude Code status
|
|
202
202
|
];
|
|
203
203
|
|
|
204
204
|
const FALLBACK_APPROVAL: RegExp[] = [
|
|
205
|
-
/
|
|
206
|
-
/
|
|
205
|
+
/Allow\s+once/i,
|
|
206
|
+
/Always\s+allow/i,
|
|
207
207
|
/\(y\/n\)/i,
|
|
208
208
|
/\[Y\/n\]/i,
|
|
209
|
-
/
|
|
210
|
-
/
|
|
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.
|
|
211
212
|
];
|
|
212
213
|
|
|
213
214
|
function defaultCleanOutput(raw: string, _lastUserInput?: string): string {
|
|
@@ -260,6 +261,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
260
261
|
// Approval cooldown
|
|
261
262
|
private lastApprovalResolvedAt: number = 0;
|
|
262
263
|
|
|
264
|
+
// Approval state machine
|
|
265
|
+
private approvalTransitionBuffer: string = '';
|
|
266
|
+
private approvalExitTimeout: NodeJS.Timeout | null = null;
|
|
267
|
+
|
|
268
|
+
// Resize redraw suppression
|
|
269
|
+
private resizeSuppressUntil: number = 0;
|
|
270
|
+
|
|
263
271
|
// Resolved timeouts (provider defaults + overrides)
|
|
264
272
|
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
|
|
265
273
|
|
|
@@ -394,6 +402,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
394
402
|
// ─── Output state machine ────────────────────────────
|
|
395
403
|
|
|
396
404
|
private handleOutput(rawData: string): void {
|
|
405
|
+
// Suppress output processing briefly after resize to avoid false triggers from screen redraws
|
|
406
|
+
if (Date.now() < this.resizeSuppressUntil) return;
|
|
407
|
+
|
|
397
408
|
const cleanData = stripAnsi(rawData);
|
|
398
409
|
const { patterns } = this.provider;
|
|
399
410
|
|
|
@@ -439,32 +450,57 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
439
450
|
if (hasApproval && this.currentStatus !== 'waiting_approval') {
|
|
440
451
|
if (this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown) return;
|
|
441
452
|
|
|
453
|
+
// Capture context before clearing (recentOutputBuffer still has content here)
|
|
454
|
+
const ctxLines = this.recentOutputBuffer.split('\n')
|
|
455
|
+
.map(l => l.trim())
|
|
456
|
+
.filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
|
|
442
457
|
this.isWaitingForResponse = true;
|
|
443
458
|
this.currentStatus = 'waiting_approval';
|
|
444
459
|
this.recentOutputBuffer = '';
|
|
445
|
-
|
|
460
|
+
this.approvalTransitionBuffer = '';
|
|
446
461
|
this.activeModal = {
|
|
447
462
|
message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
|
|
448
463
|
buttons: ['Allow once', 'Always allow', 'Deny'],
|
|
449
464
|
};
|
|
450
465
|
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
466
|
+
// Safety timeout — if stuck in waiting_approval, auto-exit after 60s
|
|
467
|
+
if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
|
|
468
|
+
this.approvalExitTimeout = setTimeout(() => {
|
|
469
|
+
if (this.currentStatus === 'waiting_approval') {
|
|
470
|
+
LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-exiting waiting_approval`);
|
|
471
|
+
this.activeModal = null;
|
|
472
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
473
|
+
this.recentOutputBuffer = '';
|
|
474
|
+
this.approvalTransitionBuffer = '';
|
|
475
|
+
this.approvalExitTimeout = null;
|
|
476
|
+
this.currentStatus = this.isWaitingForResponse ? 'generating' : 'idle';
|
|
477
|
+
this.onStatusChange?.();
|
|
478
|
+
}
|
|
479
|
+
}, 60000);
|
|
451
480
|
this.onStatusChange?.();
|
|
452
481
|
return;
|
|
453
482
|
}
|
|
454
483
|
|
|
455
484
|
// ─── Phase 3: Approval release
|
|
485
|
+
// Accumulate chunks into approvalTransitionBuffer — the approval dialog clears via ANSI
|
|
486
|
+
// sequences that strip to nothing, so we can't rely on a single cleanData chunk matching.
|
|
456
487
|
if (this.currentStatus === 'waiting_approval') {
|
|
457
|
-
|
|
458
|
-
const
|
|
488
|
+
this.approvalTransitionBuffer = (this.approvalTransitionBuffer + cleanData).slice(-500);
|
|
489
|
+
const genResume = patterns.generating.some(p => p.test(this.approvalTransitionBuffer));
|
|
490
|
+
const promptResume = patterns.prompt.some(p => p.test(this.approvalTransitionBuffer));
|
|
459
491
|
if (genResume) {
|
|
492
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
460
493
|
this.currentStatus = 'generating';
|
|
461
494
|
this.activeModal = null;
|
|
462
495
|
this.recentOutputBuffer = '';
|
|
496
|
+
this.approvalTransitionBuffer = '';
|
|
463
497
|
this.lastApprovalResolvedAt = Date.now();
|
|
464
498
|
this.onStatusChange?.();
|
|
465
499
|
} else if (promptResume) {
|
|
500
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
466
501
|
this.activeModal = null;
|
|
467
502
|
this.recentOutputBuffer = '';
|
|
503
|
+
this.approvalTransitionBuffer = '';
|
|
468
504
|
this.lastApprovalResolvedAt = Date.now();
|
|
469
505
|
this.finishResponse();
|
|
470
506
|
}
|
|
@@ -497,7 +533,11 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
497
533
|
}
|
|
498
534
|
|
|
499
535
|
// Prompt → response complete
|
|
500
|
-
|
|
536
|
+
// Only check the LAST 2 lines of cleanData — the prompt appears at the very end of the
|
|
537
|
+
// output stream. Checking the full chunk causes false positives when response content
|
|
538
|
+
// has '>' in code blocks, shell examples, or mid-response lines.
|
|
539
|
+
const trailingLines = cleanData.split('\n').slice(-2).join('\n');
|
|
540
|
+
if (patterns.prompt.some(p => p.test(trailingLines))) {
|
|
501
541
|
this.finishResponse();
|
|
502
542
|
} else {
|
|
503
543
|
this.idleTimeout = setTimeout(() => {
|
|
@@ -513,6 +553,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
513
553
|
private finishResponse(): void {
|
|
514
554
|
if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
|
|
515
555
|
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
556
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
516
557
|
|
|
517
558
|
const lastUserText = this.messages.filter(m => m.role === 'user').pop()?.content;
|
|
518
559
|
let response = this.provider.cleanOutput(this.responseBuffer, lastUserText);
|
|
@@ -576,6 +617,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
576
617
|
cancel(): void { this.shutdown(); }
|
|
577
618
|
|
|
578
619
|
shutdown(): void {
|
|
620
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
579
621
|
if (this.ptyProcess) {
|
|
580
622
|
this.ptyProcess.write('\x03');
|
|
581
623
|
setTimeout(() => {
|
|
@@ -600,9 +642,26 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
600
642
|
this.ptyProcess?.write(data);
|
|
601
643
|
}
|
|
602
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Resolve an approval modal by navigating to the button at `buttonIndex` and pressing Enter.
|
|
647
|
+
* Index 0 = first option (already selected by default — just Enter).
|
|
648
|
+
* Index N = press Arrow Down N times, then Enter.
|
|
649
|
+
*/
|
|
650
|
+
resolveModal(buttonIndex: number): void {
|
|
651
|
+
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);
|
|
655
|
+
}
|
|
656
|
+
|
|
603
657
|
resize(cols: number, rows: number): void {
|
|
604
658
|
if (this.ptyProcess) {
|
|
605
|
-
try {
|
|
659
|
+
try {
|
|
660
|
+
this.ptyProcess.resize(cols, rows);
|
|
661
|
+
// Suppress output for 300ms after resize — PTY redraws the screen and
|
|
662
|
+
// the redrawn content (spinners, status text) would falsely trigger generating detection
|
|
663
|
+
this.resizeSuppressUntil = Date.now() + 300;
|
|
664
|
+
} catch { }
|
|
606
665
|
}
|
|
607
666
|
}
|
|
608
667
|
}
|
|
@@ -601,6 +601,42 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
|
|
|
601
601
|
|
|
602
602
|
LOG.info('Command', `[resolveAction] action=${action} button="${button}" provider=${provider?.type}`);
|
|
603
603
|
|
|
604
|
+
// 0. CLI / ACP category: navigate approval dialog via PTY arrow keys + Enter
|
|
605
|
+
if (provider?.category === 'cli') {
|
|
606
|
+
const adapter = h.getCliAdapter(provider.type);
|
|
607
|
+
if (!adapter) return { success: false, error: 'CLI adapter not running' };
|
|
608
|
+
const status = (adapter as any).getStatus?.();
|
|
609
|
+
if (status?.status !== 'waiting_approval') {
|
|
610
|
+
return { success: false, error: 'Not in approval state' };
|
|
611
|
+
}
|
|
612
|
+
const buttons: string[] = status.activeModal?.buttons || ['Allow once', 'Always allow', 'Deny'];
|
|
613
|
+
// Resolve button index: explicit buttonIndex arg → button text match → action fallback
|
|
614
|
+
let buttonIndex = typeof args?.buttonIndex === 'number' ? args.buttonIndex : -1;
|
|
615
|
+
if (buttonIndex < 0) {
|
|
616
|
+
const btnLower = button.toLowerCase();
|
|
617
|
+
buttonIndex = buttons.findIndex(b => b.toLowerCase().includes(btnLower));
|
|
618
|
+
}
|
|
619
|
+
if (buttonIndex < 0) {
|
|
620
|
+
if (action === 'reject' || action === 'deny') {
|
|
621
|
+
buttonIndex = buttons.findIndex(b => /deny|reject|no/i.test(b));
|
|
622
|
+
if (buttonIndex < 0) buttonIndex = buttons.length - 1;
|
|
623
|
+
} else if (action === 'always' || /always/i.test(button)) {
|
|
624
|
+
buttonIndex = buttons.findIndex(b => /always/i.test(b));
|
|
625
|
+
if (buttonIndex < 0) buttonIndex = 1;
|
|
626
|
+
} else {
|
|
627
|
+
buttonIndex = 0; // approve → first option (default selected)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (typeof (adapter as any).resolveModal === 'function') {
|
|
631
|
+
(adapter as any).resolveModal(buttonIndex);
|
|
632
|
+
} else {
|
|
633
|
+
const keys = '\x1B[B'.repeat(Math.max(0, buttonIndex)) + '\r';
|
|
634
|
+
(adapter as any).writeRaw?.(keys);
|
|
635
|
+
}
|
|
636
|
+
LOG.info('Command', `[resolveAction] CLI PTY → buttonIndex=${buttonIndex} "${buttons[buttonIndex] ?? '?'}"`);
|
|
637
|
+
return { success: true, buttonIndex, button: buttons[buttonIndex] ?? button };
|
|
638
|
+
}
|
|
639
|
+
|
|
604
640
|
// 1. Extension: via AgentStreamManager
|
|
605
641
|
if (provider?.category === 'extension' && h.agentStream && h.getCdp()) {
|
|
606
642
|
const ok = await h.agentStream.resolveAgentAction(
|
|
@@ -13,6 +13,7 @@ import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
|
13
13
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
14
14
|
import { StatusMonitor } from './status-monitor.js';
|
|
15
15
|
import { ChatHistoryWriter } from '../config/chat-history.js';
|
|
16
|
+
import { LOG } from '../logging/logger.js';
|
|
16
17
|
|
|
17
18
|
export class CliProviderInstance implements ProviderInstance {
|
|
18
19
|
readonly type: string;
|
|
@@ -156,18 +157,22 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
156
157
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
157
158
|
|
|
158
159
|
if (newStatus !== this.lastStatus) {
|
|
160
|
+
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
159
161
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
160
162
|
this.generatingStartedAt = now;
|
|
161
163
|
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
|
|
162
164
|
} else if (newStatus === 'waiting_approval') {
|
|
163
165
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
166
|
+
const modal = adapterStatus.activeModal;
|
|
167
|
+
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
164
168
|
this.pushEvent({
|
|
165
169
|
event: 'agent:waiting_approval', chatTitle, timestamp: now,
|
|
166
|
-
modalMessage:
|
|
167
|
-
modalButtons:
|
|
170
|
+
modalMessage: modal?.message,
|
|
171
|
+
modalButtons: modal?.buttons,
|
|
168
172
|
});
|
|
169
173
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
170
174
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
175
|
+
LOG.info('CLI', `[${this.type}] completed in ${duration}s`);
|
|
171
176
|
this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
|
|
172
177
|
this.generatingStartedAt = 0;
|
|
173
178
|
} else if (newStatus === 'stopped') {
|