@adhdev/daemon-core 0.9.82-rc.536 → 0.9.82-rc.537

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.536",
3
+ "version": "0.9.82-rc.537",
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",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "0.9.82-rc.536",
51
- "@adhdev/session-host-core": "0.9.82-rc.536",
50
+ "@adhdev/mesh-shared": "0.9.82-rc.537",
51
+ "@adhdev/session-host-core": "0.9.82-rc.537",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -356,6 +356,13 @@ export class ProviderCliAdapter implements CliAdapter {
356
356
  const currentScreenLooksIdle = /(?:^|\n|\r)\s*[❯›>]\s*(?:Try\s+["“][^\n\r"”]+["”])?\s*(?:\n|\r|$)/.test(screenText)
357
357
  && !activeScreenPattern.test(screenText);
358
358
  if (staleSnapshotLooksActive && currentScreenLooksIdle) return screenText;
359
+ // When the current frame already resolves to a settled/idle prompt via the
360
+ // provider's own detector, do NOT graft an older snapshot onto it: the
361
+ // older frame can carry a dead modal box (e.g. cursor-agent's leftover
362
+ // "Workspace Trust Required" rows) that re-fires `waiting_approval` at the
363
+ // appended tail and wedges the turn in `generating`. A truly idle current
364
+ // screen needs no historical supplement.
365
+ if (this.runDetectStatus(screenText) === 'idle') return screenText;
359
366
  if (currentSnapshot.length >= lastSnapshot.length) return screenText;
360
367
  // Terminal screen reads can miss a just-rendered completed Hermes box while
361
368
  // the normalized snapshot captured during output still has it. Feed both
@@ -227,6 +227,108 @@ function modalMatches(spec: ModalSpec, input: CliStatusInput): boolean {
227
227
  return false;
228
228
  }
229
229
 
230
+ /**
231
+ * Index (line number) of the last line in `screenText` that carries a modal
232
+ * cue — question line, question variant, or a button-block label line — or -1
233
+ * if none. Used to detect a *stale* modal box: some CLIs (e.g. cursor-agent's
234
+ * "Workspace Trust Required" prompt) never clear their box rows after the user
235
+ * answers. The redraw that replaces the modal with the idle composer is shorter
236
+ * than the box, so the top modal rows linger in the terminal grid. Without a
237
+ * spatial check, the unscoped whole-screen `modalMatches` keeps firing
238
+ * `waiting_approval` forever and the session wedges in `starting` — the
239
+ * startup gate never releases because `detectStatus` never returns `idle`.
240
+ */
241
+ function lastModalCueLine(spec: ModalSpec, screenText: string): number {
242
+ if (!screenText) return -1;
243
+ const lines = screenText.split('\n');
244
+ const question = compile(spec.questionPattern, spec.questionFlags ?? 'i');
245
+ const variants = (spec.questionVariants ?? []).map((v) => compile(v.regex, v.flags ?? 'i'));
246
+ const buttonFlags = spec.buttonFlags && spec.buttonFlags.includes('m')
247
+ ? spec.buttonFlags
248
+ : `${spec.buttonFlags ?? ''}m`;
249
+ const buttonRe = compile(spec.buttonPattern, buttonFlags);
250
+ let last = -1;
251
+ for (let i = 0; i < lines.length; i++) {
252
+ const line = lines[i];
253
+ question.lastIndex = 0;
254
+ if (question.test(line)) { last = i; continue; }
255
+ if (variants.some((re) => { re.lastIndex = 0; return re.test(line); })) { last = i; continue; }
256
+ buttonRe.lastIndex = 0;
257
+ if (buttonRe.test(line)) { last = i; continue; }
258
+ }
259
+ return last;
260
+ }
261
+
262
+ /**
263
+ * True when the modal cue is *stale* — a leftover box the CLI failed to clear —
264
+ * because the live idle composer has repainted BELOW it.
265
+ *
266
+ * The discriminator is spatial, so a live modal (whose own selection cursor /
267
+ * button block can incidentally match a settled-prompt regex) is NOT mistaken
268
+ * for stale:
269
+ *
270
+ * - A settled-prompt cue must match strictly BELOW the last modal cue line.
271
+ * - AND at least one non-blank line between them is neither a modal cue nor
272
+ * part of the settled-prompt match itself (the separator prose).
273
+ *
274
+ * A live modal renders its question + button block FLUSH against its own
275
+ * composer/selection cursor (no intervening prose). A stale box, by contrast,
276
+ * has the CLI's welcome banner / follow-up hint / mode footer repainted between
277
+ * the leftover box rows and the live composer. So the discriminator is: a
278
+ * settled-prompt cue matches strictly BELOW the last modal cue line AND at least
279
+ * one non-blank, non-modal line separates them. That separator is exactly the
280
+ * content a live modal never has between its buttons and its cursor, and it is
281
+ * robust to the terminal-snapshot append that can shuffle the tail window.
282
+ */
283
+ function modalSupersededBySettledPrompt(
284
+ modalSpec: ModalSpec,
285
+ settledSpec: SettledPromptSpec | undefined,
286
+ settled: ReturnType<typeof compileSettledPromptMatchers> | null,
287
+ input: CliStatusInput,
288
+ ): boolean {
289
+ if (!settled || !settledSpec) return false;
290
+ if (settledSpec.scope === 'whole-screen') return false;
291
+ const screenText = input.screenText ?? '';
292
+ if (!screenText) return false;
293
+ const modalLine = lastModalCueLine(modalSpec, screenText);
294
+ if (modalLine < 0) return false;
295
+ const lines = screenText.split('\n');
296
+ const below = lines.slice(modalLine + 1);
297
+ if (below.length === 0) return false;
298
+ // A settled prompt (composer) must render somewhere below the modal box.
299
+ const belowText = below.join('\n');
300
+ if (!settled.prompt.test(belowText)) return false;
301
+ if (settled.footers.length > 0 && !settled.footers.every((f) => f.test(belowText))) return false;
302
+ // Require a real separator between the leftover box and the composer: a
303
+ // non-blank line that is neither a modal cue NOR part of the settled-prompt
304
+ // match itself. That separator is the CLI's welcome banner / follow-up hint /
305
+ // mode footer a stale box shows above the repainted composer. A live modal's
306
+ // selection cursor sits flush against its buttons with no such prose between
307
+ // them (and the cursor line, even if it matches the settled regex, is not a
308
+ // separator), so an active modal is never misread as stale.
309
+ const question = compile(modalSpec.questionPattern, modalSpec.questionFlags ?? 'i');
310
+ const variants = (modalSpec.questionVariants ?? []).map((v) => compile(v.regex, v.flags ?? 'i'));
311
+ const buttonFlags = modalSpec.buttonFlags && modalSpec.buttonFlags.includes('m')
312
+ ? modalSpec.buttonFlags
313
+ : `${modalSpec.buttonFlags ?? ''}m`;
314
+ const buttonRe = compile(modalSpec.buttonPattern, buttonFlags);
315
+ const isModalCueLine = (line: string): boolean => {
316
+ question.lastIndex = 0;
317
+ if (question.test(line)) return true;
318
+ if (variants.some((re) => { re.lastIndex = 0; return re.test(line); })) return true;
319
+ buttonRe.lastIndex = 0;
320
+ return buttonRe.test(line);
321
+ };
322
+ // A single-line settled regex would let a lone match count as its own line;
323
+ // test each below-line against the prompt regex on that line alone.
324
+ const settledPromptLineRe = compile(settledSpec.regex, (settledSpec.flags ?? 'm').includes('m') ? (settledSpec.flags ?? 'm') : `${settledSpec.flags ?? ''}m`);
325
+ const isSettledLine = (line: string): boolean => {
326
+ settledPromptLineRe.lastIndex = 0;
327
+ return settledPromptLineRe.test(line);
328
+ };
329
+ return below.some((line) => line.trim() !== '' && !isModalCueLine(line) && !isSettledLine(line));
330
+ }
331
+
230
332
  // ─── Public builder ────────────────────────────────────────────────────
231
333
 
232
334
  const DEFAULT_ORDER: DispatchGroup[] = ['spinner', 'modal', 'settled-prompt'];
@@ -248,7 +350,12 @@ function evaluateGroup(
248
350
  }
249
351
  case 'modal': {
250
352
  if (!spec.modal) return null;
251
- return modalMatches(spec.modal, input) ? 'waiting_approval' : null;
353
+ if (!modalMatches(spec.modal, input)) return null;
354
+ // A modal cue with the live composer repainted below it (in the settled
355
+ // prompt's own tail scope) is a stale box the CLI failed to clear — yield
356
+ // so settled-prompt/idle can win.
357
+ if (modalSupersededBySettledPrompt(spec.modal, spec.settledPrompt, compiled.settled, input)) return null;
358
+ return 'waiting_approval';
252
359
  }
253
360
  case 'settled-prompt': {
254
361
  if (!spec.settledPrompt || !compiled.settled) return null;