@adhdev/daemon-core 0.9.82-rc.188 → 0.9.82-rc.189

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.
@@ -49,6 +49,10 @@ export declare class TerminalAdapter {
49
49
  send_keys(s: string): void;
50
50
  resize(cols: number, rows: number): void;
51
51
  snapshot(): string;
52
+ getCursorPosition(): {
53
+ row: number;
54
+ col: number;
55
+ };
52
56
  kill(): void;
53
57
  private onChunk;
54
58
  private computeScreen;
@@ -139,6 +139,10 @@ export declare class SpecDriver {
139
139
  start(): void;
140
140
  dispatch(cmd: DashboardCommand): void;
141
141
  snapshot(): string;
142
+ getCursorPosition(): {
143
+ row: number;
144
+ col: number;
145
+ };
142
146
  shutdown(): void;
143
147
  private loadSpecOrThrow;
144
148
  private buildAdapterOpts;
@@ -44,4 +44,12 @@ export interface SpecEvaluation {
44
44
  sections: ResolvedSection[];
45
45
  trace: TraceEntry[];
46
46
  }
47
- export declare function evaluate(spec: CliSpec, screenText: string): SpecEvaluation;
47
+ export declare function evaluate(spec: CliSpec, screenText: string,
48
+ /** Optional cursor position (0-based row and col). When supplied, states
49
+ * with cursor_row_min/max or cursor_col_min/max predicates are filtered.
50
+ * When omitted, cursor predicates are ignored and evaluation is text-only
51
+ * (backward-compatible with all existing specs and call sites). */
52
+ cursor?: {
53
+ row: number;
54
+ col: number;
55
+ }): SpecEvaluation;
@@ -203,6 +203,22 @@ export declare const SCHEMA: {
203
203
  readonly type: "string";
204
204
  readonly default: "i";
205
205
  };
206
+ readonly cursor_row_min: {
207
+ readonly type: "integer";
208
+ readonly minimum: 0;
209
+ };
210
+ readonly cursor_row_max: {
211
+ readonly type: "integer";
212
+ readonly minimum: 0;
213
+ };
214
+ readonly cursor_col_min: {
215
+ readonly type: "integer";
216
+ readonly minimum: 0;
217
+ };
218
+ readonly cursor_col_max: {
219
+ readonly type: "integer";
220
+ readonly minimum: 0;
221
+ };
206
222
  };
207
223
  };
208
224
  readonly sectionPattern: {
@@ -11,6 +11,21 @@ export interface SectionRegex {
11
11
  section?: string;
12
12
  regex: string;
13
13
  flags?: string;
14
+ /**
15
+ * Optional cursor-position guards. When present, the state is only
16
+ * considered matched if the terminal cursor row/column satisfies the
17
+ * bounds (0-based, inclusive). Missing or undefined means "no constraint".
18
+ *
19
+ * Use case: distinguish modal zone from body zone for TUIs that use
20
+ * cursor position rather than distinct text to locate the active prompt
21
+ * (e.g. Antigravity cursor lands in modal_zone rows 8-31 when approval
22
+ * is visible, never in body rows 0-7). Without this guard, body text
23
+ * containing "Do you want to proceed?" could false-positive a modal match.
24
+ */
25
+ cursor_row_min?: number;
26
+ cursor_row_max?: number;
27
+ cursor_col_min?: number;
28
+ cursor_col_max?: number;
14
29
  }
15
30
  export interface SectionPattern {
16
31
  section?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.188",
3
+ "version": "0.9.82-rc.189",
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",
@@ -1962,6 +1962,11 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1962
1962
  const nodeId = readNonEmptyString(payload.nodeId);
1963
1963
  const workspace = readNonEmptyString(payload.workspace);
1964
1964
  const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
1965
+ const relayModalMessage = readNonEmptyString(payload.modalMessage);
1966
+ const relayModalButtons = Array.isArray(payload.modalButtons)
1967
+ ? (payload.modalButtons as unknown[]).filter((b): b is string => typeof b === 'string' && b.trim().length > 0)
1968
+ : null;
1969
+
1965
1970
  return injectMeshSystemMessage(components, {
1966
1971
  meshId,
1967
1972
  nodeId,
@@ -1979,6 +1984,8 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
1979
1984
  startedAt: readNonEmptyString(payload.startedAt),
1980
1985
  completedAt: readNonEmptyString(payload.completedAt),
1981
1986
  retryOfJobId: readNonEmptyString(payload.retryOfJobId),
1987
+ ...(relayModalMessage ? { modalMessage: relayModalMessage } : {}),
1988
+ ...(relayModalButtons && relayModalButtons.length > 0 ? { modalButtons: relayModalButtons } : {}),
1982
1989
  ...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
1983
1990
  ...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
1984
1991
  ...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
@@ -128,6 +128,14 @@ export class TerminalAdapter {
128
128
  return this.lastScreen || this.computeScreen();
129
129
  }
130
130
 
131
+ 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
+ };
137
+ }
138
+
131
139
  kill(): void {
132
140
  this.stopTimers();
133
141
  try { this.pty?.kill(); } catch { /* ignore */ }
@@ -247,6 +247,10 @@ export class SpecDriver {
247
247
  return this.adapter.snapshot();
248
248
  }
249
249
 
250
+ getCursorPosition(): { row: number; col: number } {
251
+ return this.adapter.getCursorPosition();
252
+ }
253
+
250
254
  shutdown(): void {
251
255
  for (const t of this.delegateTimers.values()) clearTimeout(t);
252
256
  this.delegateTimers.clear();
@@ -316,7 +320,8 @@ export class SpecDriver {
316
320
 
317
321
  private reevaluate(forceEmit = false): void {
318
322
  const screen = this.adapter.snapshot();
319
- const ev = evaluate(this.spec, screen);
323
+ const cursor = this.adapter.getCursorPosition();
324
+ const ev = evaluate(this.spec, screen, cursor);
320
325
 
321
326
  // Busy hold: many TUIs flicker between busy and idle every frame
322
327
  // (claude in particular — its token counter appears and disappears
@@ -139,6 +139,7 @@ function matchState(
139
139
  sections: ResolvedSection[],
140
140
  fullScreen: string,
141
141
  trace: TraceEntry[],
142
+ cursor?: { row: number; col: number },
142
143
  ): { matched: boolean; title: string | null } {
143
144
  const haystack = sectionText(sections, state.when.section, fullScreen);
144
145
  const re = compileRegex(state.when);
@@ -146,7 +147,31 @@ function matchState(
146
147
  trace.push({ kind: 'state_skip', text: `state[${state.id}] when ${state.when.section ?? '*'}~/${state.when.regex}/ no match` });
147
148
  return { matched: false, title: null };
148
149
  }
149
- trace.push({ kind: 'state_match', text: `state[${state.id}] matched via ${state.when.section ?? '*'}~/${state.when.regex}/` });
150
+
151
+ // Cursor-position guards: check row/col bounds when the state declares them.
152
+ // Guards are skipped entirely when the caller did not supply a cursor position
153
+ // (cursor === undefined) so existing pure-text evaluation is unaffected.
154
+ if (cursor !== undefined) {
155
+ const w = state.when;
156
+ if (w.cursor_row_min !== undefined && cursor.row < w.cursor_row_min) {
157
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor row ${cursor.row} < cursor_row_min ${w.cursor_row_min}` });
158
+ return { matched: false, title: null };
159
+ }
160
+ if (w.cursor_row_max !== undefined && cursor.row > w.cursor_row_max) {
161
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor row ${cursor.row} > cursor_row_max ${w.cursor_row_max}` });
162
+ return { matched: false, title: null };
163
+ }
164
+ if (w.cursor_col_min !== undefined && cursor.col < w.cursor_col_min) {
165
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor col ${cursor.col} < cursor_col_min ${w.cursor_col_min}` });
166
+ return { matched: false, title: null };
167
+ }
168
+ if (w.cursor_col_max !== undefined && cursor.col > w.cursor_col_max) {
169
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor col ${cursor.col} > cursor_col_max ${w.cursor_col_max}` });
170
+ return { matched: false, title: null };
171
+ }
172
+ }
173
+
174
+ trace.push({ kind: 'state_match', text: `state[${state.id}] matched via ${state.when.section ?? '*'}~/${state.when.regex}/${cursor !== undefined ? ` cursor=(${cursor.row},${cursor.col})` : ''}` });
150
175
 
151
176
  let title: string | null = null;
152
177
  if (state.extract_title) {
@@ -217,19 +242,30 @@ function extractModal(
217
242
  // Public evaluator
218
243
  // ────────────────────────────────────────────────────────────────────────────
219
244
 
220
- export function evaluate(spec: CliSpec, screenText: string): SpecEvaluation {
245
+ export function evaluate(
246
+ spec: CliSpec,
247
+ screenText: string,
248
+ /** Optional cursor position (0-based row and col). When supplied, states
249
+ * with cursor_row_min/max or cursor_col_min/max predicates are filtered.
250
+ * When omitted, cursor predicates are ignored and evaluation is text-only
251
+ * (backward-compatible with all existing specs and call sites). */
252
+ cursor?: { row: number; col: number },
253
+ ): SpecEvaluation {
221
254
  const trace: TraceEntry[] = [];
222
255
  const lines = screenText.split('\n');
223
256
  const sections = resolveSections(spec, lines);
224
257
  for (const s of sections) {
225
258
  trace.push({ kind: 'section', text: `section[${s.id}] lines [${s.fromLine}, ${s.toLine}) (${s.toLine - s.fromLine} lines)` });
226
259
  }
260
+ if (cursor !== undefined) {
261
+ trace.push({ kind: 'section', text: `cursor (${cursor.row}, ${cursor.col})` });
262
+ }
227
263
 
228
264
  let activeState: { id: string; label: string; title: string | null } | null = null;
229
265
  let modal: ModalSnapshot | null = null;
230
266
 
231
267
  for (const st of spec.states) {
232
- const { matched, title } = matchState(st, sections, screenText, trace);
268
+ const { matched, title } = matchState(st, sections, screenText, trace, cursor);
233
269
  if (!matched) continue;
234
270
  const extractedModal = extractModal(st, sections, screenText, title, trace);
235
271
  // If the state declares modal_buttons but extraction failed (button
@@ -207,6 +207,22 @@ export const SCHEMA = {
207
207
  "flags": {
208
208
  "type": "string",
209
209
  "default": "i"
210
+ },
211
+ "cursor_row_min": {
212
+ "type": "integer",
213
+ "minimum": 0
214
+ },
215
+ "cursor_row_max": {
216
+ "type": "integer",
217
+ "minimum": 0
218
+ },
219
+ "cursor_col_min": {
220
+ "type": "integer",
221
+ "minimum": 0
222
+ },
223
+ "cursor_col_max": {
224
+ "type": "integer",
225
+ "minimum": 0
210
226
  }
211
227
  }
212
228
  },
@@ -111,7 +111,11 @@
111
111
  "properties": {
112
112
  "section": { "type": "string" },
113
113
  "regex": { "type": "string", "minLength": 1 },
114
- "flags": { "type": "string", "default": "i" }
114
+ "flags": { "type": "string", "default": "i" },
115
+ "cursor_row_min": { "type": "integer", "minimum": 0 },
116
+ "cursor_row_max": { "type": "integer", "minimum": 0 },
117
+ "cursor_col_min": { "type": "integer", "minimum": 0 },
118
+ "cursor_col_max": { "type": "integer", "minimum": 0 }
115
119
  }
116
120
  },
117
121
  "sectionPattern": {
@@ -19,6 +19,21 @@ export interface SectionRegex {
19
19
  section?: string;
20
20
  regex: string;
21
21
  flags?: string;
22
+ /**
23
+ * Optional cursor-position guards. When present, the state is only
24
+ * considered matched if the terminal cursor row/column satisfies the
25
+ * bounds (0-based, inclusive). Missing or undefined means "no constraint".
26
+ *
27
+ * Use case: distinguish modal zone from body zone for TUIs that use
28
+ * cursor position rather than distinct text to locate the active prompt
29
+ * (e.g. Antigravity cursor lands in modal_zone rows 8-31 when approval
30
+ * is visible, never in body rows 0-7). Without this guard, body text
31
+ * containing "Do you want to proceed?" could false-positive a modal match.
32
+ */
33
+ cursor_row_min?: number;
34
+ cursor_row_max?: number;
35
+ cursor_col_min?: number;
36
+ cursor_col_max?: number;
22
37
  }
23
38
 
24
39
  export interface SectionPattern {