@adhdev/daemon-core 0.9.82-rc.209 → 0.9.82-rc.210

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 (64) hide show
  1. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +2 -0
  2. package/dist/commands/router.d.ts +6 -0
  3. package/dist/git/git-commands.d.ts +2 -0
  4. package/dist/git/git-diff.d.ts +6 -0
  5. package/dist/index.d.ts +11 -5
  6. package/dist/index.js +5699 -3486
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5679 -3483
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/mesh/coordinator-prompt.d.ts +6 -0
  11. package/dist/mesh/mesh-delivery-policy.d.ts +5 -0
  12. package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
  13. package/dist/mesh/mesh-events-pending.d.ts +33 -0
  14. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  15. package/dist/mesh/mesh-events-utils.d.ts +14 -0
  16. package/dist/mesh/mesh-events.d.ts +5 -198
  17. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  18. package/dist/mesh/mesh-ledger.d.ts +19 -0
  19. package/dist/mesh/mesh-missions.d.ts +58 -0
  20. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  21. package/dist/mesh/mesh-runtime-store.d.ts +175 -0
  22. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  23. package/dist/mesh/mesh-work-queue.d.ts +82 -0
  24. package/dist/mesh/refine-config.d.ts +24 -2
  25. package/dist/mesh/worktree-bootstrap-config.d.ts +22 -0
  26. package/dist/providers/acp-provider-instance.d.ts +2 -0
  27. package/dist/providers/spec/driver.d.ts +8 -0
  28. package/dist/providers/spec/evaluator.d.ts +4 -5
  29. package/dist/providers/spec/loader.d.ts +1 -0
  30. package/dist/providers/spec/schema.gen.d.ts +1409 -6
  31. package/dist/providers/spec/types.d.ts +188 -175
  32. package/dist/repo-mesh-types.d.ts +1 -0
  33. package/package.json +1 -1
  34. package/src/boot/daemon-lifecycle.ts +3 -0
  35. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +20 -7
  36. package/src/commands/router.ts +594 -66
  37. package/src/git/git-commands.ts +5 -5
  38. package/src/git/git-diff.ts +53 -0
  39. package/src/index.ts +11 -5
  40. package/src/mesh/coordinator-prompt.ts +14 -1
  41. package/src/mesh/mesh-delivery-policy.ts +17 -0
  42. package/src/mesh/mesh-events-coordinator.ts +1404 -0
  43. package/src/mesh/mesh-events-pending.ts +371 -0
  44. package/src/mesh/mesh-events-stale.ts +283 -0
  45. package/src/mesh/mesh-events-utils.ts +161 -0
  46. package/src/mesh/mesh-events.ts +27 -2143
  47. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  48. package/src/mesh/mesh-ledger.ts +134 -2
  49. package/src/mesh/mesh-missions.ts +151 -0
  50. package/src/mesh/mesh-review-inbox.ts +307 -0
  51. package/src/mesh/mesh-runtime-store.ts +539 -3
  52. package/src/mesh/mesh-task-stats.ts +154 -0
  53. package/src/mesh/mesh-work-queue.ts +233 -17
  54. package/src/mesh/refine-config.ts +42 -5
  55. package/src/mesh/worktree-bootstrap-config.ts +79 -0
  56. package/src/providers/acp-provider-instance.ts +15 -1
  57. package/src/providers/cli-provider-instance.ts +34 -13
  58. package/src/providers/spec/driver.ts +57 -29
  59. package/src/providers/spec/evaluator.ts +302 -112
  60. package/src/providers/spec/loader.ts +226 -37
  61. package/src/providers/spec/schema.gen.ts +450 -334
  62. package/src/providers/spec/schema.json +162 -75
  63. package/src/providers/spec/types.ts +234 -183
  64. package/src/repo-mesh-types.ts +1 -0
@@ -1,86 +1,9 @@
1
1
  export type Size = number | string;
2
- export interface Section {
3
- id: string;
4
- from_top?: Size;
5
- from_bottom?: Size;
6
- until?: {
7
- section: string;
8
- };
9
- /** Scan the screen for the first (or last, if anchor_last=true) line
10
- * matching this regex and use that line as the section start.
11
- * Combine with `lines`, `until_regex`, or leave alone (defaults to
12
- * end-of-screen) to control how far it extends. */
13
- anchor_regex?: string;
14
- anchor_flags?: string;
15
- /** If true, use the LAST matching line instead of the first. Useful
16
- * when the anchor pattern appears multiple times (e.g. separator
17
- * lines) and you want the one closest to the bottom. */
18
- anchor_last?: boolean;
19
- /** Optional neighbouring-line guards that must also match for the
20
- * anchor to be accepted. Reduces false positives when the anchor
21
- * pattern could appear elsewhere on screen (e.g. `>` in body text).
22
- * `prev` checks the line immediately above; `next` the line below. */
23
- anchor_context?: {
24
- prev?: string;
25
- prev_flags?: string;
26
- next?: string;
27
- next_flags?: string;
28
- };
29
- /** When used with anchor_regex: take at most this many lines from the
30
- * anchor point. */
31
- lines?: number;
32
- /** When used with anchor_regex: extend until the first line matching
33
- * this regex (exclusive). Takes precedence over `lines`. */
34
- until_regex?: string;
35
- until_regex_flags?: string;
36
- }
37
- export interface SectionRegex {
38
- section?: string;
39
- regex: string;
40
- flags?: string;
41
- /**
42
- * Optional cursor-position guards. When present, the state is only
43
- * considered matched if the terminal cursor row/column satisfies the
44
- * bounds (0-based, inclusive). Missing or undefined means "no constraint".
45
- *
46
- * Use case: distinguish modal zone from body zone for TUIs that use
47
- * cursor position rather than distinct text to locate the active prompt
48
- * (e.g. Antigravity cursor lands in modal_zone rows 8-31 when approval
49
- * is visible, never in body rows 0-7). Without this guard, body text
50
- * containing "Do you want to proceed?" could false-positive a modal match.
51
- */
52
- cursor_row_min?: number;
53
- cursor_row_max?: number;
54
- cursor_col_min?: number;
55
- cursor_col_max?: number;
56
- }
57
2
  export interface SectionPattern {
58
3
  section?: string;
59
4
  pattern: string;
60
5
  flags?: string;
61
6
  }
62
- export interface ModalButtonsRule {
63
- section?: string;
64
- /** Single pattern (original). Use `patterns` for multi-format fallback. */
65
- pattern?: string;
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
- }>;
73
- key_for_index: string;
74
- min_count?: number;
75
- continuation_lines?: boolean;
76
- }
77
- export interface SpecState {
78
- id: string;
79
- label: string;
80
- when: SectionRegex;
81
- extract_title?: SectionRegex;
82
- modal_buttons?: ModalButtonsRule;
83
- }
84
7
  export type ControlAction = {
85
8
  type: 'send_keys';
86
9
  keys: string;
@@ -117,23 +40,10 @@ export interface DelegateTrigger {
117
40
  * Native history config — three modes, picked by which fields are present:
118
41
  *
119
42
  * 1. `reader`: built-in reader id (claude-cli / codex-cli / antigravity-cli / hermes-cli).
120
- * Backwards-compatible path used by the four shipped providers.
121
43
  *
122
- * 2. `source`: declarative source descriptor. The daemon's spec native-history
123
- * executor reads the on-disk store (jsonl file / sqlite db) directly using
124
- * paths + jsonpath maps from this block. New providers should prefer this
125
- * mode — no daemon changes required.
44
+ * 2. `source`: declarative source descriptor.
126
45
  *
127
- * 3. `override_path`: relative path to a provider-supplied reader module
128
- * (e.g. "./native_history/index.js"). Escape hatch for formats too
129
- * exotic for the declarative executor. The module must default-export
130
- * a function with the signature
131
- * (input: NativeHistoryInput) => NativeHistoryResult | null
132
- * and runs under provider-script-root sandboxing (same gate as
133
- * scripts.js).
134
- *
135
- * The three modes are mutually exclusive — set exactly one. Loader rejects
136
- * specs that set zero or more than one.
46
+ * 3. `override_path`: relative path to a provider-supplied reader module.
137
47
  */
138
48
  export interface NativeHistoryConfig {
139
49
  reader?: 'claude-cli' | 'codex-cli' | 'antigravity-cli' | 'hermes-cli';
@@ -141,20 +51,6 @@ export interface NativeHistoryConfig {
141
51
  override_path?: string;
142
52
  }
143
53
  export type NativeHistorySource = NativeHistoryJsonlSource | NativeHistorySqliteSource;
144
- /**
145
- * JSONL source — each line in the resolved file is one record.
146
- *
147
- * `path` accepts ~/, environment variables (${HOME}), and a small set of
148
- * runtime variables:
149
- * {cwd} — the session's working directory, raw
150
- * {cwd_dashed} — cwd with `/` replaced by `-` (claude's per-project key)
151
- * {session_id} — providerSessionId, when known
152
- * {yyyy} {mm} {dd} — UTC date components
153
- *
154
- * When `path` resolves to a directory, the daemon picks the newest file
155
- * matching `file_pattern` whose mtime is inside `recent_window_ms` (default
156
- * 5min) — prevents the wrong session's transcript from leaking through.
157
- */
158
54
  export interface NativeHistoryJsonlSource {
159
55
  kind: 'jsonl';
160
56
  path: string;
@@ -167,12 +63,6 @@ export interface NativeHistoryJsonlSource {
167
63
  };
168
64
  message_map: NativeHistoryMessageMap;
169
65
  }
170
- /**
171
- * SQLite source — provider runs two queries on a read-only handle: one to
172
- * pick the session row (`session_query` — first row's first column is the
173
- * sessionId), then one to fetch messages (`message_query` — `?` is bound
174
- * to the sessionId). Output rows are projected through `message_map`.
175
- */
176
66
  export interface NativeHistorySqliteSource {
177
67
  kind: 'sqlite';
178
68
  path: string;
@@ -180,50 +70,200 @@ export interface NativeHistorySqliteSource {
180
70
  message_query: string;
181
71
  message_map: NativeHistoryMessageMap;
182
72
  }
183
- /**
184
- * Maps a source record (jsonl line object / sqlite row) to a daemon
185
- * native-history message. Values are jsonpath-lite strings: `$.foo.bar`
186
- * or `$.items[0].text`. Literal strings (no leading `$`) pass through.
187
- *
188
- * `content_strip` lets a spec drop wrapper tags that the agent injects
189
- * into its own transcript (claude's `<local-command-caveat>`, agy's
190
- * `<USER_REQUEST>` / `<ADDITIONAL_METADATA>` / `<USER_SETTINGS_CHANGE>`).
191
- * Each entry is a tag name; the executor removes `<tag>…</tag>` segments
192
- * non-greedily before the value is handed to the dashboard. After all
193
- * strips, leading/trailing whitespace is trimmed. If the result is empty
194
- * the message is dropped.
195
- */
196
73
  export interface NativeHistoryMessageMap {
197
74
  role: string;
198
75
  content: string;
199
- /**
200
- * Tags whose entire `<tag>…</tag>` segment is removed from content.
201
- * Use for noise wrappers the agent ships alongside the real message
202
- * (claude's `<local-command-caveat>`, agy's `<ADDITIONAL_METADATA>`).
203
- */
204
76
  content_strip?: string[];
205
- /**
206
- * Tags whose open/close markers are removed but inner content is kept.
207
- * Use when the *real* user/assistant message is wrapped in a tag
208
- * the agent uses for routing (agy wraps user input in `<USER_REQUEST>`).
209
- */
210
77
  content_unwrap?: string[];
211
78
  timestamp_ms?: string;
212
79
  kind?: string;
213
80
  }
81
+ export interface Section {
82
+ id: string;
83
+ from_top?: Size;
84
+ from_bottom?: Size;
85
+ until?: {
86
+ section: string;
87
+ };
88
+ anchor_regex?: string;
89
+ anchor_flags?: string;
90
+ anchor_last?: boolean;
91
+ anchor_context?: {
92
+ prev?: string;
93
+ prev_flags?: string;
94
+ next?: string;
95
+ next_flags?: string;
96
+ };
97
+ lines?: number;
98
+ until_regex?: string;
99
+ until_regex_flags?: string;
100
+ }
101
+ /** v1 condition — flat SectionRegex object. Also used in control_bar.wait_for. */
102
+ export interface SectionRegex {
103
+ section?: string;
104
+ regex?: string;
105
+ flags?: string;
106
+ cursor_row_min?: number;
107
+ cursor_row_max?: number;
108
+ cursor_col_min?: number;
109
+ cursor_col_max?: number;
110
+ /** v1 delta detection (cursor_above_lines + changed). */
111
+ cursor_above_lines?: number;
112
+ /** Required when cursor_above_lines is set. */
113
+ changed?: true;
114
+ }
115
+ export interface ModalButtonsRule {
116
+ section?: string;
117
+ pattern?: string;
118
+ flags?: string;
119
+ patterns?: Array<{
120
+ pattern: string;
121
+ flags?: string;
122
+ }>;
123
+ key_for_index: string;
124
+ min_count?: number;
125
+ continuation_lines?: boolean;
126
+ }
127
+ /**
128
+ * v3 section definition — keyed by id in `sections{}` object.
129
+ * `until` is EITHER a section-id string (no `^` prefix) OR a regex string (starts with `^`).
130
+ */
131
+ export interface SectionDef {
132
+ from_top?: Size;
133
+ from_bottom?: Size;
134
+ until?: string;
135
+ anchor?: string;
136
+ anchor_flags?: string;
137
+ anchor_last?: boolean;
138
+ anchor_context?: {
139
+ prev?: string;
140
+ next?: string;
141
+ prev_flags?: string;
142
+ next_flags?: string;
143
+ };
144
+ lines?: number;
145
+ until_regex?: string;
146
+ until_regex_flags?: string;
147
+ }
148
+ /** v3 regex condition against a section (or full screen). */
149
+ export interface RegexCondition {
150
+ section?: string;
151
+ matches: string;
152
+ flags?: string;
153
+ cursor_row_min?: number;
154
+ cursor_row_max?: number;
155
+ cursor_col_min?: number;
156
+ cursor_col_max?: number;
157
+ }
158
+ /** v3 delta condition: N lines above cursor changed vs prevLines. */
159
+ export interface ChangedCondition {
160
+ cursor_above: number;
161
+ changed: true;
162
+ }
163
+ /** v3 AND composite. */
164
+ export interface AllCondition {
165
+ all: Condition[];
166
+ }
167
+ /** v3 OR composite. */
168
+ export interface AnyCondition {
169
+ any: Condition[];
170
+ }
171
+ export type Condition = RegexCondition | ChangedCondition | AllCondition | AnyCondition;
172
+ /** v3 extract_title config. */
173
+ export interface ExtractTitle {
174
+ section?: string;
175
+ regex?: string;
176
+ flags?: string;
177
+ first_line?: true;
178
+ }
179
+ /** v3 extract_buttons config. */
180
+ export interface ExtractButtons {
181
+ section?: string;
182
+ pattern: string;
183
+ flags?: string;
184
+ key_for_index: string;
185
+ min_count?: number;
186
+ continuation_lines?: boolean;
187
+ }
188
+ /** v3 state. */
189
+ export interface SpecStateV3 {
190
+ id: string;
191
+ label: string;
192
+ when: AllCondition | AnyCondition;
193
+ extract?: {
194
+ title?: ExtractTitle;
195
+ buttons?: ExtractButtons;
196
+ };
197
+ }
214
198
  export interface CliSpec {
215
- $schema: 'adhdev:cli/spec@1';
199
+ $schema: 'adhdev:cli/spec@3';
216
200
  id: string;
217
201
  name: string;
218
202
  binary: string;
219
203
  spawn_args?: string[];
220
204
  env?: Record<string, string>;
205
+ cli_version_range?: string;
206
+ send_message: {
207
+ submit_key: string;
208
+ delay_ms_before_submit?: number;
209
+ delay_ms_per_char?: number;
210
+ };
211
+ sections: Record<string, SectionDef>;
212
+ states: SpecStateV3[];
213
+ default_state: string;
214
+ control_bar?: Control[];
215
+ notifications?: NotificationRule[];
216
+ delegate?: DelegateTrigger[];
217
+ native_history?: NativeHistoryConfig;
218
+ requiresFinalAssistantBeforeIdle?: boolean;
221
219
  /**
222
- * Optional version constraint this spec is authored for, e.g. ">=2.1.0".
223
- * Pure metadata used by tooling/UI the actual version-to-spec match
224
- * happens in provider-loader via the manifest's compatibility array.
225
- * Spec authors include this so a misrouted spec is easy to spot.
220
+ * Per-spec timing knobs. Replaces v1 `debounce`.
221
+ * Also accessible via the legacy `debounce` alias in the driver for
222
+ * backward compat with older callers.
226
223
  */
224
+ timing?: {
225
+ busy_hold_ms?: number;
226
+ idle_hold_ms?: number;
227
+ startup_grace_ms?: number;
228
+ completion_marker?: {
229
+ section?: string;
230
+ matches: string;
231
+ flags?: string;
232
+ hold_ms: number;
233
+ force_after_ms?: number;
234
+ };
235
+ };
236
+ /**
237
+ * Legacy alias: v1 callers (tests, tooling) may read `debounce`.
238
+ * The loader populates both `timing` and `debounce` pointing at the
239
+ * same object so callers that use either field see consistent values.
240
+ * @deprecated Use `timing` instead.
241
+ */
242
+ debounce?: {
243
+ busy_hold_ms?: number;
244
+ idle_hold_ms?: number;
245
+ startup_grace_ms?: number;
246
+ /**
247
+ * Legacy field name for completion_marker. Populated by the loader
248
+ * from `timing.completion_marker` for backward compat.
249
+ */
250
+ completion_idle_after?: {
251
+ section?: string;
252
+ regex: string;
253
+ flags?: string;
254
+ hold_ms: number;
255
+ force_after_ms?: number;
256
+ };
257
+ };
258
+ }
259
+ /** Raw v1 spec shape as loaded from JSON. Used only in migrateV1toV3. */
260
+ export interface RawCliSpecV1 {
261
+ $schema: 'adhdev:cli/spec@1';
262
+ id: string;
263
+ name: string;
264
+ binary: string;
265
+ spawn_args?: string[];
266
+ env?: Record<string, string>;
227
267
  cli_version_range?: string;
228
268
  send_message: {
229
269
  submit_key: string;
@@ -233,57 +273,30 @@ export interface CliSpec {
233
273
  layout: {
234
274
  sections: Section[];
235
275
  };
236
- states: SpecState[];
276
+ states: Array<{
277
+ id: string;
278
+ label: string;
279
+ when: SectionRegex;
280
+ extract_title?: SectionRegex;
281
+ modal_buttons?: ModalButtonsRule;
282
+ }>;
237
283
  default_state: string;
238
284
  control_bar?: Control[];
239
285
  notifications?: NotificationRule[];
240
286
  delegate?: DelegateTrigger[];
241
287
  native_history?: NativeHistoryConfig;
242
- /**
243
- * When true, the daemon's completed-finalization gate will not emit
244
- * `generating_completed` until native history confirms the last message
245
- * role is `assistant`. Prevents PTY quiet-period from triggering idle
246
- * before the CLI has finished writing its response to disk.
247
- */
248
288
  requiresFinalAssistantBeforeIdle?: boolean;
249
- /**
250
- * Per-spec debounce knobs. Defaults are conservative (busy_hold_ms
251
- * 6000) and live in SpecDriver. Spec authors override here when a
252
- * particular CLI flickers slower/faster than the default.
253
- */
254
289
  debounce?: {
255
- /** Min time to stay in busy after the evaluator last reported it.
256
- * Absorbs per-frame flicker in TUIs that stream output through
257
- * the same region as the spinner. */
258
290
  busy_hold_ms?: number;
259
- /** Min time the idle state must remain matched before it is
260
- * committed. Filters transient idle flickers that appear during
261
- * approval dismissals, layout reflows, or brief spinner gaps.
262
- * Any non-idle reading within the window cancels the transition.
263
- * When omitted the idle transition is immediate (legacy behaviour). */
264
291
  idle_hold_ms?: number;
265
- /** Min time after start() before a send_message is allowed to
266
- * reach the PTY. Banner paints + auth flows + skill listings
267
- * can keep the agent unable to accept input for several seconds
268
- * even though the idle regex matches a transient empty prompt.
269
- * Send_messages issued before this window are queued and drained
270
- * once the window passes and an idle state has actually been
271
- * observed. */
272
292
  startup_grace_ms?: number;
273
- /** Treat a provider-specific completion marker as idle after it has
274
- * remained visible for hold_ms. This handles TUIs that leave their
275
- * last spinner glyph next to a completed timer, causing the normal
276
- * busy regex to keep matching after the turn is done. */
277
293
  completion_idle_after?: {
278
294
  section?: string;
279
295
  regex: string;
280
296
  flags?: string;
281
297
  hold_ms: number;
282
- /** If the target-state check still fails this many ms after hold_ms
283
- * expired, force an idle transition anyway. Guards against TUIs that
284
- * show transient UI (pickers, option lists) after the completion
285
- * marker appears, keeping the target-state regex from matching. */
286
298
  force_after_ms?: number;
287
299
  };
288
300
  };
289
301
  }
302
+ export type SpecState = SpecStateV3;
@@ -361,6 +361,7 @@ export interface RepoMeshNodeStatus {
361
361
  worktreeBootstrap?: LocalMeshNodeEntry['worktreeBootstrap'];
362
362
  launchBlockedReason?: string;
363
363
  launchBlockedMessage?: string;
364
+ recoveryHint?: string;
364
365
  lastSeenAt?: string;
365
366
  updatedAt?: string;
366
367
  connection?: RepoMeshPeerConnectionStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.209",
3
+ "version": "0.9.82-rc.210",
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",
@@ -353,6 +353,9 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
353
353
  // 11. Setup Mesh Event Forwarding
354
354
  setupMeshEventForwarding(components);
355
355
 
356
+ // 12. Resume any refine jobs that were interrupted by a previous daemon restart.
357
+ setImmediate(() => void router.resumePendingRefineJobsOnStartup());
358
+
356
359
  return components;
357
360
  }
358
361
 
@@ -83,45 +83,58 @@ function loadGhosttyVtBinding(): GhosttyVtBinding {
83
83
  export class GhosttyVtTerminalBackend implements TerminalViewportBackend {
84
84
  readonly kind = 'ghostty-vt' as const;
85
85
  private terminal: GhosttyVtTerminal;
86
+ private rows: number;
87
+ private disposed = false;
86
88
 
87
89
  constructor(options: TerminalViewportBackendOptions) {
88
90
  const binding = loadGhosttyVtBinding();
91
+ this.rows = Math.max(1, options.rows | 0);
89
92
  this.terminal = binding.createTerminal({
90
93
  cols: Math.max(1, options.cols | 0),
91
- rows: Math.max(1, options.rows | 0),
94
+ rows: this.rows,
92
95
  scrollback: Math.max(0, options.scrollback | 0),
93
96
  });
94
97
  }
95
98
 
96
99
  resize(rows: number, cols: number): void {
97
- this.terminal.resize(Math.max(1, cols | 0), Math.max(1, rows | 0));
100
+ this.rows = Math.max(1, rows | 0);
101
+ this.terminal.resize(Math.max(1, cols | 0), this.rows);
98
102
  }
99
103
 
100
104
  write(data: string): void {
101
- if (!data) return;
105
+ if (!data || this.disposed) return;
102
106
  this.terminal.write(data);
103
107
  }
104
108
 
105
109
  getText(): string {
110
+ if (this.disposed) return '';
106
111
  // ghostty's `trim:true` collapses CUF-advanced cells — many TUIs including
107
112
  // Claude Code render spaces via cursor-forward rather than literal spaces,
108
113
  // which would break downstream regex matching. Keep per-row padding and
109
114
  // trim trailing whitespace ourselves.
115
+ //
116
+ // formatPlainText uses a .screen pin which includes scrollback history.
117
+ // Slice to the last `rows` lines to get only the visible viewport.
110
118
  const raw = this.terminal.formatPlainText({ trim: false }) || '';
111
119
  if (!raw) return '';
112
120
  const lines = raw.split('\n').map((row) => row.replace(/\s+$/, ''));
121
+ // Take only the viewport (last `rows` lines) to exclude scrollback history.
122
+ const viewport = lines.length > this.rows ? lines.slice(-this.rows) : lines;
113
123
  let first = 0;
114
- let last = lines.length;
115
- while (first < last && !lines[first]) first += 1;
116
- while (last > first && !lines[last - 1]) last -= 1;
117
- return lines.slice(first, last).join('\n');
124
+ let last = viewport.length;
125
+ while (first < last && !viewport[first]) first += 1;
126
+ while (last > first && !viewport[last - 1]) last -= 1;
127
+ return viewport.slice(first, last).join('\n');
118
128
  }
119
129
 
120
130
  getCursorPosition(): { col: number; row: number } {
131
+ if (this.disposed) return { col: 0, row: 0 };
121
132
  return this.terminal.getCursorPosition();
122
133
  }
123
134
 
124
135
  dispose(): void {
136
+ if (this.disposed) return;
137
+ this.disposed = true;
125
138
  this.terminal.dispose();
126
139
  }
127
140
  }