@adhdev/daemon-core 0.9.82-rc.207 → 0.9.82-rc.209
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/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +1 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +1 -2
- package/dist/cli-adapters/terminal-screen.d.ts +2 -11
- package/dist/index.js +156 -212
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +141 -197
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/adapter.d.ts +11 -19
- package/dist/providers/spec/driver.d.ts +14 -0
- package/dist/providers/spec/schema.gen.d.ts +24 -1
- package/dist/providers/spec/types.d.ts +8 -1
- package/dist/shared-types-extra.d.ts +1 -3
- package/package.json +1 -3
- package/src/cli-adapters/provider-cli-adapter.ts +3 -2
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +11 -34
- package/src/cli-adapters/terminal-backends/types.ts +1 -3
- package/src/cli-adapters/terminal-screen.ts +11 -81
- package/src/commands/mesh-coordinator.ts +3 -2
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/providers/spec/adapter.ts +40 -78
- package/src/providers/spec/cli-adapter.ts +2 -0
- package/src/providers/spec/driver.ts +71 -7
- package/src/providers/spec/evaluator.ts +44 -14
- package/src/providers/spec/loader.ts +6 -1
- package/src/providers/spec/schema.gen.ts +17 -1
- package/src/providers/spec/types.ts +5 -1
- package/src/shared-types-extra.ts +1 -3
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -104
|
@@ -360,6 +360,8 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
360
360
|
idleHoldPending: this.driver.hasIdleHoldPending(),
|
|
361
361
|
lastBusyAt: this.driver.getLastBusyAt(),
|
|
362
362
|
specPath: this.driver.getSpecPath(),
|
|
363
|
+
cursorPosition: this.driver.getCursorPosition(),
|
|
364
|
+
completionIdleDebounce: this.driver.getCompletionIdleDebounceState(),
|
|
363
365
|
// Extended fields
|
|
364
366
|
name: this.cliName,
|
|
365
367
|
status: this.getStatus().status,
|
|
@@ -38,6 +38,7 @@ import * as os from 'node:os';
|
|
|
38
38
|
import * as path from 'node:path';
|
|
39
39
|
import { TerminalAdapter, type TerminalAdapterOpts } from './adapter.js';
|
|
40
40
|
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
41
|
+
import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
|
|
41
42
|
import { evaluate, type SpecEvaluation, type TraceEntry } from './evaluator.js';
|
|
42
43
|
import { loadSpec } from './loader.js';
|
|
43
44
|
import type { CliSpec, Control, DelegateTrigger } from './types.js';
|
|
@@ -142,8 +143,13 @@ export function matchesCompletionIdleRule(spec: CliSpec, ev: SpecEvaluation, scr
|
|
|
142
143
|
if (!haystack) return null;
|
|
143
144
|
try {
|
|
144
145
|
const regex = new RegExp(rule.regex, rule.flags || '');
|
|
145
|
-
const
|
|
146
|
-
|
|
146
|
+
const matched = regex.test(haystack);
|
|
147
|
+
// Return the regex pattern as the stable key rather than the match
|
|
148
|
+
// text. The match text often contains a live counter (e.g. "Compacted
|
|
149
|
+
// for 1m 6s") that changes every second, which would reset
|
|
150
|
+
// completionIdleFirstSeenAt on every PTY frame and prevent the hold
|
|
151
|
+
// window from ever expiring.
|
|
152
|
+
return matched ? rule.regex : null;
|
|
147
153
|
} catch {
|
|
148
154
|
return null;
|
|
149
155
|
}
|
|
@@ -211,6 +217,14 @@ export class SpecDriver {
|
|
|
211
217
|
* because the evaluator already moved past busy by the time the hold
|
|
212
218
|
* kicks in. */
|
|
213
219
|
private lastBusyState: SpecEvaluation['state'] | null = null;
|
|
220
|
+
/** Timestamp of the last time we entered a modal state (approval/picker or
|
|
221
|
+
* any non-busy non-idle state). Used to suppress brief busy blips that
|
|
222
|
+
* appear while the modal is still on screen — Claude Code streams body
|
|
223
|
+
* text that transiently shows a spinner even while an approval modal is
|
|
224
|
+
* visible, causing rapid approval→busy→approval flicker on the dashboard. */
|
|
225
|
+
private lastModalAt = 0;
|
|
226
|
+
/** The modal state snapshot held across busy blips. */
|
|
227
|
+
private lastModalState: SpecEvaluation['state'] | null = null;
|
|
214
228
|
private completionIdleFirstSeenAt = 0;
|
|
215
229
|
private completionIdleKey = '';
|
|
216
230
|
/** Timer that re-runs evaluate() once the hold window expires. Needed
|
|
@@ -353,6 +367,17 @@ export class SpecDriver {
|
|
|
353
367
|
getLastBusyAt(): number { return this.lastBusyAt; }
|
|
354
368
|
hasIdleHoldPending(): boolean { return this.idleHoldTimer !== null; }
|
|
355
369
|
getSpecPath(): string { return this.opts.specPath; }
|
|
370
|
+
getCompletionIdleDebounceState(): { active: boolean; ageMs: number; holdMs: number; forceAfterMs: number } | null {
|
|
371
|
+
if (!this.completionIdleKey || !this.completionIdleFirstSeenAt) return null;
|
|
372
|
+
const rule = this.spec.debounce?.completion_idle_after;
|
|
373
|
+
if (!rule) return null;
|
|
374
|
+
return {
|
|
375
|
+
active: true,
|
|
376
|
+
ageMs: Date.now() - this.completionIdleFirstSeenAt,
|
|
377
|
+
holdMs: rule.hold_ms ?? 0,
|
|
378
|
+
forceAfterMs: typeof rule.force_after_ms === 'number' ? rule.force_after_ms : 0,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
356
381
|
getScreen(): string { return this.adapter.snapshot(); }
|
|
357
382
|
getSections(): Array<{ id: string; text: string }> | null {
|
|
358
383
|
try {
|
|
@@ -380,8 +405,8 @@ export class SpecDriver {
|
|
|
380
405
|
args: [...baseArgs, ...extra],
|
|
381
406
|
cwd: this.opts.workingDir,
|
|
382
407
|
env: { ...(this.spec.env ?? {}), ...(this.opts.extraEnv ?? {}) },
|
|
383
|
-
cols: this.opts.cols ??
|
|
384
|
-
rows: this.opts.rows ??
|
|
408
|
+
cols: this.opts.cols ?? DEFAULT_SESSION_HOST_COLS,
|
|
409
|
+
rows: this.opts.rows ?? DEFAULT_SESSION_HOST_ROWS,
|
|
385
410
|
transportFactory: this.opts.transportFactory,
|
|
386
411
|
};
|
|
387
412
|
}
|
|
@@ -454,6 +479,24 @@ export class SpecDriver {
|
|
|
454
479
|
evState = this.lastBusyState ?? evState;
|
|
455
480
|
}
|
|
456
481
|
}
|
|
482
|
+
// Modal hold: when in a modal state (approval, picker, etc.) a brief
|
|
483
|
+
// busy reading should not interrupt the modal. Claude Code streams
|
|
484
|
+
// body content while the approval modal is visible, causing a spinner
|
|
485
|
+
// to appear transiently — without this hold, the dashboard sees a
|
|
486
|
+
// rapid modal→busy→modal flicker and the approval UI disappears and
|
|
487
|
+
// reappears every few seconds. Apply the same busy_hold_ms window:
|
|
488
|
+
// if the modal was entered recently and the evaluator now returns
|
|
489
|
+
// busy, stay in the modal state until the hold expires or a non-busy
|
|
490
|
+
// non-modal reading arrives.
|
|
491
|
+
const idleStateId = this.spec.default_state ?? 'idle';
|
|
492
|
+
const isModalState = (id: string | null) =>
|
|
493
|
+
id !== null && id !== 'busy' && id !== idleStateId;
|
|
494
|
+
if (isModalState(this.currentStateId) && evState.id === 'busy') {
|
|
495
|
+
const ageMs = Date.now() - this.lastModalAt;
|
|
496
|
+
if (ageMs < busyHoldMs && this.lastModalState) {
|
|
497
|
+
evState = this.lastModalState;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
457
500
|
const completionIdleRule = this.spec.debounce?.completion_idle_after;
|
|
458
501
|
let busyWakeMs = busyHoldMs;
|
|
459
502
|
if (evState.id === 'busy' && completionIdleRule) {
|
|
@@ -500,6 +543,17 @@ export class SpecDriver {
|
|
|
500
543
|
this.lastBusyState = evState;
|
|
501
544
|
// Cancel any pending idle commit — non-idle reading invalidates it.
|
|
502
545
|
this.cancelIdleHold();
|
|
546
|
+
// Reset completion_idle_after tracking only when the completion
|
|
547
|
+
// marker is NOT present. If the marker is on screen (key is set
|
|
548
|
+
// by the block above), clearing it here would restart the hold
|
|
549
|
+
// window on every PTY frame and the hold would never expire.
|
|
550
|
+
// Only reset when the marker disappeared (key is empty), meaning
|
|
551
|
+
// a new tool-output burst arrived that pushed the marker off
|
|
552
|
+
// screen — in that case the old firstSeenAt is stale and should
|
|
553
|
+
// restart when the marker reappears.
|
|
554
|
+
if (!this.completionIdleKey) {
|
|
555
|
+
this.completionIdleFirstSeenAt = 0;
|
|
556
|
+
}
|
|
503
557
|
// Schedule a re-evaluation when the hold window expires. PTYs
|
|
504
558
|
// typically stop emitting once the agent stops printing (the
|
|
505
559
|
// footer settles), so without an explicit timer the driver
|
|
@@ -512,6 +566,16 @@ export class SpecDriver {
|
|
|
512
566
|
if (evState.id !== (this.spec.default_state ?? 'idle')) {
|
|
513
567
|
this.cancelIdleHold();
|
|
514
568
|
}
|
|
569
|
+
// Track the modal entry timestamp so the modal-hold above can
|
|
570
|
+
// suppress brief busy blips while the modal is still on screen.
|
|
571
|
+
if (isModalState(evState.id)) {
|
|
572
|
+
this.lastModalAt = Date.now();
|
|
573
|
+
this.lastModalState = evState;
|
|
574
|
+
} else {
|
|
575
|
+
// Leaving modal territory (going to idle) — clear the hold.
|
|
576
|
+
this.lastModalAt = 0;
|
|
577
|
+
this.lastModalState = null;
|
|
578
|
+
}
|
|
515
579
|
}
|
|
516
580
|
|
|
517
581
|
// Idle hold: if idle_hold_ms is set, don't commit idle immediately.
|
|
@@ -813,8 +877,8 @@ function guessExt(mime: string): string {
|
|
|
813
877
|
|
|
814
878
|
function extractMatchedRules(ev: SpecEvaluation): string[] {
|
|
815
879
|
if (!Array.isArray(ev.trace)) return [];
|
|
816
|
-
return
|
|
817
|
-
.filter(t => t.
|
|
818
|
-
.map(t => t.
|
|
880
|
+
return ev.trace
|
|
881
|
+
.filter(t => t.kind === 'state_match')
|
|
882
|
+
.map(t => t.text)
|
|
819
883
|
.filter(Boolean);
|
|
820
884
|
}
|
|
@@ -212,18 +212,15 @@ function matchState(
|
|
|
212
212
|
return { matched: true, title };
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
): ModalSnapshot | null {
|
|
222
|
-
if (!state.modal_buttons) return null;
|
|
223
|
-
const hay = sectionText(sections, state.modal_buttons.section, fullScreen);
|
|
215
|
+
function extractButtonsWithPattern(
|
|
216
|
+
rule: { pattern: string; flags?: string },
|
|
217
|
+
hay: string,
|
|
218
|
+
keyTemplate: string,
|
|
219
|
+
continuationLines: boolean,
|
|
220
|
+
): { index: number; label: string; key: string }[] {
|
|
224
221
|
const buttons: { index: number; label: string; key: string }[] = [];
|
|
225
|
-
if (
|
|
226
|
-
const re = compileLinePattern(
|
|
222
|
+
if (continuationLines) {
|
|
223
|
+
const re = compileLinePattern(rule);
|
|
227
224
|
const lines = hay.split('\n');
|
|
228
225
|
for (let i = 0; i < lines.length; i += 1) {
|
|
229
226
|
const m = re.exec(lines[i]);
|
|
@@ -241,24 +238,57 @@ function extractModal(
|
|
|
241
238
|
j += 1;
|
|
242
239
|
}
|
|
243
240
|
if (buttons.some(b => b.index === idx)) continue;
|
|
244
|
-
const key =
|
|
241
|
+
const key = keyTemplate.replace(/\{index\}/g, String(idx));
|
|
245
242
|
buttons.push({ index: idx, label, key });
|
|
246
243
|
i = j - 1;
|
|
247
244
|
}
|
|
248
245
|
} else {
|
|
249
|
-
const re = compilePattern(
|
|
246
|
+
const re = compilePattern(rule);
|
|
250
247
|
let m: RegExpExecArray | null;
|
|
251
248
|
while ((m = re.exec(hay)) !== null) {
|
|
252
249
|
const idx = Number(m[1]);
|
|
253
250
|
const label = String(m[2] ?? '').trim();
|
|
254
251
|
if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
|
|
255
252
|
if (buttons.some(b => b.index === idx)) continue;
|
|
256
|
-
const key =
|
|
253
|
+
const key = keyTemplate.replace(/\{index\}/g, String(idx));
|
|
257
254
|
buttons.push({ index: idx, label, key });
|
|
258
255
|
}
|
|
259
256
|
}
|
|
260
257
|
buttons.sort((a, b) => a.index - b.index);
|
|
258
|
+
return buttons;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function extractModal(
|
|
262
|
+
state: SpecState,
|
|
263
|
+
sections: ResolvedSection[],
|
|
264
|
+
fullScreen: string,
|
|
265
|
+
title: string | null,
|
|
266
|
+
trace: TraceEntry[],
|
|
267
|
+
): ModalSnapshot | null {
|
|
268
|
+
if (!state.modal_buttons) return null;
|
|
269
|
+
const hay = sectionText(sections, state.modal_buttons.section, fullScreen);
|
|
261
270
|
const minCount = state.modal_buttons.min_count ?? 2;
|
|
271
|
+
const keyTemplate = state.modal_buttons.key_for_index;
|
|
272
|
+
const continuationLines = state.modal_buttons.continuation_lines ?? false;
|
|
273
|
+
|
|
274
|
+
// Build ordered list of pattern candidates: `patterns` array takes precedence,
|
|
275
|
+
// falling back to the single `pattern` field.
|
|
276
|
+
const candidates: Array<{ pattern: string; flags?: string }> =
|
|
277
|
+
state.modal_buttons.patterns?.length
|
|
278
|
+
? state.modal_buttons.patterns
|
|
279
|
+
: state.modal_buttons.pattern
|
|
280
|
+
? [{ pattern: state.modal_buttons.pattern, flags: state.modal_buttons.flags }]
|
|
281
|
+
: [];
|
|
282
|
+
|
|
283
|
+
let buttons: { index: number; label: string; key: string }[] = [];
|
|
284
|
+
for (const candidate of candidates) {
|
|
285
|
+
const result = extractButtonsWithPattern(candidate, hay, keyTemplate, continuationLines);
|
|
286
|
+
if (result.length >= minCount) {
|
|
287
|
+
buttons = result;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
262
292
|
if (buttons.length < minCount) {
|
|
263
293
|
trace.push({ kind: 'modal', text: `modal_buttons matched ${buttons.length}/${minCount} required — discarded` });
|
|
264
294
|
return null;
|
|
@@ -77,7 +77,12 @@ function validateRefs(spec: CliSpec): string[] {
|
|
|
77
77
|
if (s.modal_buttons.section && !sectionIds.has(s.modal_buttons.section)) {
|
|
78
78
|
errs.push(`states[${s.id}].modal_buttons.section "${s.modal_buttons.section}" unknown`);
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
if (s.modal_buttons.pattern) {
|
|
81
|
+
compileRegex(s.modal_buttons.pattern, s.modal_buttons.flags ?? 'm', `states[${s.id}].modal_buttons.pattern`, errs);
|
|
82
|
+
}
|
|
83
|
+
for (const [pi, p] of (s.modal_buttons.patterns ?? []).entries()) {
|
|
84
|
+
compileRegex(p.pattern, p.flags ?? 'm', `states[${s.id}].modal_buttons.patterns[${pi}]`, errs);
|
|
85
|
+
}
|
|
81
86
|
}
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -295,9 +295,12 @@ export const SCHEMA = {
|
|
|
295
295
|
"type": "object",
|
|
296
296
|
"additionalProperties": false,
|
|
297
297
|
"required": [
|
|
298
|
-
"pattern",
|
|
299
298
|
"key_for_index"
|
|
300
299
|
],
|
|
300
|
+
"oneOf": [
|
|
301
|
+
{ "required": ["pattern"] },
|
|
302
|
+
{ "required": ["patterns"] }
|
|
303
|
+
],
|
|
301
304
|
"properties": {
|
|
302
305
|
"section": {
|
|
303
306
|
"type": "string"
|
|
@@ -309,6 +312,19 @@ export const SCHEMA = {
|
|
|
309
312
|
"flags": {
|
|
310
313
|
"type": "string"
|
|
311
314
|
},
|
|
315
|
+
"patterns": {
|
|
316
|
+
"type": "array",
|
|
317
|
+
"minItems": 1,
|
|
318
|
+
"items": {
|
|
319
|
+
"type": "object",
|
|
320
|
+
"additionalProperties": false,
|
|
321
|
+
"required": ["pattern"],
|
|
322
|
+
"properties": {
|
|
323
|
+
"pattern": { "type": "string", "minLength": 1 },
|
|
324
|
+
"flags": { "type": "string" }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
},
|
|
312
328
|
"key_for_index": {
|
|
313
329
|
"type": "string",
|
|
314
330
|
"minLength": 1
|
|
@@ -71,8 +71,12 @@ export interface SectionPattern {
|
|
|
71
71
|
|
|
72
72
|
export interface ModalButtonsRule {
|
|
73
73
|
section?: string;
|
|
74
|
-
pattern
|
|
74
|
+
/** Single pattern (original). Use `patterns` for multi-format fallback. */
|
|
75
|
+
pattern?: string;
|
|
75
76
|
flags?: string;
|
|
77
|
+
/** Ordered list of pattern alternatives. Evaluated in order; first that
|
|
78
|
+
* yields >= min_count buttons wins. Mutually exclusive with `pattern`. */
|
|
79
|
+
patterns?: Array<{ pattern: string; flags?: string }>;
|
|
76
80
|
key_for_index: string;
|
|
77
81
|
min_count?: number;
|
|
78
82
|
continuation_lines?: boolean;
|
|
@@ -28,7 +28,5 @@ export type RecentSessionBucket = 'needs_attention' | 'working' | 'task_complete
|
|
|
28
28
|
|
|
29
29
|
/** Terminal backend status */
|
|
30
30
|
export interface TerminalBackendStatus {
|
|
31
|
-
backend: '
|
|
32
|
-
preference: 'auto' | 'xterm' | 'ghostty-vt';
|
|
33
|
-
ghosttyAvailable: boolean;
|
|
31
|
+
backend: 'ghostty-vt';
|
|
34
32
|
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { TerminalViewportBackend, TerminalViewportBackendOptions } from './types.js';
|
|
2
|
-
export declare class XtermTerminalBackend implements TerminalViewportBackend {
|
|
3
|
-
readonly kind: "xterm";
|
|
4
|
-
private rows;
|
|
5
|
-
private cols;
|
|
6
|
-
private terminal;
|
|
7
|
-
constructor(options: TerminalViewportBackendOptions);
|
|
8
|
-
resize(rows: number, cols: number): void;
|
|
9
|
-
write(data: string): void;
|
|
10
|
-
getText(): string;
|
|
11
|
-
getCursorPosition(): {
|
|
12
|
-
col: number;
|
|
13
|
-
row: number;
|
|
14
|
-
};
|
|
15
|
-
dispose(): void;
|
|
16
|
-
private createTerminal;
|
|
17
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { TerminalViewportBackend, TerminalViewportBackendOptions, TerminalViewportBackendPreference } from './types.js';
|
|
2
|
-
export declare function resolveTerminalBackendPreference(): TerminalViewportBackendPreference;
|
|
3
|
-
export declare function isGhosttyVtBackendAvailable(): boolean;
|
|
4
|
-
export declare class GhosttyVtTerminalBackend implements TerminalViewportBackend {
|
|
5
|
-
readonly kind: "ghostty-vt";
|
|
6
|
-
private terminal;
|
|
7
|
-
constructor(options: TerminalViewportBackendOptions);
|
|
8
|
-
resize(rows: number, cols: number): void;
|
|
9
|
-
write(data: string): void;
|
|
10
|
-
getText(): string;
|
|
11
|
-
getCursorPosition(): {
|
|
12
|
-
col: number;
|
|
13
|
-
row: number;
|
|
14
|
-
};
|
|
15
|
-
dispose(): void;
|
|
16
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import type { TerminalViewportBackend, TerminalViewportBackendOptions } from './types.js';
|
|
2
|
-
|
|
3
|
-
type XtermBufferLine = {
|
|
4
|
-
translateToString(trimRight?: boolean): string;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
type XtermBuffer = {
|
|
8
|
-
length: number;
|
|
9
|
-
viewportY: number;
|
|
10
|
-
cursorX?: number;
|
|
11
|
-
cursorY?: number;
|
|
12
|
-
getLine(index: number): XtermBufferLine | undefined;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type XtermTerminal = {
|
|
16
|
-
buffer: { active: XtermBuffer };
|
|
17
|
-
write(data: string, callback?: () => void): void;
|
|
18
|
-
resize(cols: number, rows: number): void;
|
|
19
|
-
dispose(): void;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
let TerminalCtor: (new (options: { cols: number; rows: number; scrollback: number }) => XtermTerminal) | null = null;
|
|
23
|
-
|
|
24
|
-
function loadTerminalCtor(): new (options: { cols: number; rows: number; scrollback: number }) => XtermTerminal {
|
|
25
|
-
if (!TerminalCtor) {
|
|
26
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
27
|
-
const mod = require('@xterm/xterm');
|
|
28
|
-
TerminalCtor = mod.Terminal || mod.default?.Terminal || mod.default;
|
|
29
|
-
if (!TerminalCtor) {
|
|
30
|
-
throw new Error('@xterm/xterm Terminal export not found');
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return TerminalCtor;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class XtermTerminalBackend implements TerminalViewportBackend {
|
|
37
|
-
readonly kind = 'xterm' as const;
|
|
38
|
-
private rows: number;
|
|
39
|
-
private cols: number;
|
|
40
|
-
private terminal: XtermTerminal;
|
|
41
|
-
|
|
42
|
-
constructor(options: TerminalViewportBackendOptions) {
|
|
43
|
-
this.rows = Math.max(1, options.rows | 0);
|
|
44
|
-
this.cols = Math.max(1, options.cols | 0);
|
|
45
|
-
this.terminal = this.createTerminal(options.scrollback);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
resize(rows: number, cols: number): void {
|
|
49
|
-
this.rows = Math.max(1, rows | 0);
|
|
50
|
-
this.cols = Math.max(1, cols | 0);
|
|
51
|
-
this.terminal.resize(this.cols, this.rows);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
write(data: string): void {
|
|
55
|
-
if (!data) return;
|
|
56
|
-
this.terminal.write(data);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
getText(): string {
|
|
60
|
-
const buffer = this.terminal.buffer.active;
|
|
61
|
-
const start = Math.max(0, buffer.viewportY || 0);
|
|
62
|
-
const end = Math.max(start, Math.min(buffer.length || 0, start + this.rows));
|
|
63
|
-
const lines: string[] = [];
|
|
64
|
-
|
|
65
|
-
for (let i = start; i < end; i++) {
|
|
66
|
-
const line = buffer.getLine(i);
|
|
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+$/, ''));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let first = 0;
|
|
78
|
-
let last = lines.length;
|
|
79
|
-
while (first < last && !lines[first]?.trim()) first++;
|
|
80
|
-
while (last > first && !lines[last - 1]?.trim()) last--;
|
|
81
|
-
return lines.slice(first, last).join('\n');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
getCursorPosition(): { col: number; row: number } {
|
|
85
|
-
const buffer = this.terminal.buffer.active;
|
|
86
|
-
return {
|
|
87
|
-
col: Math.max(0, buffer.cursorX || 0),
|
|
88
|
-
row: Math.max(0, buffer.cursorY || 0),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
dispose(): void {
|
|
93
|
-
this.terminal.dispose();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private createTerminal(scrollback: number): XtermTerminal {
|
|
97
|
-
const Terminal = loadTerminalCtor();
|
|
98
|
-
return new Terminal({
|
|
99
|
-
cols: this.cols,
|
|
100
|
-
rows: this.rows,
|
|
101
|
-
scrollback,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
}
|