@adhdev/daemon-core 0.9.82-rc.137 → 0.9.82-rc.138
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/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +15 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +0 -1
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +1 -0
- package/dist/index.js +922 -328
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +922 -328
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/providers/contracts.d.ts +19 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/shared-types.d.ts +7 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +9 -0
- package/src/cli-adapters/cli-state-engine.ts +103 -6
- package/src/cli-adapters/provider-cli-adapter.ts +51 -5
- package/src/cli-adapters/provider-cli-parse.ts +3 -0
- package/src/cli-adapters/provider-cli-shared.ts +13 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +712 -381
- package/src/commands/router.ts +14 -2
- package/src/config/chat-history.ts +36 -13
- package/src/mesh/contracts.ts +329 -0
- package/src/providers/contracts.ts +19 -0
- package/src/providers/provider-loader.ts +21 -7
- package/src/providers/provider-schema.ts +10 -0
- package/src/providers/read-chat-contract.ts +74 -14
- package/src/providers/transcript-v2.ts +567 -0
- package/src/shared-types.ts +7 -0
- package/src/status/snapshot.ts +35 -11
- package/src/types.ts +5 -0
|
@@ -93,6 +93,7 @@ const MAX_FINISH_RETRIES = 2;
|
|
|
93
93
|
const FINISH_RETRY_DELAY_MS = 300;
|
|
94
94
|
const MAX_TRACE_ENTRIES = 250;
|
|
95
95
|
const APPROVAL_EXIT_TIMEOUT_MS = 60_000;
|
|
96
|
+
const IDLE_CONFIRMATION_GRACE_MS = 2_000;
|
|
96
97
|
|
|
97
98
|
// ─── Engine ────────────────────────────────────────────────────────────────
|
|
98
99
|
|
|
@@ -133,6 +134,21 @@ export class CliStateEngine {
|
|
|
133
134
|
// ── Idle candidate ───────────────────────────────
|
|
134
135
|
private idleFinishCandidate: IdleFinishCandidate | null = null;
|
|
135
136
|
|
|
137
|
+
// ── Idle confirmation grace ──────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* `finishResponse` produces the `generating → idle` transition that
|
|
140
|
+
* coordinators interpret as "task complete". Some providers (antigravity-
|
|
141
|
+
* cli observed in the wild) briefly paint a screen that looks like an
|
|
142
|
+
* idle prompt between tool result frames while still actively running,
|
|
143
|
+
* which fired `response_finished` and broke completion semantics.
|
|
144
|
+
* We defer the actual idle transition by IDLE_CONFIRMATION_GRACE_MS and
|
|
145
|
+
* cancel it if the scripted detection re-detects generating during that
|
|
146
|
+
* window — a true completion stays idle for many seconds, so a 2-second
|
|
147
|
+
* grace is sufficient to filter the paint blip.
|
|
148
|
+
*/
|
|
149
|
+
private pendingIdleFinishTimer: NodeJS.Timeout | null = null;
|
|
150
|
+
private pendingIdleFinishAt = 0;
|
|
151
|
+
|
|
136
152
|
// ── Status history (debug) ───────────────────────
|
|
137
153
|
private statusHistory: { status: string; at: number; trigger?: string }[] = [];
|
|
138
154
|
private traceEntries: CliTraceEntry[] = [];
|
|
@@ -333,6 +349,7 @@ export class CliStateEngine {
|
|
|
333
349
|
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
334
350
|
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
335
351
|
if (this.providerErrorRetryTimer) { clearTimeout(this.providerErrorRetryTimer); this.providerErrorRetryTimer = null; }
|
|
352
|
+
if (this.pendingIdleFinishTimer) { clearTimeout(this.pendingIdleFinishTimer); this.pendingIdleFinishTimer = null; this.pendingIdleFinishAt = 0; }
|
|
336
353
|
this.providerErrorRetryKey = '';
|
|
337
354
|
}
|
|
338
355
|
|
|
@@ -501,8 +518,19 @@ export class CliStateEngine {
|
|
|
501
518
|
`[${this.provider.type}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} status=${String(status || '')} parsedStatus=${String(parsedStatus || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify((lastParsedAssistant?.content || '').slice(0, 120)).slice(0, 160)} responseBuffer=${JSON.stringify((snap.responseBuffer || '').slice(0, 160)).slice(0, 220)}`
|
|
502
519
|
);
|
|
503
520
|
|
|
504
|
-
|
|
505
|
-
|
|
521
|
+
// recent_activity_hold protects an in-flight user turn from a false
|
|
522
|
+
// idle blip. It must NOT fire during startup — when the adapter has
|
|
523
|
+
// no currentTurnScope, there is no user turn to protect; the recent
|
|
524
|
+
// activity is just the CLI painting its welcome screen. Firing here
|
|
525
|
+
// produced the startup status flip the user reported
|
|
526
|
+
// (generating → idle → generating → idle within the first few seconds
|
|
527
|
+
// of claude-cli launch).
|
|
528
|
+
const shouldHoldGenerating = status === 'idle'
|
|
529
|
+
&& this.isWaitingForResponse
|
|
530
|
+
&& !!this.currentTurnScope
|
|
531
|
+
&& !modal
|
|
532
|
+
&& recentInteractiveActivity
|
|
533
|
+
&& !(parsedStatus === 'idle' && !!lastParsedAssistant);
|
|
506
534
|
|
|
507
535
|
if (shouldHoldGenerating) { this.applyHoldGenerating(ctx); return; }
|
|
508
536
|
if (status === 'error') {
|
|
@@ -588,20 +616,51 @@ export class CliStateEngine {
|
|
|
588
616
|
if (!inCooldown) {
|
|
589
617
|
if (!modal) {
|
|
590
618
|
LOG.warn('CLI', `[${this.provider.type}] detectStatus=waiting_approval but parseApproval returned null; ignoring`);
|
|
619
|
+
// (fix) If we previously surfaced waiting_approval but the
|
|
620
|
+
// modal extraction is now failing, do NOT keep the status
|
|
621
|
+
// pinned to waiting_approval forever — the dashboard would
|
|
622
|
+
// show a "waiting" badge with no buttons (activeModal=null)
|
|
623
|
+
// and the user perceives the agent as stuck. Drop the modal
|
|
624
|
+
// and fall back to generating so the rest of the run can
|
|
625
|
+
// settle normally; a future evaluate with a real modal will
|
|
626
|
+
// re-enter waiting_approval cleanly.
|
|
627
|
+
if (this.currentStatus === 'waiting_approval') {
|
|
628
|
+
this.activeModal = null;
|
|
629
|
+
this.setStatus('generating', 'approval_lost_modal');
|
|
630
|
+
this.callbacks.onStatusChange();
|
|
631
|
+
}
|
|
591
632
|
return;
|
|
592
633
|
}
|
|
593
634
|
this.isWaitingForResponse = true;
|
|
594
635
|
this.setStatus('waiting_approval', 'script_detect');
|
|
595
|
-
|
|
636
|
+
// (fix) Don't overwrite an already-captured modal with a fresh
|
|
637
|
+
// re-parse on every evaluate — Claude TUI redraws option labels
|
|
638
|
+
// partially between paints (e.g. "Yes, and don't ask again for ..."
|
|
639
|
+
// is wider than the row and ships with a different trailing
|
|
640
|
+
// string each paint), which made the dashboard flap the modal
|
|
641
|
+
// signature continuously. Keep the first stable modal whose
|
|
642
|
+
// button count matches the latest parse; only swap when the
|
|
643
|
+
// shape clearly changed (different number of buttons → different
|
|
644
|
+
// approval).
|
|
645
|
+
const prev = this.activeModal;
|
|
646
|
+
const prevBtnCount = Array.isArray(prev?.buttons) ? prev!.buttons.length : 0;
|
|
647
|
+
const nextBtnCount = Array.isArray(modal.buttons) ? modal.buttons.length : 0;
|
|
648
|
+
if (!prev || prevBtnCount !== nextBtnCount) {
|
|
649
|
+
this.activeModal = modal;
|
|
650
|
+
this.callbacks.onStatusChange();
|
|
651
|
+
}
|
|
596
652
|
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
597
653
|
this.armApprovalExitTimeout();
|
|
598
|
-
this.callbacks.onStatusChange();
|
|
599
654
|
}
|
|
600
655
|
}
|
|
601
656
|
|
|
602
657
|
private applyGenerating(ctx: SettledEvalContext): void {
|
|
603
658
|
const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
|
|
604
659
|
this.clearIdleFinishCandidate('generating');
|
|
660
|
+
// Cancel any pending grace-window idle transition. We have fresh
|
|
661
|
+
// evidence the provider is still generating; the previous
|
|
662
|
+
// finishResponse() was a paint blip, not a real completion.
|
|
663
|
+
this.cancelPendingIdleFinish('generating_signal_returned');
|
|
605
664
|
const snap = this.transport.getSnapshot();
|
|
606
665
|
const effectiveScreenText = snap.screenText || snap.accumulatedBuffer;
|
|
607
666
|
const noActiveTurn = !this.currentTurnScope;
|
|
@@ -762,11 +821,49 @@ export class CliStateEngine {
|
|
|
762
821
|
}
|
|
763
822
|
this.resetActiveTurnState();
|
|
764
823
|
this.callbacks.onTurnCompleted();
|
|
765
|
-
|
|
766
|
-
|
|
824
|
+
// Defer the actual `generating → idle` transition by a short grace
|
|
825
|
+
// window. If applyGenerating fires again before the grace expires —
|
|
826
|
+
// antigravity's tool-result paint blips do this — cancelPendingIdle
|
|
827
|
+
// Finish() drops the pending transition and we stay generating.
|
|
828
|
+
this.scheduleIdleFinish('response_finished');
|
|
767
829
|
this.transport.flushOutboundQueue();
|
|
768
830
|
}
|
|
769
831
|
|
|
832
|
+
private scheduleIdleFinish(reason: string): void {
|
|
833
|
+
// If we are already deferring, replace the schedule so the most
|
|
834
|
+
// recent finishResponse "wins". Reasons accumulate via the trigger.
|
|
835
|
+
if (this.pendingIdleFinishTimer) clearTimeout(this.pendingIdleFinishTimer);
|
|
836
|
+
this.pendingIdleFinishAt = Date.now() + IDLE_CONFIRMATION_GRACE_MS;
|
|
837
|
+
this.pendingIdleFinishTimer = setTimeout(() => {
|
|
838
|
+
this.pendingIdleFinishTimer = null;
|
|
839
|
+
this.pendingIdleFinishAt = 0;
|
|
840
|
+
// If a new user turn started during the grace window, we owe
|
|
841
|
+
// generating semantics to that turn — do not retroactively idle.
|
|
842
|
+
if (this.isWaitingForResponse) return;
|
|
843
|
+
// The timer firing without a cancelPendingIdleFinish call means
|
|
844
|
+
// no fresh applyGenerating happened during the grace window;
|
|
845
|
+
// the previous fake-blip is over. Commit the idle.
|
|
846
|
+
//
|
|
847
|
+
// Note: the previous "currentStatus === 'generating' → return"
|
|
848
|
+
// guard caused stuck-generating sessions — finishResponse leaves
|
|
849
|
+
// currentStatus='generating' on purpose (so the dashboard keeps
|
|
850
|
+
// the spinner during the 2s grace), and then the timer fired
|
|
851
|
+
// without cancellation but refused to commit because the very
|
|
852
|
+
// status we are about to transition out of was still set.
|
|
853
|
+
// Re-checking currentStatus there meant idle was unreachable.
|
|
854
|
+
this.setStatus('idle', reason);
|
|
855
|
+
this.callbacks.onStatusChange();
|
|
856
|
+
}, IDLE_CONFIRMATION_GRACE_MS);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private cancelPendingIdleFinish(reason: string): void {
|
|
860
|
+
if (!this.pendingIdleFinishTimer) return;
|
|
861
|
+
clearTimeout(this.pendingIdleFinishTimer);
|
|
862
|
+
this.pendingIdleFinishTimer = null;
|
|
863
|
+
this.pendingIdleFinishAt = 0;
|
|
864
|
+
this.recordTrace('idle_finish_cancelled', { trigger: reason });
|
|
865
|
+
}
|
|
866
|
+
|
|
770
867
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
771
868
|
|
|
772
869
|
private armApprovalExitTimeout(): void {
|
|
@@ -170,9 +170,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
170
170
|
// Resize redraw suppression
|
|
171
171
|
private resizeSuppressUntil: number = 0;
|
|
172
172
|
|
|
173
|
-
// Native transcript anchor
|
|
174
|
-
//
|
|
175
|
-
|
|
173
|
+
// (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
|
|
174
|
+
// ChatSourceMachine holds the lock by state, not by a mutable field on
|
|
175
|
+
// the adapter. Removed entirely; no callers remain after the readChat
|
|
176
|
+
// ladder was replaced.
|
|
176
177
|
|
|
177
178
|
// ─── Script runner (parsing isolated here, adapter stays as transport) ───
|
|
178
179
|
private readonly runner: CliScriptRunner;
|
|
@@ -666,6 +667,14 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
666
667
|
this.engine.lastApprovalResolvedAt = Date.now();
|
|
667
668
|
}
|
|
668
669
|
this.engine.activeModal = null;
|
|
670
|
+
// Clear the in-flight turn flag at the same time we declare
|
|
671
|
+
// startup-idle. Otherwise the next settled evaluation sees
|
|
672
|
+
// isWaitingForResponse=true + recent CLI welcome-screen paints
|
|
673
|
+
// and flips us right back to generating via the hold path
|
|
674
|
+
// (the "startup → generating → idle → generating → idle"
|
|
675
|
+
// flicker the user observed on claude-cli launch).
|
|
676
|
+
this.engine.isWaitingForResponse = false;
|
|
677
|
+
this.engine.currentTurnScope = null;
|
|
669
678
|
this.engine.setStatus('idle', `startup_ready:${trigger}`);
|
|
670
679
|
}
|
|
671
680
|
LOG.info(
|
|
@@ -760,6 +769,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
760
769
|
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
761
770
|
scope: this.engine.currentTurnScope,
|
|
762
771
|
runtimeSettings: this.runtimeSettings,
|
|
772
|
+
spawnAt: this.spawnAt,
|
|
763
773
|
});
|
|
764
774
|
const session = this.runner.parseSession({
|
|
765
775
|
...input,
|
|
@@ -801,7 +811,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
801
811
|
const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
|
|
802
812
|
? parsed.providerSessionId.trim()
|
|
803
813
|
: '';
|
|
804
|
-
if (providerSessionId) {
|
|
814
|
+
if (providerSessionId && providerSessionId !== this.providerSessionId) {
|
|
805
815
|
this.providerSessionId = providerSessionId;
|
|
806
816
|
this.updateRuntimeMeta({ providerSessionId });
|
|
807
817
|
}
|
|
@@ -824,7 +834,42 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
824
834
|
: null;
|
|
825
835
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
826
836
|
let effectiveModal = startupModal || this.engine.activeModal;
|
|
827
|
-
|
|
837
|
+
// (fix) When we have no captured modal yet, take one more live attempt
|
|
838
|
+
// with the current screen text — the engine's settle pass can miss
|
|
839
|
+
// the modal when it happens to fire exactly between writes, and
|
|
840
|
+
// without a modal here the dashboard could never show the buttons.
|
|
841
|
+
// We deliberately do NOT overwrite an existing engine.activeModal so
|
|
842
|
+
// a stable matched modal wins. This runs even outside the startup
|
|
843
|
+
// gate because Claude's approval frames can appear long after launch.
|
|
844
|
+
if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
|
|
845
|
+
const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
|
|
846
|
+
if (liveDetect === 'waiting_approval') {
|
|
847
|
+
const liveModal = this.runParseApproval(this.terminalScreen.getText())
|
|
848
|
+
|| this.runParseApproval(this.recentOutputBuffer);
|
|
849
|
+
if (liveModal) {
|
|
850
|
+
effectiveModal = liveModal;
|
|
851
|
+
// Promote so subsequent calls don't re-walk the buffer.
|
|
852
|
+
// Only set if engine hasn't already captured one — keeps
|
|
853
|
+
// the first stable modal as authoritative.
|
|
854
|
+
if (!this.engine.activeModal) this.engine.activeModal = liveModal;
|
|
855
|
+
} else {
|
|
856
|
+
LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: detect=waiting_approval but parseApproval still null (recentLen=${this.recentOutputBuffer.length} screenLen=${this.terminalScreen.getText().length})`);
|
|
857
|
+
}
|
|
858
|
+
} else if (liveDetect && liveDetect !== 'generating' && liveDetect !== 'idle') {
|
|
859
|
+
LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: detect=${liveDetect} (not waiting_approval)`);
|
|
860
|
+
} else if (this.engine.currentStatus === 'waiting_approval' && liveDetect !== 'waiting_approval') {
|
|
861
|
+
LOG.warn('CLI', `[${this.cliType}] getStatus live re-extract: engine.status=waiting_approval but live detect=${liveDetect}`);
|
|
862
|
+
}
|
|
863
|
+
} else if (!effectiveModal && this.engine.currentStatus === 'waiting_approval') {
|
|
864
|
+
LOG.warn('CLI', `[${this.cliType}] getStatus skipped live re-extract: allowParse=${allowParse} isWaitingForResponse=${this.engine.isWaitingForResponse}`);
|
|
865
|
+
}
|
|
866
|
+
// Only surface waiting_approval when we ALSO have a concrete modal
|
|
867
|
+
// (message + buttons). detectStatus alone can fire while parseApproval
|
|
868
|
+
// is still null — the engine logs "detectStatus=waiting_approval but
|
|
869
|
+
// parseApproval returned null; ignoring". Without this guard getStatus
|
|
870
|
+
// was shipping a bare waiting_approval with activeModal=null and the
|
|
871
|
+
// user perceived the flow as broken.
|
|
872
|
+
if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
|
|
828
873
|
effectiveStatus = 'waiting_approval';
|
|
829
874
|
} else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
|
|
830
875
|
effectiveStatus = 'idle';
|
|
@@ -964,6 +1009,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
964
1009
|
isWaitingForResponse: this.engine.isWaitingForResponse,
|
|
965
1010
|
scope: this.engine.currentTurnScope,
|
|
966
1011
|
runtimeSettings: this.runtimeSettings,
|
|
1012
|
+
spawnAt: this.spawnAt,
|
|
967
1013
|
});
|
|
968
1014
|
return await Promise.resolve(this.runner.invokeByName(scriptName, {
|
|
969
1015
|
...input,
|
|
@@ -43,6 +43,7 @@ export function buildCliParseInput(options: {
|
|
|
43
43
|
isWaitingForResponse?: boolean;
|
|
44
44
|
scope?: TurnParseScope | null;
|
|
45
45
|
runtimeSettings: Record<string, any>;
|
|
46
|
+
spawnAt?: number;
|
|
46
47
|
}): CliScriptInput {
|
|
47
48
|
const {
|
|
48
49
|
accumulatedBuffer,
|
|
@@ -57,6 +58,7 @@ export function buildCliParseInput(options: {
|
|
|
57
58
|
isWaitingForResponse,
|
|
58
59
|
scope,
|
|
59
60
|
runtimeSettings,
|
|
61
|
+
spawnAt,
|
|
60
62
|
} = options;
|
|
61
63
|
const buffer = scope
|
|
62
64
|
? sliceFromOffset(accumulatedBuffer, scope.bufferStart)
|
|
@@ -84,6 +86,7 @@ export function buildCliParseInput(options: {
|
|
|
84
86
|
isWaitingForResponse,
|
|
85
87
|
promptText: scope?.prompt || '',
|
|
86
88
|
settings: { ...runtimeSettings },
|
|
89
|
+
...(typeof spawnAt === 'number' && spawnAt > 0 ? { spawnAt } : {}),
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -119,6 +119,7 @@ export interface CliScriptInput {
|
|
|
119
119
|
promptText?: string;
|
|
120
120
|
settings?: Record<string, any>;
|
|
121
121
|
args?: Record<string, any>;
|
|
122
|
+
spawnAt?: number;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
export interface CliStatusInput {
|
|
@@ -362,7 +363,18 @@ export class TerminalTranscriptAccumulator {
|
|
|
362
363
|
this.ensureRow();
|
|
363
364
|
if (final === 'A') this.row = Math.max(0, this.row - count);
|
|
364
365
|
else if (final === 'B') this.row += count;
|
|
365
|
-
else if (final === 'C')
|
|
366
|
+
else if (final === 'C') {
|
|
367
|
+
// (fix) Cursor-forward must materialize spaces in the cells it
|
|
368
|
+
// skips, otherwise rendered transcripts collapse "Do you" written
|
|
369
|
+
// as "Do\x1b[1Cyou" into "Doyou". That mis-rendering broke
|
|
370
|
+
// Claude Code's approval-prompt and prompt-line detection
|
|
371
|
+
// (parseApproval saw "Doyouwanttoproceed?" and returned null).
|
|
372
|
+
const line = this.lines[this.row];
|
|
373
|
+
for (let c = this.col; c < this.col + count; c += 1) {
|
|
374
|
+
if (line[c] === undefined) line[c] = ' ';
|
|
375
|
+
}
|
|
376
|
+
this.col += count;
|
|
377
|
+
}
|
|
366
378
|
else if (final === 'D') this.col = Math.max(0, this.col - count);
|
|
367
379
|
else if (final === 'G') this.col = Math.max(0, count - 1);
|
|
368
380
|
else if (final === 'H' || final === 'f') {
|
|
@@ -121,7 +121,23 @@ export class GhosttyVtTerminalBackend implements TerminalViewportBackend {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
getText(): string {
|
|
124
|
-
|
|
124
|
+
// (fix) ghostty's `trim:true` mode strips trailing whitespace per row
|
|
125
|
+
// AND collapses cells that were touched only by cursor-forward (ESC[<n>C)
|
|
126
|
+
// without a printable glyph. Many TUIs (Claude Code most prominently)
|
|
127
|
+
// render inter-word spaces via CUF rather than literal spaces, so
|
|
128
|
+
// trim:true would smash "Do you want to proceed?" into
|
|
129
|
+
// "Doyouwanttoproceed?" and break every downstream regex
|
|
130
|
+
// (approval detection, prompt-line discovery, etc.). Keep the per-row
|
|
131
|
+
// padding, then trim each row's trailing whitespace ourselves so the
|
|
132
|
+
// serialized text matches what the user sees in the terminal.
|
|
133
|
+
const raw = this.terminal.formatPlainText({ trim: false }) || '';
|
|
134
|
+
if (!raw) return '';
|
|
135
|
+
const lines = raw.split('\n').map((row) => row.replace(/\s+$/, ''));
|
|
136
|
+
let first = 0;
|
|
137
|
+
let last = lines.length;
|
|
138
|
+
while (first < last && !lines[first]) first += 1;
|
|
139
|
+
while (last > first && !lines[last - 1]) last -= 1;
|
|
140
|
+
return lines.slice(first, last).join('\n');
|
|
125
141
|
}
|
|
126
142
|
|
|
127
143
|
getCursorPosition(): { col: number; row: number } {
|
|
@@ -64,7 +64,14 @@ export class XtermTerminalBackend implements TerminalViewportBackend {
|
|
|
64
64
|
|
|
65
65
|
for (let i = start; i < end; i++) {
|
|
66
66
|
const line = buffer.getLine(i);
|
|
67
|
-
|
|
67
|
+
// (fix) translateToString(true) strips trailing whitespace per row
|
|
68
|
+
// AND collapses cells touched only by cursor-forward (ESC[<n>C),
|
|
69
|
+
// so Claude Code's "Do you want to proceed?" arrives as
|
|
70
|
+
// "Doyouwanttoproceed?" — every downstream approval/prompt regex
|
|
71
|
+
// misses. Use false to preserve inter-word padding; we trim each
|
|
72
|
+
// row's trailing whitespace ourselves below.
|
|
73
|
+
const raw = line ? line.translateToString(false) : '';
|
|
74
|
+
lines.push(raw.replace(/\s+$/, ''));
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
let first = 0;
|