@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
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
43
|
-
private
|
|
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 ["
|
|
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
|
|
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: '
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
51
|
+
function loadGhosttyVtBinding(): GhosttyVtBinding {
|
|
53
52
|
if (cachedBinding !== undefined) {
|
|
54
|
-
if (!cachedBinding &&
|
|
55
|
-
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
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 = '
|
|
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 {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
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
|
-
|
|
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
|
|
89
|
-
private terminal: TerminalViewportBackend;
|
|
21
|
+
private terminal: GhosttyVtTerminalBackend;
|
|
90
22
|
|
|
91
|
-
constructor(rows =
|
|
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():
|
|
129
|
-
return
|
|
58
|
+
private createBackend(): GhosttyVtTerminalBackend {
|
|
59
|
+
return new GhosttyVtTerminalBackend({
|
|
130
60
|
cols: this.cols,
|
|
131
61
|
rows: this.rows,
|
|
132
62
|
scrollback: DEFAULT_SCROLLBACK,
|
|
133
|
-
}
|
|
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:
|
|
561
|
-
rows:
|
|
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:
|
|
486
|
-
rows:
|
|
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
|
-
*
|
|
2
|
+
* PTY + terminal-screen adapter for the spec driver.
|
|
3
3
|
*
|
|
4
|
-
* Spawns a PTY, feeds bytes into a
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
|
81
|
-
private
|
|
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 ??
|
|
91
|
-
this.rows = opts.rows ??
|
|
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.
|
|
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.
|
|
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
|
|
133
|
-
return {
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 {
|