@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.
Files changed (30) hide show
  1. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +1 -3
  2. package/dist/cli-adapters/terminal-backends/types.d.ts +1 -2
  3. package/dist/cli-adapters/terminal-screen.d.ts +2 -11
  4. package/dist/index.js +156 -212
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +141 -197
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/providers/spec/adapter.d.ts +11 -19
  9. package/dist/providers/spec/driver.d.ts +14 -0
  10. package/dist/providers/spec/schema.gen.d.ts +24 -1
  11. package/dist/providers/spec/types.d.ts +8 -1
  12. package/dist/shared-types-extra.d.ts +1 -3
  13. package/package.json +1 -3
  14. package/src/cli-adapters/provider-cli-adapter.ts +3 -2
  15. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +11 -34
  16. package/src/cli-adapters/terminal-backends/types.ts +1 -3
  17. package/src/cli-adapters/terminal-screen.ts +11 -81
  18. package/src/commands/mesh-coordinator.ts +3 -2
  19. package/src/daemon/dev-auto-implement.ts +3 -2
  20. package/src/providers/spec/adapter.ts +40 -78
  21. package/src/providers/spec/cli-adapter.ts +2 -0
  22. package/src/providers/spec/driver.ts +71 -7
  23. package/src/providers/spec/evaluator.ts +44 -14
  24. package/src/providers/spec/loader.ts +6 -1
  25. package/src/providers/spec/schema.gen.ts +17 -1
  26. package/src/providers/spec/types.ts +5 -1
  27. package/src/shared-types-extra.ts +1 -3
  28. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  29. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  30. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -104
@@ -1,23 +1,15 @@
1
- import { type PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
1
+ import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
2
+ export type { PtyTransportFactory };
2
3
  export interface TerminalAdapterOpts {
3
4
  binary: string;
4
5
  args?: string[];
5
- cwd: string;
6
+ cwd?: string;
6
7
  env?: Record<string, string>;
7
8
  cols?: number;
8
9
  rows?: number;
9
10
  /** Coalesce screen snapshots: emit on_screen_changed at most this often. */
10
11
  screenChangeDebounceMs?: number;
11
- /** tick() period. 0 disables ticks. */
12
12
  tickIntervalMs?: number;
13
- /**
14
- * Optional PTY transport factory. When the daemon supplies a
15
- * SessionHostPtyTransportFactory (standalone runs the PTY inside the
16
- * session-host so the runtime/<sid>/snapshot endpoint can serve it),
17
- * forward it here. Without it the PTY spawns locally inside the
18
- * daemon process and the dashboard's terminal pane reports
19
- * "Runtime terminal unavailable: Unknown session".
20
- */
21
13
  transportFactory?: PtyTransportFactory;
22
14
  }
23
15
  export interface TerminalAdapterHandlers {
@@ -26,33 +18,33 @@ export interface TerminalAdapterHandlers {
26
18
  }): void;
27
19
  on_pty_data?(chunk: string): void;
28
20
  on_screen_changed?(snapshot: string): void;
29
- tick?(): void;
30
21
  on_exit?(info: {
31
22
  exitCode: number;
32
23
  }): void;
24
+ tick?(): void;
33
25
  }
34
26
  export declare class TerminalAdapter {
35
27
  private readonly opts;
36
28
  private readonly handlers;
37
- private term;
38
- private pty;
39
- private factory;
40
- private cols;
41
29
  private rows;
42
- private screenDebounceMs;
43
- private tickIntervalMs;
30
+ private cols;
31
+ private readonly screenDebounceMs;
32
+ private readonly tickIntervalMs;
33
+ private readonly factory;
34
+ private screen;
35
+ private pty;
44
36
  private screenTimer;
45
37
  private tickTimer;
46
38
  private lastScreen;
47
39
  constructor(opts: TerminalAdapterOpts, handlers: TerminalAdapterHandlers);
48
40
  start(): void;
49
- send_keys(s: string): void;
50
41
  resize(cols: number, rows: number): void;
51
42
  snapshot(): string;
52
43
  getCursorPosition(): {
53
44
  row: number;
54
45
  col: number;
55
46
  };
47
+ send_keys(text: string): void;
56
48
  kill(): void;
57
49
  private onChunk;
58
50
  private computeScreen;
@@ -128,6 +128,14 @@ export declare class SpecDriver {
128
128
  * because the evaluator already moved past busy by the time the hold
129
129
  * kicks in. */
130
130
  private lastBusyState;
131
+ /** Timestamp of the last time we entered a modal state (approval/picker or
132
+ * any non-busy non-idle state). Used to suppress brief busy blips that
133
+ * appear while the modal is still on screen — Claude Code streams body
134
+ * text that transiently shows a spinner even while an approval modal is
135
+ * visible, causing rapid approval→busy→approval flicker on the dashboard. */
136
+ private lastModalAt;
137
+ /** The modal state snapshot held across busy blips. */
138
+ private lastModalState;
131
139
  private completionIdleFirstSeenAt;
132
140
  private completionIdleKey;
133
141
  /** Timer that re-runs evaluate() once the hold window expires. Needed
@@ -174,6 +182,12 @@ export declare class SpecDriver {
174
182
  getLastBusyAt(): number;
175
183
  hasIdleHoldPending(): boolean;
176
184
  getSpecPath(): string;
185
+ getCompletionIdleDebounceState(): {
186
+ active: boolean;
187
+ ageMs: number;
188
+ holdMs: number;
189
+ forceAfterMs: number;
190
+ } | null;
177
191
  getScreen(): string;
178
192
  getSections(): Array<{
179
193
  id: string;
@@ -312,7 +312,12 @@ export declare const SCHEMA: {
312
312
  readonly modal_buttons: {
313
313
  readonly type: "object";
314
314
  readonly additionalProperties: false;
315
- readonly required: readonly ["pattern", "key_for_index"];
315
+ readonly required: readonly ["key_for_index"];
316
+ readonly oneOf: readonly [{
317
+ readonly required: readonly ["pattern"];
318
+ }, {
319
+ readonly required: readonly ["patterns"];
320
+ }];
316
321
  readonly properties: {
317
322
  readonly section: {
318
323
  readonly type: "string";
@@ -324,6 +329,24 @@ export declare const SCHEMA: {
324
329
  readonly flags: {
325
330
  readonly type: "string";
326
331
  };
332
+ readonly patterns: {
333
+ readonly type: "array";
334
+ readonly minItems: 1;
335
+ readonly items: {
336
+ readonly type: "object";
337
+ readonly additionalProperties: false;
338
+ readonly required: readonly ["pattern"];
339
+ readonly properties: {
340
+ readonly pattern: {
341
+ readonly type: "string";
342
+ readonly minLength: 1;
343
+ };
344
+ readonly flags: {
345
+ readonly type: "string";
346
+ };
347
+ };
348
+ };
349
+ };
327
350
  readonly key_for_index: {
328
351
  readonly type: "string";
329
352
  readonly minLength: 1;
@@ -61,8 +61,15 @@ export interface SectionPattern {
61
61
  }
62
62
  export interface ModalButtonsRule {
63
63
  section?: string;
64
- pattern: string;
64
+ /** Single pattern (original). Use `patterns` for multi-format fallback. */
65
+ pattern?: string;
65
66
  flags?: string;
67
+ /** Ordered list of pattern alternatives. Evaluated in order; first that
68
+ * yields >= min_count buttons wins. Mutually exclusive with `pattern`. */
69
+ patterns?: Array<{
70
+ pattern: string;
71
+ flags?: string;
72
+ }>;
66
73
  key_for_index: string;
67
74
  min_count?: number;
68
75
  continuation_lines?: boolean;
@@ -23,7 +23,5 @@ export type SessionStatus = 'idle' | 'generating' | 'waiting_approval' | 'waitin
23
23
  export type RecentSessionBucket = 'needs_attention' | 'working' | 'task_complete' | 'idle';
24
24
  /** Terminal backend status */
25
25
  export interface TerminalBackendStatus {
26
- backend: 'xterm' | 'ghostty-vt';
27
- preference: 'auto' | 'xterm' | 'ghostty-vt';
28
- ghosttyAvailable: boolean;
26
+ backend: 'ghostty-vt';
29
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.207",
3
+ "version": "0.9.82-rc.209",
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",
@@ -48,8 +48,6 @@
48
48
  "dependencies": {
49
49
  "@adhdev/session-host-core": "*",
50
50
  "@agentclientprotocol/sdk": "^0.16.1",
51
- "@xterm/headless": "^6.0.0",
52
- "@xterm/xterm": "^6.0.0",
53
51
  "ajv": "^8.20.0",
54
52
  "ajv-formats": "^3.0.1",
55
53
  "better-sqlite3": "^12.10.0",
@@ -20,6 +20,7 @@ import type { InteractivePromptResponse } from '../providers/types/interactive-p
20
20
  import { LOG } from '../logging/logger.js';
21
21
  import { getDebugRuntimeConfig } from '../logging/debug-config.js';
22
22
  import { TerminalScreen } from './terminal-screen.js';
23
+ import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
23
24
  import {
24
25
  NodePtyTransportFactory,
25
26
  type PtyRuntimeMetadata,
@@ -212,7 +213,7 @@ export class ProviderCliAdapter implements CliAdapter {
212
213
  /** Full accumulated raw PTY output (with ANSI) */
213
214
  private accumulatedRawBuffer: string = '';
214
215
  /** Current visible terminal screen snapshot */
215
- private terminalScreen = new TerminalScreen(24, 80);
216
+ private terminalScreen = new TerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
216
217
  private static readonly MAX_RESPONSE_BUFFER = 8000;
217
218
  private static readonly MAX_RECENT_OUTPUT_BUFFER = 1000;
218
219
  private responseBufferDroppedChars = 0;
@@ -563,7 +564,7 @@ export class ProviderCliAdapter implements CliAdapter {
563
564
  this.startupBuffer = '';
564
565
  this.startupFirstOutputAt = 0;
565
566
  if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
566
- this.resetTerminalScreen(24, 80);
567
+ this.resetTerminalScreen(DEFAULT_SESSION_HOST_ROWS, DEFAULT_SESSION_HOST_COLS);
567
568
  this.pendingTerminalQueryTail = '';
568
569
  this.ready = false;
569
570
  await this.ptyProcess.ready;
@@ -1,7 +1,6 @@
1
1
  import type {
2
2
  TerminalViewportBackend,
3
3
  TerminalViewportBackendOptions,
4
- TerminalViewportBackendPreference,
5
4
  } from './types.js';
6
5
 
7
6
  type GhosttyVtTerminal = {
@@ -49,12 +48,10 @@ function getBindingCandidates(): string[] {
49
48
  return explicit ? [explicit] : DEFAULT_BINDING_CANDIDATES;
50
49
  }
51
50
 
52
- function loadGhosttyVtBinding(required: boolean): GhosttyVtBinding | null {
51
+ function loadGhosttyVtBinding(): GhosttyVtBinding {
53
52
  if (cachedBinding !== undefined) {
54
- if (!cachedBinding && required && cachedBindingError) {
55
- throw cachedBindingError;
56
- }
57
- return cachedBinding;
53
+ if (!cachedBinding && cachedBindingError) throw cachedBindingError;
54
+ return cachedBinding!;
58
55
  }
59
56
 
60
57
  const errors: string[] = [];
@@ -65,7 +62,7 @@ function loadGhosttyVtBinding(required: boolean): GhosttyVtBinding | null {
65
62
  const mod = require(ref);
66
63
  cachedBinding = normalizeBinding(mod, ref);
67
64
  cachedBindingError = null;
68
- return cachedBinding;
65
+ return cachedBinding!;
69
66
  } catch (error) {
70
67
  if (isModuleNotFoundError(error, ref)) {
71
68
  errors.push(`${ref}: module not found`);
@@ -78,21 +75,9 @@ function loadGhosttyVtBinding(required: boolean): GhosttyVtBinding | null {
78
75
 
79
76
  cachedBinding = null;
80
77
  cachedBindingError = new Error(
81
- `ghostty-vt backend requested but no binding is available (${errors.join('; ') || 'no candidates tried'})`,
78
+ `ghostty-vt binding unavailable (${errors.join('; ') || 'no candidates tried'})`,
82
79
  );
83
-
84
- if (required) throw cachedBindingError;
85
- return null;
86
- }
87
-
88
- export function resolveTerminalBackendPreference(): TerminalViewportBackendPreference {
89
- const raw = process.env.ADHDEV_TERMINAL_BACKEND?.trim().toLowerCase();
90
- if (raw === 'ghostty-vt' || raw === 'xterm' || raw === 'auto') return raw;
91
- return 'auto';
92
- }
93
-
94
- export function isGhosttyVtBackendAvailable(): boolean {
95
- return !!loadGhosttyVtBinding(false);
80
+ throw cachedBindingError;
96
81
  }
97
82
 
98
83
  export class GhosttyVtTerminalBackend implements TerminalViewportBackend {
@@ -100,10 +85,7 @@ export class GhosttyVtTerminalBackend implements TerminalViewportBackend {
100
85
  private terminal: GhosttyVtTerminal;
101
86
 
102
87
  constructor(options: TerminalViewportBackendOptions) {
103
- const binding = loadGhosttyVtBinding(true);
104
- if (!binding) {
105
- throw new Error('ghostty-vt backend requested but no binding is available');
106
- }
88
+ const binding = loadGhosttyVtBinding();
107
89
  this.terminal = binding.createTerminal({
108
90
  cols: Math.max(1, options.cols | 0),
109
91
  rows: Math.max(1, options.rows | 0),
@@ -121,15 +103,10 @@ export class GhosttyVtTerminalBackend implements TerminalViewportBackend {
121
103
  }
122
104
 
123
105
  getText(): string {
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.
106
+ // ghostty's `trim:true` collapses CUF-advanced cells many TUIs including
107
+ // Claude Code render spaces via cursor-forward rather than literal spaces,
108
+ // which would break downstream regex matching. Keep per-row padding and
109
+ // trim trailing whitespace ourselves.
133
110
  const raw = this.terminal.formatPlainText({ trim: false }) || '';
134
111
  if (!raw) return '';
135
112
  const lines = raw.split('\n').map((row) => row.replace(/\s+$/, ''));
@@ -1,6 +1,4 @@
1
- export type TerminalViewportBackendKind = 'xterm' | 'ghostty-vt';
2
-
3
- export type TerminalViewportBackendPreference = TerminalViewportBackendKind | 'auto';
1
+ export type TerminalViewportBackendKind = 'ghostty-vt';
4
2
 
5
3
  export type TerminalViewportBackendOptions = {
6
4
  rows: number;
@@ -1,99 +1,29 @@
1
1
  /**
2
- * PTY screen snapshot abstraction.
3
- *
4
- * Terminal viewport backend selection.
5
- *
6
- * Runtime preference is backend-agnostic:
7
- * - prefer ghostty-vt when available (or when explicitly requested)
8
- * - fall back to xterm when ghostty-vt is unavailable
2
+ * PTY screen snapshot abstraction — ghostty-vt backend.
9
3
  */
10
4
 
11
- import { LOG } from '../logging/logger.js';
12
- import { GhosttyVtTerminalBackend, isGhosttyVtBackendAvailable, resolveTerminalBackendPreference } from './terminal-backends/ghostty-vt-backend.js';
13
- import type {
14
- TerminalViewportBackend,
15
- TerminalViewportBackendKind,
16
- TerminalViewportBackendOptions,
17
- TerminalViewportBackendPreference,
18
- } from './terminal-backends/types.js';
19
- import { XtermTerminalBackend } from './terminal-backends/xterm-backend.js';
5
+ import { GhosttyVtTerminalBackend } from './terminal-backends/ghostty-vt-backend.js';
6
+ import type { TerminalViewportBackendKind } from './terminal-backends/types.js';
7
+ import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
20
8
 
21
9
  const DEFAULT_SCROLLBACK = 2000;
22
- const loggedTerminalBackends = new Set<string>();
23
10
 
24
11
  export function getTerminalBackendRuntimeStatus(): {
25
12
  backend: TerminalViewportBackendKind;
26
- preference: TerminalViewportBackendPreference;
27
- ghosttyAvailable: boolean;
28
13
  } {
29
- const preference = resolveTerminalBackendPreference();
30
- const ghosttyAvailable = isGhosttyVtBackendAvailable();
31
- const backend: TerminalViewportBackendKind = (
32
- preference === 'ghostty-vt' || (preference === 'auto' && ghosttyAvailable)
33
- ? 'ghostty-vt'
34
- : 'xterm'
35
- );
36
- return { backend, preference, ghosttyAvailable };
37
- }
38
-
39
- function createTerminalBackend(
40
- options: TerminalViewportBackendOptions,
41
- preference: TerminalViewportBackendPreference,
42
- ): TerminalViewportBackend {
43
- const ghosttyAvailable = isGhosttyVtBackendAvailable();
44
- if (preference === 'ghostty-vt') {
45
- const backend = new GhosttyVtTerminalBackend(options);
46
- logTerminalBackendSelection(preference, ghosttyAvailable, backend.kind);
47
- return backend;
48
- }
49
-
50
- if (preference === 'auto' && ghosttyAvailable) {
51
- const backend = new GhosttyVtTerminalBackend(options);
52
- logTerminalBackendSelection(preference, ghosttyAvailable, backend.kind);
53
- return backend;
54
- }
55
-
56
- const backend = new XtermTerminalBackend(options);
57
- logTerminalBackendSelection(preference, ghosttyAvailable, backend.kind);
58
- return backend;
59
- }
60
-
61
- function logTerminalBackendSelection(
62
- preference: TerminalViewportBackendPreference,
63
- ghosttyAvailable: boolean,
64
- backendKind: TerminalViewportBackendKind,
65
- ): void {
66
- const key = `${preference}:${ghosttyAvailable}:${backendKind}`;
67
- if (loggedTerminalBackends.has(key)) return;
68
- loggedTerminalBackends.add(key);
69
- if (backendKind === 'xterm' && preference !== 'xterm' && !ghosttyAvailable) {
70
- const message = `[terminal-screen] ghostty-vt unavailable; using xterm fallback (preference=${preference})`;
71
- if (preference === 'auto') {
72
- LOG.info('Terminal', message);
73
- } else {
74
- LOG.warn('Terminal', message);
75
- }
76
- return;
77
- }
78
- LOG.info(
79
- 'Terminal',
80
- `[terminal-screen] backend=${backendKind} preference=${preference} ghosttyAvailable=${ghosttyAvailable}`,
81
- );
14
+ return { backend: 'ghostty-vt' };
82
15
  }
83
16
 
84
17
  export class TerminalScreen {
85
- readonly backendKind: TerminalViewportBackendKind;
18
+ readonly backendKind: TerminalViewportBackendKind = 'ghostty-vt';
86
19
  private rows: number;
87
20
  private cols: number;
88
- private readonly preference: TerminalViewportBackendPreference;
89
- private terminal: TerminalViewportBackend;
21
+ private terminal: GhosttyVtTerminalBackend;
90
22
 
91
- constructor(rows = 40, cols = 120) {
23
+ constructor(rows = DEFAULT_SESSION_HOST_ROWS, cols = DEFAULT_SESSION_HOST_COLS) {
92
24
  this.rows = Math.max(1, rows | 0);
93
25
  this.cols = Math.max(1, cols | 0);
94
- this.preference = resolveTerminalBackendPreference();
95
26
  this.terminal = this.createBackend();
96
- this.backendKind = this.terminal.kind;
97
27
  }
98
28
 
99
29
  reset(rows = this.rows, cols = this.cols): void {
@@ -125,11 +55,11 @@ export class TerminalScreen {
125
55
  this.terminal.dispose();
126
56
  }
127
57
 
128
- private createBackend(): TerminalViewportBackend {
129
- return createTerminalBackend({
58
+ private createBackend(): GhosttyVtTerminalBackend {
59
+ return new GhosttyVtTerminalBackend({
130
60
  cols: this.cols,
131
61
  rows: this.rows,
132
62
  scrollback: DEFAULT_SCROLLBACK,
133
- }, this.preference);
63
+ });
134
64
  }
135
65
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
3
  import * as os from 'node:os'
4
+ import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core'
4
5
  import { basename, isAbsolute, join, resolve } from 'node:path'
5
6
  import { LOG } from '../logging/logger.js'
6
7
  import type {
@@ -557,8 +558,8 @@ export async function execUnderPty(
557
558
  try {
558
559
  child = ptyLib.spawn(command, args, {
559
560
  name: 'xterm-256color',
560
- cols: 120,
561
- rows: 30,
561
+ cols: DEFAULT_SESSION_HOST_COLS,
562
+ rows: DEFAULT_SESSION_HOST_ROWS,
562
563
  cwd: options.cwd ?? process.cwd(),
563
564
  env,
564
565
  })
@@ -9,6 +9,7 @@
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import * as os from 'os';
12
+ import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
12
13
  import type * as http from 'http';
13
14
  import type { ChildProcess } from 'child_process';
14
15
  import type { DevServerContext, ProviderCategory } from './dev-server-types.js';
@@ -482,8 +483,8 @@ export async function handleAutoImplement(ctx: DevServerContext, type: string, r
482
483
  const isWin = os.platform() === 'win32';
483
484
  child = pty.spawn(isWin ? 'cmd.exe' : (process.env.SHELL || '/bin/zsh'), [isWin ? '/c' : '-c', shellCmd], {
484
485
  name: 'xterm-256color',
485
- cols: 120,
486
- rows: 40,
486
+ cols: DEFAULT_SESSION_HOST_COLS,
487
+ rows: DEFAULT_SESSION_HOST_ROWS,
487
488
  cwd: providerDir,
488
489
  env: { ...process.env, ...(spawn.env || {}) },
489
490
  });
@@ -1,65 +1,35 @@
1
1
  /**
2
- * Thin generic terminal adapter.
2
+ * PTY + terminal-screen adapter for the spec driver.
3
3
  *
4
- * Spawns a PTY, feeds bytes into a headless xterm so a visible screen
5
- * snapshot is always available, and exposes a small hook surface
6
- * (init / tick / on_screen_changed / on_exit). Nothing in this module
7
- * knows about agents, modals, or specs — it's just a terminal host.
4
+ * Spawns a PTY, feeds bytes into a TerminalScreen (ghostty-vt when available,
5
+ * xterm fallback otherwise) so a visible-screen snapshot is always available,
6
+ * and exposes a small hook surface for the SpecDriver to consume:
8
7
  *
9
- * The Spec Driver is built on top of this adapter and consumes the
10
- * hooks to drive its declarative state machine.
11
- *
12
- * Inbound from driver:
13
- * send_keys(str) raw bytes to PTY
14
- * resize(c, r) — resize the PTY and the xterm
15
- * kill() — terminate the child
16
- *
17
- * Outbound to driver:
18
- * init({ pid })
19
- * on_pty_data(chunk) — raw bytes for dashboard's terminal pane
20
- * on_screen_changed(snapshot) — coalesced visible-screen change
21
- * tick() — periodic timer (configurable interval)
22
- * on_exit({ exitCode })
8
+ * init({ pid }) — PTY spawned
9
+ * on_pty_data(chunk) — raw PTY byte chunk received
10
+ * on_screen_changed(snapshot) — coalesced visible-screen change
11
+ * on_exit({ exitCode }) — PTY exited
12
+ * tick() periodic tick (if tickIntervalMs > 0)
23
13
  */
24
14
  'use strict';
25
15
 
26
- // @xterm/headless ships as CJS only but stamps __esModule = true on its
27
- // export object, so tsup's __toESM helper neither synthesizes a `default`
28
- // property (it sees the __esModule marker and trusts it) nor preserves
29
- // the named `Terminal` export through a plain ESM-style
30
- // `import { Terminal } from '@xterm/headless'`. The first symptom we saw
31
- // from this was the published `adhdev` CLI exploding with
32
- // "Named export 'Terminal' not found" inside the mcp ipc bootstrap.
33
- //
34
- // Pull Terminal off the namespace import (which tsup compiles into a
35
- // shim that *does* see the package's own Terminal property) instead.
36
- // Type-only import keeps the .d.ts surface honest.
37
- import type { Terminal as TerminalType } from '@xterm/headless';
38
- import * as xtermHeadlessNs from '@xterm/headless';
39
- const TerminalCtor: { new (...args: unknown[]): TerminalType } =
40
- (xtermHeadlessNs as any).Terminal
41
- ?? (xtermHeadlessNs as any).default?.Terminal;
42
- import { NodePtyTransportFactory, type PtyRuntimeTransport, type PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
16
+ import { TerminalScreen } from '../../cli-adapters/terminal-screen.js';
17
+ import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
18
+ import type { PtyRuntimeTransport } from '../../cli-adapters/pty-transport.js';
19
+ import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
20
+
21
+ export type { PtyTransportFactory };
43
22
 
44
23
  export interface TerminalAdapterOpts {
45
24
  binary: string;
46
25
  args?: string[];
47
- cwd: string;
26
+ cwd?: string;
48
27
  env?: Record<string, string>;
49
28
  cols?: number;
50
29
  rows?: number;
51
30
  /** Coalesce screen snapshots: emit on_screen_changed at most this often. */
52
31
  screenChangeDebounceMs?: number;
53
- /** tick() period. 0 disables ticks. */
54
32
  tickIntervalMs?: number;
55
- /**
56
- * Optional PTY transport factory. When the daemon supplies a
57
- * SessionHostPtyTransportFactory (standalone runs the PTY inside the
58
- * session-host so the runtime/<sid>/snapshot endpoint can serve it),
59
- * forward it here. Without it the PTY spawns locally inside the
60
- * daemon process and the dashboard's terminal pane reports
61
- * "Runtime terminal unavailable: Unknown session".
62
- */
63
33
  transportFactory?: PtyTransportFactory;
64
34
  }
65
35
 
@@ -67,18 +37,18 @@ export interface TerminalAdapterHandlers {
67
37
  init?(info: { pid: number }): void;
68
38
  on_pty_data?(chunk: string): void;
69
39
  on_screen_changed?(snapshot: string): void;
70
- tick?(): void;
71
40
  on_exit?(info: { exitCode: number }): void;
41
+ tick?(): void;
72
42
  }
73
43
 
74
44
  export class TerminalAdapter {
75
- private term: TerminalType;
76
- private pty: PtyRuntimeTransport | null = null;
77
- private factory: PtyTransportFactory;
78
- private cols: number;
79
45
  private rows: number;
80
- private screenDebounceMs: number;
81
- private tickIntervalMs: number;
46
+ private cols: number;
47
+ private readonly screenDebounceMs: number;
48
+ private readonly tickIntervalMs: number;
49
+ private readonly factory: PtyTransportFactory;
50
+ private screen: TerminalScreen;
51
+ private pty: PtyRuntimeTransport | null = null;
82
52
  private screenTimer: ReturnType<typeof setTimeout> | null = null;
83
53
  private tickTimer: ReturnType<typeof setInterval> | null = null;
84
54
  private lastScreen = '';
@@ -87,17 +57,20 @@ export class TerminalAdapter {
87
57
  private readonly opts: TerminalAdapterOpts,
88
58
  private readonly handlers: TerminalAdapterHandlers,
89
59
  ) {
90
- this.cols = opts.cols ?? 100;
91
- this.rows = opts.rows ?? 30;
60
+ this.cols = opts.cols ?? DEFAULT_SESSION_HOST_COLS;
61
+ this.rows = opts.rows ?? DEFAULT_SESSION_HOST_ROWS;
92
62
  this.screenDebounceMs = opts.screenChangeDebounceMs ?? 80;
93
63
  this.tickIntervalMs = opts.tickIntervalMs ?? 0;
64
+ // Import NodePtyTransportFactory lazily to avoid loading node-pty in
65
+ // environments that don't need it (tests, fixture runners).
66
+ const { NodePtyTransportFactory } = require('../../cli-adapters/pty-transport.js');
94
67
  this.factory = opts.transportFactory ?? new NodePtyTransportFactory();
95
- this.term = new TerminalCtor({ cols: this.cols, rows: this.rows, allowProposedApi: true, scrollback: 1000 });
68
+ this.screen = new TerminalScreen(this.rows, this.cols);
96
69
  }
97
70
 
98
71
  start(): void {
99
72
  this.pty = this.factory.spawn(this.opts.binary, this.opts.args ?? [], {
100
- cwd: this.opts.cwd,
73
+ cwd: this.opts.cwd ?? process.cwd(),
101
74
  env: { ...process.env, ...(this.opts.env ?? {}) } as Record<string, string>,
102
75
  cols: this.cols,
103
76
  rows: this.rows,
@@ -114,14 +87,10 @@ export class TerminalAdapter {
114
87
  }
115
88
  }
116
89
 
117
- send_keys(s: string): void {
118
- this.pty?.write(s);
119
- }
120
-
121
90
  resize(cols: number, rows: number): void {
122
91
  this.cols = cols; this.rows = rows;
123
92
  this.pty?.resize(cols, rows);
124
- this.term.resize(cols, rows);
93
+ this.screen.resize(rows, cols);
125
94
  }
126
95
 
127
96
  snapshot(): string {
@@ -129,23 +98,24 @@ export class TerminalAdapter {
129
98
  }
130
99
 
131
100
  getCursorPosition(): { row: number; col: number } {
132
- const buf = this.term.buffer.active;
133
- return {
134
- row: Math.max(0, (buf as any).cursorY ?? 0),
135
- col: Math.max(0, (buf as any).cursorX ?? 0),
136
- };
101
+ const pos = this.screen.getCursorPosition();
102
+ return { row: pos.row, col: pos.col };
103
+ }
104
+
105
+ send_keys(text: string): void {
106
+ this.pty?.write(text);
137
107
  }
138
108
 
139
109
  kill(): void {
140
110
  this.stopTimers();
141
111
  try { this.pty?.kill(); } catch { /* ignore */ }
142
112
  this.pty = null;
143
- this.term.dispose();
113
+ this.screen.dispose();
144
114
  }
145
115
 
146
116
  private onChunk(chunk: string): void {
147
117
  try { this.handlers.on_pty_data?.(chunk); } catch { /* user side */ }
148
- this.term.write(chunk);
118
+ this.screen.write(chunk);
149
119
  // Coalesce snapshot emission — rapid bursts shouldn't fire 200x.
150
120
  if (this.screenTimer) return;
151
121
  this.screenTimer = setTimeout(() => {
@@ -158,15 +128,7 @@ export class TerminalAdapter {
158
128
  }
159
129
 
160
130
  private computeScreen(): string {
161
- const buf = this.term.buffer.active;
162
- const out: string[] = [];
163
- for (let y = 0; y < buf.length; y += 1) {
164
- const line = buf.getLine(y);
165
- if (!line) continue;
166
- out.push(line.translateToString(true));
167
- }
168
- while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
169
- return out.join('\n');
131
+ return this.screen.getText();
170
132
  }
171
133
 
172
134
  private stopTimers(): void {