@adhdev/daemon-core 0.5.62 → 0.5.63
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 +11 -1
- package/dist/index.js +93 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/providers/_builtin/cli/claude-cli/provider.json +4 -1
- package/providers/_builtin/registry.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +103 -71
- package/src/providers/extension-provider-instance.ts +9 -18
package/package.json
CHANGED
|
@@ -87,8 +87,11 @@
|
|
|
87
87
|
{ "source": "\\(y/n\\)", "flags": "i" },
|
|
88
88
|
{ "source": "\\[Y/n\\]", "flags": "i" },
|
|
89
89
|
{ "source": "Run\\s+\\w+\\s+command", "flags": "i" },
|
|
90
|
+
{ "source": "Allow\\s*tool", "flags": "i" },
|
|
90
91
|
{ "source": "Yes,?\\s*don'?t\\s*ask", "flags": "i" },
|
|
91
|
-
{ "source": "
|
|
92
|
+
{ "source": "Deny", "flags": "i" },
|
|
93
|
+
{ "source": "Do you want to proceed", "flags": "i" },
|
|
94
|
+
{ "source": "Esc to cancel", "flags": "i" }
|
|
92
95
|
],
|
|
93
96
|
"ready": [
|
|
94
97
|
{ "source": "for\\s*shortcuts", "flags": "i" },
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* binary: string — binary name
|
|
12
12
|
* spawn: { command, args, shell, env }
|
|
13
13
|
* patterns: { prompt, generating, approval, ready }
|
|
14
|
-
* timeouts?: { idleFinish, generatingIdle, maxResponse, approvalCooldown, ... }
|
|
14
|
+
* timeouts?: { idleFinish, generatingIdle, maxResponse, approvalCooldown, outputSettle, ... }
|
|
15
15
|
* cleanOutput(raw, lastUserInput): string
|
|
16
16
|
*/
|
|
17
17
|
|
|
@@ -89,6 +89,8 @@ export interface CliProviderModule {
|
|
|
89
89
|
maxResponse?: number;
|
|
90
90
|
/** shutdown after kill wait (default 1000ms) */
|
|
91
91
|
shutdownGrace?: number;
|
|
92
|
+
/** Output settle debounce before evaluating idle/approval (default 300ms) */
|
|
93
|
+
outputSettle?: number;
|
|
92
94
|
};
|
|
93
95
|
cleanOutput(raw: string, lastUserInput?: string): string;
|
|
94
96
|
}
|
|
@@ -271,6 +273,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
271
273
|
private approvalTransitionBuffer: string = '';
|
|
272
274
|
private approvalExitTimeout: NodeJS.Timeout | null = null;
|
|
273
275
|
|
|
276
|
+
// Output settle debounce — fires after PTY output goes quiet
|
|
277
|
+
private settleTimer: NodeJS.Timeout | null = null;
|
|
278
|
+
private settledBuffer: string = ''; // snapshot of recentOutputBuffer at settle time
|
|
279
|
+
|
|
274
280
|
// Resize redraw suppression
|
|
275
281
|
private resizeSuppressUntil: number = 0;
|
|
276
282
|
|
|
@@ -310,6 +316,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
310
316
|
idleFinish: t.idleFinish ?? 5000,
|
|
311
317
|
maxResponse: t.maxResponse ?? 300000,
|
|
312
318
|
shutdownGrace: t.shutdownGrace ?? 1000,
|
|
319
|
+
outputSettle: t.outputSettle ?? 300,
|
|
313
320
|
};
|
|
314
321
|
|
|
315
322
|
// Load approval key mapping from provider (e.g. approvalKeys: {"0":"1","1":"2","2":"3"})
|
|
@@ -474,55 +481,69 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
474
481
|
return;
|
|
475
482
|
}
|
|
476
483
|
|
|
477
|
-
// ─── Phase 2: Approval detect
|
|
478
|
-
// DEBUG: log recent output for approval pattern debugging
|
|
479
484
|
if (cleanData.trim().length > 5) {
|
|
480
485
|
LOG.debug('CLI', `[${this.cliType}] output chunk (${cleanData.length}): ${cleanData.slice(0, 300).replace(/\n/g, '\\n')}`);
|
|
481
486
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (
|
|
486
|
-
|
|
487
|
-
const ctxLines = this.recentOutputBuffer.split('\n')
|
|
488
|
-
.map(l => l.trim())
|
|
489
|
-
.filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
|
|
487
|
+
|
|
488
|
+
// ─── Phase 2: generating — immediate detection (fast UX response)
|
|
489
|
+
if (!this.isWaitingForResponse) {
|
|
490
|
+
if (patterns.generating.some(p => p.test(cleanData))) {
|
|
491
|
+
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
490
492
|
this.isWaitingForResponse = true;
|
|
491
|
-
this.
|
|
492
|
-
this.
|
|
493
|
-
this.
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
this.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
this.lastApprovalResolvedAt = Date.now();
|
|
508
|
-
this.recentOutputBuffer = '';
|
|
509
|
-
this.approvalTransitionBuffer = '';
|
|
510
|
-
this.approvalExitTimeout = null;
|
|
511
|
-
this.setStatus(this.isWaitingForResponse ? 'generating' : 'idle', 'approval_cleared');
|
|
512
|
-
this.onStatusChange?.();
|
|
513
|
-
}
|
|
514
|
-
}, 60000);
|
|
493
|
+
this.responseBuffer = '';
|
|
494
|
+
this.setStatus('generating', 'autonomous_gen');
|
|
495
|
+
this.onStatusChange?.();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (this.isWaitingForResponse) {
|
|
500
|
+
this.responseBuffer += cleanData;
|
|
501
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
502
|
+
|
|
503
|
+
// ─── Phase 3: still generating
|
|
504
|
+
if (patterns.generating.some(p => p.test(cleanData))) {
|
|
505
|
+
this.setStatus('generating', 'still_generating');
|
|
506
|
+
this.idleTimeout = setTimeout(() => {
|
|
507
|
+
if (this.isWaitingForResponse) this.finishResponse();
|
|
508
|
+
}, this.timeouts.generatingIdle);
|
|
515
509
|
this.onStatusChange?.();
|
|
510
|
+
// Cancel any pending settle — we're clearly still generating
|
|
511
|
+
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
516
512
|
return;
|
|
517
513
|
}
|
|
518
|
-
// In cooldown — don't set approval, but DO continue to other phases
|
|
519
514
|
}
|
|
520
515
|
|
|
521
|
-
// ─── Phase
|
|
522
|
-
// Accumulate chunks into approvalTransitionBuffer — the approval dialog clears via ANSI
|
|
523
|
-
// sequences that strip to nothing, so we can't rely on a single cleanData chunk matching.
|
|
516
|
+
// ─── Phase 4: waiting_approval — accumulate transition buffer
|
|
524
517
|
if (this.currentStatus === 'waiting_approval') {
|
|
525
518
|
this.approvalTransitionBuffer = (this.approvalTransitionBuffer + cleanData).slice(-500);
|
|
519
|
+
// Schedule settle check — output may still be arriving (ANSI redraws etc)
|
|
520
|
+
this.scheduleSettle();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ─── Phase 5: settle debounce — schedule idle/approval evaluation
|
|
525
|
+
this.scheduleSettle();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Fired after output goes quiet for outputSettle ms.
|
|
530
|
+
* Evaluates the stabilised buffer for approval, prompt (idle), or timeout.
|
|
531
|
+
*/
|
|
532
|
+
private scheduleSettle(): void {
|
|
533
|
+
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
534
|
+
this.settleTimer = setTimeout(() => {
|
|
535
|
+
this.settleTimer = null;
|
|
536
|
+
this.settledBuffer = this.recentOutputBuffer;
|
|
537
|
+
this.evaluateSettled();
|
|
538
|
+
}, this.timeouts.outputSettle);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private evaluateSettled(): void {
|
|
542
|
+
const { patterns } = this.provider;
|
|
543
|
+
const buf = this.settledBuffer;
|
|
544
|
+
|
|
545
|
+
// ─── waiting_approval: check for transition out
|
|
546
|
+
if (this.currentStatus === 'waiting_approval') {
|
|
526
547
|
const genResume = patterns.generating.some(p => p.test(this.approvalTransitionBuffer));
|
|
527
548
|
const promptResume = patterns.prompt.some(p => p.test(this.approvalTransitionBuffer));
|
|
528
549
|
if (genResume) {
|
|
@@ -541,47 +562,56 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
541
562
|
this.lastApprovalResolvedAt = Date.now();
|
|
542
563
|
this.finishResponse();
|
|
543
564
|
}
|
|
565
|
+
// else: still waiting — approvalExitTimeout will handle the 60s safety case
|
|
544
566
|
return;
|
|
545
567
|
}
|
|
546
568
|
|
|
547
|
-
// ───
|
|
548
|
-
|
|
549
|
-
|
|
569
|
+
// ─── check for approval on stabilised buffer
|
|
570
|
+
const hasApproval = patterns.approval.some(p => p.test(buf));
|
|
571
|
+
if (hasApproval) {
|
|
572
|
+
const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
|
|
573
|
+
if (!inCooldown) {
|
|
574
|
+
const ctxLines = buf.split('\n')
|
|
575
|
+
.map(l => l.trim())
|
|
576
|
+
.filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
|
|
550
577
|
this.isWaitingForResponse = true;
|
|
551
|
-
this.
|
|
552
|
-
this.
|
|
578
|
+
this.setStatus('waiting_approval', 'approval_pattern');
|
|
579
|
+
this.recentOutputBuffer = '';
|
|
580
|
+
this.approvalTransitionBuffer = '';
|
|
581
|
+
this.activeModal = {
|
|
582
|
+
message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
|
|
583
|
+
buttons: this.cliType === 'claude-cli'
|
|
584
|
+
? ['Yes (y)', 'Always allow (a)', 'Deny (Esc)']
|
|
585
|
+
: ['Allow once', 'Always allow', 'Deny'],
|
|
586
|
+
};
|
|
587
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
588
|
+
if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
|
|
589
|
+
this.approvalExitTimeout = setTimeout(() => {
|
|
590
|
+
if (this.currentStatus === 'waiting_approval') {
|
|
591
|
+
LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-exiting waiting_approval`);
|
|
592
|
+
this.activeModal = null;
|
|
593
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
594
|
+
this.recentOutputBuffer = '';
|
|
595
|
+
this.approvalTransitionBuffer = '';
|
|
596
|
+
this.approvalExitTimeout = null;
|
|
597
|
+
this.setStatus(this.isWaitingForResponse ? 'generating' : 'idle', 'approval_cleared');
|
|
598
|
+
this.onStatusChange?.();
|
|
599
|
+
}
|
|
600
|
+
}, 60000);
|
|
553
601
|
this.onStatusChange?.();
|
|
602
|
+
return;
|
|
554
603
|
}
|
|
555
604
|
}
|
|
556
605
|
|
|
557
|
-
// ───
|
|
606
|
+
// ─── check for prompt (idle) on stabilised buffer
|
|
558
607
|
if (this.isWaitingForResponse) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (
|
|
564
|
-
|
|
565
|
-
this.idleTimeout
|
|
566
|
-
if (this.isWaitingForResponse) this.finishResponse();
|
|
567
|
-
}, this.timeouts.generatingIdle);
|
|
568
|
-
this.onStatusChange?.();
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Prompt → response complete
|
|
573
|
-
// Only check the LAST 2 lines of cleanData — the prompt appears at the very end of the
|
|
574
|
-
// output stream. Checking the full chunk causes false positives when response content
|
|
575
|
-
// has '>' in code blocks, shell examples, or mid-response lines.
|
|
576
|
-
const trailingLines = cleanData.split('\n').slice(-2).join('\n');
|
|
577
|
-
if (patterns.prompt.some(p => p.test(trailingLines))) {
|
|
578
|
-
// Guard: don't finishResponse if approval text is also present —
|
|
579
|
-
// Claude Code's approval UI can contain prompt-like characters
|
|
580
|
-
const hasApprovalHere = patterns.approval.some(p => p.test(this.recentOutputBuffer));
|
|
581
|
-
if (!hasApprovalHere) {
|
|
582
|
-
this.finishResponse();
|
|
583
|
-
}
|
|
584
|
-
} else {
|
|
608
|
+
// Only look at the trailing portion — prompt appears at the very end
|
|
609
|
+
const trailingLines = buf.split('\n').slice(-3).join('\n');
|
|
610
|
+
if (patterns.prompt.some(p => p.test(trailingLines)) && !hasApproval) {
|
|
611
|
+
this.finishResponse();
|
|
612
|
+
} else if (!patterns.generating.some(p => p.test(buf))) {
|
|
613
|
+
// Output has settled with no generating signal and no prompt — schedule idle finish
|
|
614
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
585
615
|
this.idleTimeout = setTimeout(() => {
|
|
586
616
|
if (this.isWaitingForResponse && this.responseBuffer.trim()) {
|
|
587
617
|
this.finishResponse();
|
|
@@ -659,6 +689,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
659
689
|
cancel(): void { this.shutdown(); }
|
|
660
690
|
|
|
661
691
|
shutdown(): void {
|
|
692
|
+
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
662
693
|
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
663
694
|
if (this.ptyProcess) {
|
|
664
695
|
this.ptyProcess.write('\x03');
|
|
@@ -730,6 +761,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
730
761
|
// Buffers
|
|
731
762
|
startupBuffer: this.startupBuffer.slice(-500),
|
|
732
763
|
recentOutputBuffer: this.recentOutputBuffer.slice(-500),
|
|
764
|
+
settledBuffer: this.settledBuffer.slice(-500),
|
|
733
765
|
responseBuffer: this.responseBuffer.slice(-500),
|
|
734
766
|
approvalTransitionBuffer: this.approvalTransitionBuffer.slice(-500),
|
|
735
767
|
// State
|
|
@@ -117,39 +117,30 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// ─── status transition detect ──────────────────────────────
|
|
120
|
+
// NOTE: Extension transitions are TRACKED but NOT emitted as events.
|
|
121
|
+
// The parent IdeProviderInstance already emits identical events
|
|
122
|
+
// (generating_started, generating_completed, waiting_approval)
|
|
123
|
+
// via its own detectAgentTransitions(). Emitting here would cause
|
|
124
|
+
// duplicate toasts with slightly different content.
|
|
120
125
|
|
|
121
|
-
private detectTransition(newStatus: string,
|
|
126
|
+
private detectTransition(newStatus: string, _data: any): void {
|
|
122
127
|
const now = Date.now();
|
|
123
128
|
const agentStatus = (newStatus === 'streaming' || newStatus === 'generating') ? 'generating'
|
|
124
129
|
: newStatus === 'waiting_approval' ? 'waiting_approval'
|
|
125
130
|
: 'idle';
|
|
126
131
|
|
|
127
132
|
if (agentStatus !== this.lastAgentStatus) {
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
// Track generating start time (for monitor elapsed calculation)
|
|
130
134
|
if (this.lastAgentStatus === 'idle' && agentStatus === 'generating') {
|
|
131
135
|
this.generatingStartedAt = now;
|
|
132
|
-
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
|
|
133
|
-
} else if (agentStatus === 'waiting_approval') {
|
|
134
|
-
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
135
|
-
const msg = data?.activeModal?.message || data?.modalMessage;
|
|
136
|
-
this.pushEvent({
|
|
137
|
-
event: 'agent:waiting_approval', chatTitle, timestamp: now,
|
|
138
|
-
ideType: this.ideType,
|
|
139
|
-
agentType: this.type,
|
|
140
|
-
modalMessage: msg,
|
|
141
|
-
modalButtons: data?.activeModal?.buttons || data?.modalButtons,
|
|
142
|
-
});
|
|
143
136
|
} else if (agentStatus === 'idle' && (this.lastAgentStatus === 'generating' || this.lastAgentStatus === 'waiting_approval')) {
|
|
144
|
-
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
145
|
-
this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
|
|
146
137
|
this.generatingStartedAt = 0;
|
|
147
138
|
}
|
|
148
|
-
|
|
139
|
+
// Do NOT pushEvent for transitions — parent IDE instance handles these
|
|
149
140
|
this.lastAgentStatus = agentStatus;
|
|
150
141
|
}
|
|
151
142
|
|
|
152
|
-
// Monitor check (cooldown based notification)
|
|
143
|
+
// Monitor check (cooldown based notification) — keep monitor events (long_generating etc)
|
|
153
144
|
const agentKey = `${this.type}:ext`;
|
|
154
145
|
const monitorEvents = this.monitor.check(agentKey, agentStatus, now);
|
|
155
146
|
for (const me of monitorEvents) {
|