@adhdev/daemon-core 0.6.55 → 0.6.56

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.6.55",
3
+ "version": "0.6.56",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,6 +31,7 @@
31
31
  "license": "AGPL-3.0-or-later",
32
32
  "dependencies": {
33
33
  "@agentclientprotocol/sdk": "^0.16.1",
34
+ "@xterm/xterm": "^6.0.0",
34
35
  "chalk": "^5.3.0",
35
36
  "conf": "^13.0.0",
36
37
  "ws": "^8.19.0"
@@ -68,7 +68,7 @@ export interface CliScripts {
68
68
  /** Full PTY buffer → ReadChatResult (messages, status, activeModal) */
69
69
  parseOutput?: (input: CliScriptInput) => any;
70
70
  /** Lightweight status detection (high-frequency polling) → AgentStatus string */
71
- detectStatus?: (input: { tail: string }) => string | null;
71
+ detectStatus?: (input: { tail: string; screenText?: string; rawBuffer?: string }) => string | null;
72
72
  /** Parse approval modal from PTY output → ModalInfo | null */
73
73
  parseApproval?: (input: { buffer: string; rawBuffer?: string; tail: string }) => { message: string; buttons: string[] } | null;
74
74
  /** Produce a cli-specific prompt from a dashboard action payload */
@@ -91,6 +91,8 @@ export interface CliProviderModule {
91
91
  name: string;
92
92
  category: 'cli';
93
93
  binary: string;
94
+ sendDelayMs?: number;
95
+ sendKey?: string;
94
96
  spawn: {
95
97
  command: string;
96
98
  args: string[];
@@ -270,6 +272,7 @@ export class ProviderCliAdapter implements CliAdapter {
270
272
  // Output settle debounce — fires after PTY output goes quiet
271
273
  private settleTimer: NodeJS.Timeout | null = null;
272
274
  private settledBuffer: string = '';
275
+ private submitPendingUntil = 0;
273
276
 
274
277
  // Resize redraw suppression
275
278
  private resizeSuppressUntil: number = 0;
@@ -302,6 +305,8 @@ export class ProviderCliAdapter implements CliAdapter {
302
305
 
303
306
  // Provider approval key mapping
304
307
  private readonly approvalKeys: Record<number, string>;
308
+ private readonly sendDelayMs: number;
309
+ private readonly sendKey: string;
305
310
 
306
311
  constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
307
312
  this.provider = provider;
@@ -325,6 +330,10 @@ export class ProviderCliAdapter implements CliAdapter {
325
330
 
326
331
  const rawKeys = (provider as any).approvalKeys;
327
332
  this.approvalKeys = (rawKeys && typeof rawKeys === 'object') ? rawKeys : {};
333
+ this.sendDelayMs = typeof (provider as any).sendDelayMs === 'number' ? Math.max(0, (provider as any).sendDelayMs) : 0;
334
+ this.sendKey = typeof (provider as any).sendKey === 'string' && (provider as any).sendKey.length > 0
335
+ ? (provider as any).sendKey
336
+ : '\r';
328
337
 
329
338
  // Scripts are required — loaded by ProviderLoader via compatibility array
330
339
  this.cliScripts = (provider as any).scripts || {};
@@ -460,6 +469,11 @@ export class ProviderCliAdapter implements CliAdapter {
460
469
  private handleOutput(rawData: string): void {
461
470
  if (Date.now() < this.resizeSuppressUntil) return;
462
471
 
472
+ if (rawData.includes('\x1b[6n') || rawData.includes('\x1b[?6n')) {
473
+ // Some TUIs probe cursor position during startup; node-pty does not answer automatically.
474
+ this.ptyProcess?.write('\x1b[1;1R');
475
+ }
476
+
463
477
  this.terminalScreen.write(rawData);
464
478
  const cleanData = stripAnsi(rawData);
465
479
 
@@ -519,11 +533,17 @@ export class ProviderCliAdapter implements CliAdapter {
519
533
 
520
534
  private scheduleSettle(): void {
521
535
  if (this.settleTimer) clearTimeout(this.settleTimer);
536
+ const delay = Math.max(
537
+ this.timeouts.outputSettle,
538
+ this.submitPendingUntil > Date.now()
539
+ ? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
540
+ : 0,
541
+ );
522
542
  this.settleTimer = setTimeout(() => {
523
543
  this.settleTimer = null;
524
544
  this.settledBuffer = this.recentOutputBuffer;
525
545
  this.evaluateSettled();
526
- }, this.timeouts.outputSettle);
546
+ }, delay);
527
547
  }
528
548
 
529
549
  private evaluateSettled(): void {
@@ -612,7 +632,11 @@ export class ProviderCliAdapter implements CliAdapter {
612
632
  private runDetectStatus(text: string): string | null {
613
633
  if (!this.cliScripts?.detectStatus) return null;
614
634
  try {
615
- return this.cliScripts.detectStatus({ tail: text.slice(-500) });
635
+ return this.cliScripts.detectStatus({
636
+ tail: text.slice(-500),
637
+ screenText: this.terminalScreen.getText(),
638
+ rawBuffer: this.accumulatedRawBuffer,
639
+ });
616
640
  } catch (e: any) {
617
641
  LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
618
642
  return null;
@@ -729,12 +753,22 @@ export class ProviderCliAdapter implements CliAdapter {
729
753
  this.responseBuffer = '';
730
754
  this.setStatus('generating', 'sendMessage');
731
755
  this.onStatusChange?.();
756
+ this.ptyProcess.write(text);
732
757
 
733
- this.ptyProcess.write(text + '\r');
734
-
735
- this.responseTimeout = setTimeout(() => {
736
- if (this.isWaitingForResponse) this.finishResponse();
737
- }, this.timeouts.maxResponse);
758
+ const submit = () => {
759
+ if (!this.ptyProcess) return;
760
+ this.submitPendingUntil = 0;
761
+ this.ptyProcess.write(this.sendKey);
762
+ this.responseTimeout = setTimeout(() => {
763
+ if (this.isWaitingForResponse) this.finishResponse();
764
+ }, this.timeouts.maxResponse);
765
+ };
766
+ if (this.sendDelayMs > 0) {
767
+ this.submitPendingUntil = Date.now() + this.sendDelayMs;
768
+ setTimeout(submit, this.sendDelayMs);
769
+ } else {
770
+ submit();
771
+ }
738
772
  }
739
773
 
740
774
  getPartialResponse(): string {
@@ -810,10 +844,14 @@ export class ProviderCliAdapter implements CliAdapter {
810
844
  messages: this.messages.slice(-20),
811
845
  structuredMessages: this.structuredMessages.slice(-20),
812
846
  messageCount: this.messages.length,
847
+ screenText: this.terminalScreen.getText().slice(-4000),
813
848
  startupBuffer: this.startupBuffer.slice(-4000),
814
849
  recentOutputBuffer: this.recentOutputBuffer.slice(-500),
815
850
  settledBuffer: this.settledBuffer.slice(-500),
816
851
  accumulatedBufferLength: this.accumulatedBuffer.length,
852
+ accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
853
+ rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
854
+ responseBuffer: this.responseBuffer.slice(-1000),
817
855
  isWaitingForResponse: this.isWaitingForResponse,
818
856
  activeModal: this.activeModal,
819
857
  lastApprovalResolvedAt: this.lastApprovalResolvedAt,
@@ -1,268 +1,95 @@
1
1
  /**
2
- * Minimal VT screen model for PTY parsing.
2
+ * PTY screen snapshot backed by xterm's parser.
3
3
  *
4
- * This is not a full terminal emulator. It exists to turn raw PTY output into a
5
- * stable "current screen" snapshot so provider parsers can inspect visible UI
6
- * state instead of reparsing the full accumulated log stream.
4
+ * Claude Code and similar CLIs use a real terminal UI. A handwritten ANSI
5
+ * parser quickly drifts from reality, so we reuse xterm's terminal model and
6
+ * expose only the current visible viewport as plain text for provider scripts.
7
7
  */
8
8
 
9
- function clamp(value: number, min: number, max: number): number {
10
- return Math.max(min, Math.min(max, value));
9
+ type XtermBufferLine = {
10
+ translateToString(trimRight?: boolean): string;
11
+ };
12
+
13
+ type XtermBuffer = {
14
+ length: number;
15
+ viewportY: number;
16
+ getLine(index: number): XtermBufferLine | undefined;
17
+ };
18
+
19
+ type XtermTerminal = {
20
+ buffer: { active: XtermBuffer };
21
+ write(data: string, callback?: () => void): void;
22
+ resize(cols: number, rows: number): void;
23
+ dispose(): void;
24
+ };
25
+
26
+ let TerminalCtor: (new (options: { cols: number; rows: number; scrollback: number }) => XtermTerminal) | null = null;
27
+
28
+ function loadTerminalCtor(): new (options: { cols: number; rows: number; scrollback: number }) => XtermTerminal {
29
+ if (!TerminalCtor) {
30
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
31
+ const mod = require('@xterm/xterm');
32
+ TerminalCtor = mod.Terminal || mod.default?.Terminal || mod.default;
33
+ if (!TerminalCtor) {
34
+ throw new Error('@xterm/xterm Terminal export not found');
35
+ }
36
+ }
37
+ return TerminalCtor;
11
38
  }
12
39
 
13
40
  export class TerminalScreen {
14
41
  private rows: number;
15
42
  private cols: number;
16
- private cursorRow = 0;
17
- private cursorCol = 0;
18
- private savedRow = 0;
19
- private savedCol = 0;
20
- private lines: string[][];
43
+ private terminal: XtermTerminal;
21
44
 
22
45
  constructor(rows = 40, cols = 120) {
23
- this.rows = rows;
24
- this.cols = cols;
25
- this.lines = this.makeLines(rows, cols);
46
+ this.rows = Math.max(1, rows | 0);
47
+ this.cols = Math.max(1, cols | 0);
48
+ this.terminal = this.createTerminal();
26
49
  }
27
50
 
28
51
  reset(rows = this.rows, cols = this.cols): void {
29
- this.rows = rows;
30
- this.cols = cols;
31
- this.cursorRow = 0;
32
- this.cursorCol = 0;
33
- this.savedRow = 0;
34
- this.savedCol = 0;
35
- this.lines = this.makeLines(rows, cols);
52
+ this.rows = Math.max(1, rows | 0);
53
+ this.cols = Math.max(1, cols | 0);
54
+ this.terminal.dispose();
55
+ this.terminal = this.createTerminal();
36
56
  }
37
57
 
38
58
  resize(rows: number, cols: number): void {
39
- const nextRows = Math.max(1, rows | 0);
40
- const nextCols = Math.max(1, cols | 0);
41
- const next = this.makeLines(nextRows, nextCols);
42
- const copyRows = Math.min(this.rows, nextRows);
43
- const copyCols = Math.min(this.cols, nextCols);
44
- for (let r = 0; r < copyRows; r++) {
45
- for (let c = 0; c < copyCols; c++) {
46
- next[r][c] = this.lines[r][c];
47
- }
48
- }
49
- this.rows = nextRows;
50
- this.cols = nextCols;
51
- this.lines = next;
52
- this.cursorRow = clamp(this.cursorRow, 0, this.rows - 1);
53
- this.cursorCol = clamp(this.cursorCol, 0, this.cols - 1);
54
- this.savedRow = clamp(this.savedRow, 0, this.rows - 1);
55
- this.savedCol = clamp(this.savedCol, 0, this.cols - 1);
59
+ this.rows = Math.max(1, rows | 0);
60
+ this.cols = Math.max(1, cols | 0);
61
+ this.terminal.resize(this.cols, this.rows);
56
62
  }
57
63
 
58
64
  write(data: string): void {
59
- let i = 0;
60
- while (i < data.length) {
61
- const ch = data[i];
62
-
63
- if (ch === '\x1b') {
64
- const consumed = this.consumeEscape(data, i);
65
- i = consumed > i ? consumed : i + 1;
66
- continue;
67
- }
68
-
69
- if (ch === '\r') {
70
- this.cursorCol = 0;
71
- i++;
72
- continue;
73
- }
74
-
75
- if (ch === '\n') {
76
- this.newLine();
77
- i++;
78
- continue;
79
- }
80
-
81
- if (ch === '\b') {
82
- this.cursorCol = Math.max(0, this.cursorCol - 1);
83
- i++;
84
- continue;
85
- }
86
-
87
- if (ch === '\t') {
88
- const nextStop = Math.min(this.cols - 1, this.cursorCol + (8 - (this.cursorCol % 8 || 8)));
89
- while (this.cursorCol < nextStop) this.putChar(' ');
90
- i++;
91
- continue;
92
- }
93
-
94
- if (ch >= ' ' && ch !== '\x7f') {
95
- this.putChar(ch);
96
- }
97
-
98
- i++;
99
- }
65
+ if (!data) return;
66
+ this.terminal.write(data);
100
67
  }
101
68
 
102
69
  getText(): string {
103
- const raw = this.lines.map(line => line.join('').replace(/\s+$/, ''));
104
- let start = 0;
105
- let end = raw.length;
106
- while (start < end && raw[start] === '') start++;
107
- while (end > start && raw[end - 1] === '') end--;
108
- return raw.slice(start, end).join('\n');
109
- }
110
-
111
- private consumeEscape(data: string, start: number): number {
112
- const next = data[start + 1];
113
- if (!next) return start + 1;
114
-
115
- if (next === '[') {
116
- let end = start + 2;
117
- while (end < data.length && !/[@-~]/.test(data[end])) end++;
118
- if (end >= data.length) return data.length;
119
- this.applyCsi(data.slice(start + 2, end), data[end]);
120
- return end + 1;
121
- }
122
-
123
- if (next === ']') {
124
- let end = start + 2;
125
- while (end < data.length) {
126
- if (data[end] === '\x07') return end + 1;
127
- if (data[end] === '\x1b' && data[end + 1] === '\\') return end + 2;
128
- end++;
129
- }
130
- return data.length;
131
- }
132
-
133
- if (next === '7') {
134
- this.savedRow = this.cursorRow;
135
- this.savedCol = this.cursorCol;
136
- return start + 2;
137
- }
138
- if (next === '8') {
139
- this.cursorRow = this.savedRow;
140
- this.cursorCol = this.savedCol;
141
- return start + 2;
70
+ const buffer = this.terminal.buffer.active;
71
+ const start = Math.max(0, buffer.viewportY || 0);
72
+ const end = Math.max(start, Math.min(buffer.length || 0, start + this.rows));
73
+ const lines: string[] = [];
74
+
75
+ for (let i = start; i < end; i++) {
76
+ const line = buffer.getLine(i);
77
+ lines.push(line ? line.translateToString(true) : '');
142
78
  }
143
79
 
144
- return start + 2;
145
- }
146
-
147
- private applyCsi(paramText: string, finalChar: string): void {
148
- const privateMode = paramText.startsWith('?');
149
- const normalized = privateMode ? paramText.slice(1) : paramText;
150
- const params = normalized.length > 0
151
- ? normalized.split(';').map(p => parseInt(p || '0', 10) || 0)
152
- : [0];
153
-
154
- switch (finalChar) {
155
- case 'A':
156
- this.cursorRow = clamp(this.cursorRow - (params[0] || 1), 0, this.rows - 1);
157
- return;
158
- case 'B':
159
- this.cursorRow = clamp(this.cursorRow + (params[0] || 1), 0, this.rows - 1);
160
- return;
161
- case 'C':
162
- this.cursorCol = clamp(this.cursorCol + (params[0] || 1), 0, this.cols - 1);
163
- return;
164
- case 'D':
165
- this.cursorCol = clamp(this.cursorCol - (params[0] || 1), 0, this.cols - 1);
166
- return;
167
- case 'E':
168
- this.cursorRow = clamp(this.cursorRow + (params[0] || 1), 0, this.rows - 1);
169
- this.cursorCol = 0;
170
- return;
171
- case 'F':
172
- this.cursorRow = clamp(this.cursorRow - (params[0] || 1), 0, this.rows - 1);
173
- this.cursorCol = 0;
174
- return;
175
- case 'G':
176
- this.cursorCol = clamp((params[0] || 1) - 1, 0, this.cols - 1);
177
- return;
178
- case 'H':
179
- case 'f': {
180
- const row = (params[0] || 1) - 1;
181
- const col = (params[1] || 1) - 1;
182
- this.cursorRow = clamp(row, 0, this.rows - 1);
183
- this.cursorCol = clamp(col, 0, this.cols - 1);
184
- return;
185
- }
186
- case 'J': {
187
- const mode = params[0] || 0;
188
- if (mode === 2 || mode === 3) {
189
- this.reset(this.rows, this.cols);
190
- } else if (mode === 0) {
191
- this.clearToEndOfScreen();
192
- } else if (mode === 1) {
193
- this.clearToStartOfScreen();
194
- }
195
- return;
196
- }
197
- case 'K': {
198
- const mode = params[0] || 0;
199
- if (mode === 2) this.clearLine(this.cursorRow, 0, this.cols - 1);
200
- else if (mode === 1) this.clearLine(this.cursorRow, 0, this.cursorCol);
201
- else this.clearLine(this.cursorRow, this.cursorCol, this.cols - 1);
202
- return;
203
- }
204
- case 'm':
205
- return;
206
- case 's':
207
- this.savedRow = this.cursorRow;
208
- this.savedCol = this.cursorCol;
209
- return;
210
- case 'u':
211
- this.cursorRow = this.savedRow;
212
- this.cursorCol = this.savedCol;
213
- return;
214
- case 'h':
215
- case 'l':
216
- if (privateMode && (normalized === '1049' || normalized === '47')) {
217
- this.reset(this.rows, this.cols);
218
- }
219
- return;
220
- default:
221
- return;
222
- }
223
- }
224
-
225
- private putChar(ch: string): void {
226
- if (this.cursorRow < 0 || this.cursorRow >= this.rows) return;
227
- if (this.cursorCol < 0) this.cursorCol = 0;
228
- if (this.cursorCol >= this.cols) this.newLine();
229
- this.lines[this.cursorRow][this.cursorCol] = ch;
230
- this.cursorCol++;
231
- if (this.cursorCol >= this.cols) this.newLine();
232
- }
233
-
234
- private newLine(): void {
235
- this.cursorCol = 0;
236
- if (this.cursorRow >= this.rows - 1) {
237
- this.lines.shift();
238
- this.lines.push(Array.from({ length: this.cols }, () => ' '));
239
- } else {
240
- this.cursorRow++;
241
- }
242
- }
243
-
244
- private clearLine(row: number, start: number, end: number): void {
245
- if (row < 0 || row >= this.rows) return;
246
- for (let c = clamp(start, 0, this.cols - 1); c <= clamp(end, 0, this.cols - 1); c++) {
247
- this.lines[row][c] = ' ';
248
- }
249
- }
250
-
251
- private clearToEndOfScreen(): void {
252
- this.clearLine(this.cursorRow, this.cursorCol, this.cols - 1);
253
- for (let r = this.cursorRow + 1; r < this.rows; r++) {
254
- this.clearLine(r, 0, this.cols - 1);
255
- }
256
- }
257
-
258
- private clearToStartOfScreen(): void {
259
- for (let r = 0; r < this.cursorRow; r++) {
260
- this.clearLine(r, 0, this.cols - 1);
261
- }
262
- this.clearLine(this.cursorRow, 0, this.cursorCol);
80
+ let first = 0;
81
+ let last = lines.length;
82
+ while (first < last && !lines[first]?.trim()) first++;
83
+ while (last > first && !lines[last - 1]?.trim()) last--;
84
+ return lines.slice(first, last).join('\n');
263
85
  }
264
86
 
265
- private makeLines(rows: number, cols: number): string[][] {
266
- return Array.from({ length: rows }, () => Array.from({ length: cols }, () => ' '));
87
+ private createTerminal(): XtermTerminal {
88
+ const Terminal = loadTerminalCtor();
89
+ return new Terminal({
90
+ cols: this.cols,
91
+ rows: this.rows,
92
+ scrollback: 2000,
93
+ });
267
94
  }
268
95
  }