@adhdev/daemon-core 0.9.82-rc.541 → 0.9.82-rc.543

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.
@@ -11,6 +11,49 @@ import type { DaemonCdpManager } from '../cdp/manager.js';
11
11
  import type { GitCompactSummary } from '../git/git-types.js';
12
12
  import type { SessionEntry } from '../shared-types.js';
13
13
  import type { ProviderState } from '../providers/provider-instance.js';
14
+ /**
15
+ * A coordinator-spawned worker session that mesh policy launched hidden. This is
16
+ * the daemon-side equivalent of the web `shouldAutoHideMeshConversation` predicate:
17
+ * these sessions should default to muted+hidden in the user dashboard (the user
18
+ * interacts through the ONE coordinator session, not each worker), while the
19
+ * coordinator↔worker mesh data/completion path (mesh-event-forwarding) is
20
+ * unaffected.
21
+ */
22
+ export declare function isCoordinatorSpawnedHiddenWorker(settings: Record<string, any> | undefined): boolean;
23
+ /**
24
+ * A session is surface-hidden (collapsed from the user's inbox/notifications) when
25
+ * mesh policy spawned it hidden, OR when a coordinator-spawned worker defaults
26
+ * hidden, OR when the user manually hid it (userHidden). userHidden === false is an
27
+ * explicit un-hide that overrides the policy/worker default until daemon restart.
28
+ */
29
+ export declare function resolveSurfaceHidden(settings: Record<string, any> | undefined): boolean;
30
+ /**
31
+ * A session is muted (attention side-effects suppressed, but still shown in the
32
+ * list) when the user muted it, OR a coordinator-spawned worker defaults muted.
33
+ * userMuted === false is an explicit un-mute overriding the worker default.
34
+ */
35
+ export declare function resolveMuted(settings: Record<string, any> | undefined): boolean;
36
+ /**
37
+ * Pure resolver for a mesh-spawned worker session's dashboard hide+mute state,
38
+ * shared by the standalone/local `buildSessionEntries` path (via the wrappers
39
+ * above, which read the values off a full settings object) AND the cloud daemon's
40
+ * synthetic remote-mesh-session mirror path in daemon-cloud
41
+ * (`appendMeshOwnedSessionsToSnapshot`), which has no full ProviderState — only
42
+ * the mesh attribution fields. Keeping both paths on this ONE helper guarantees a
43
+ * remote worker surfaced on the cloud dashboard hides+mutes identically to a local
44
+ * worker on standalone. A valid `userHidden`/`userMuted` boolean is an explicit
45
+ * per-session override and wins over the policy/worker default.
46
+ */
47
+ export declare function resolveSpawnedSessionHideMute(input: {
48
+ launchedByCoordinator?: unknown;
49
+ meshNodeFor?: unknown;
50
+ spawnedSessionVisibility?: unknown;
51
+ userHidden?: unknown;
52
+ userMuted?: unknown;
53
+ }): {
54
+ surfaceHidden: boolean;
55
+ muted: boolean;
56
+ };
14
57
  export type SessionEntryProfile = 'full' | 'live' | 'metadata';
15
58
  export interface SessionEntryBuildOptions {
16
59
  profile?: SessionEntryProfile;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.541",
3
+ "version": "0.9.82-rc.543",
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.541",
51
- "@adhdev/session-host-core": "0.9.82-rc.541",
50
+ "@adhdev/mesh-shared": "0.9.82-rc.543",
51
+ "@adhdev/session-host-core": "0.9.82-rc.543",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -34,7 +34,7 @@ import { buildMeshHostRequiredFailure, resolveMeshHostStatus } from '../mesh/mes
34
34
  import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
35
35
  import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
36
36
  import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
37
- import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
37
+ import type { RepoMeshSessionCleanupMode, RepoMeshSpawnedSessionVisibility } from '../repo-mesh-types.js';
38
38
  import { DEFAULT_MESH_POLICY, magiAutoLaunchedSessionCleanupDecision } from '../repo-mesh-types.js';
39
39
  import { resolve as pathResolve } from 'path';
40
40
  import * as fs from 'fs';
@@ -362,6 +362,30 @@ export class DaemonCommandRouter {
362
362
  return nodes;
363
363
  }
364
364
 
365
+ /**
366
+ * Same flattened node list as getCachedInlineMeshNodes(), but each node is
367
+ * paired with the `spawnedSessionVisibility` from its OWNING mesh's policy.
368
+ * The flat node list loses the mesh→node association, yet the cloud daemon's
369
+ * synthetic mesh-session mirror needs the mesh-level visibility policy to
370
+ * decide whether a coordinator-spawned worker session should be hidden+muted
371
+ * on the dashboard (the cached inline-mesh session entry carries no settings
372
+ * of its own). Falls back to DEFAULT_MESH_POLICY.spawnedSessionVisibility when
373
+ * the mesh policy is absent, matching the worker-launch stamp.
374
+ */
375
+ public getCachedInlineMeshNodesWithVisibility(): Array<{ node: any; spawnedSessionVisibility: RepoMeshSpawnedSessionVisibility }> {
376
+ const out: Array<{ node: any; spawnedSessionVisibility: RepoMeshSpawnedSessionVisibility }> = [];
377
+ for (const mesh of this.inlineMeshCache.values()) {
378
+ const spawnedSessionVisibility: RepoMeshSpawnedSessionVisibility =
379
+ mesh?.policy?.spawnedSessionVisibility === 'visible' ? 'visible' : 'hidden';
380
+ if (Array.isArray(mesh?.nodes)) {
381
+ for (const node of mesh.nodes) {
382
+ out.push({ node, spawnedSessionVisibility });
383
+ }
384
+ }
385
+ }
386
+ return out;
387
+ }
388
+
365
389
  // ─── Remote mesh-session owner resolution ───────────────────────────
366
390
  // Implementation lives in ./router-mesh-session-owner.ts (behavior-preserving
367
391
  // code move). resolveRemoteMeshSessionOwnerDaemonId stays public (the [Z]
package/src/index.ts CHANGED
@@ -384,7 +384,7 @@ export type {
384
384
 
385
385
  // ── Status ──
386
386
  export { DaemonStatusReporter } from './status/reporter.js';
387
- export { buildSessionEntries, findCdpManager, hasCdpManager, isCdpConnected } from './status/builders.js';
387
+ export { buildSessionEntries, findCdpManager, hasCdpManager, isCdpConnected, isCoordinatorSpawnedHiddenWorker, resolveSurfaceHidden, resolveMuted, resolveSpawnedSessionHideMute } from './status/builders.js';
388
388
  export { buildStatusSnapshot, buildMachineInfo, buildAvailableProviders, getLastDisplayMessage } from './status/snapshot.js';
389
389
  export { getDaemonBuildInfo } from './build-info.js';
390
390
  export type { DaemonBuildInfo } from './build-info.js';
@@ -301,11 +301,19 @@ function modalSupersededBySettledPrompt(
301
301
  if (settled.footers.length > 0 && !settled.footers.every((f) => f.test(belowText))) return false;
302
302
  // Require a real separator between the leftover box and the composer: a
303
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.
304
+ // match itself (its bare-prompt line OR one of its declared footer lines).
305
+ // That separator is the CLI's welcome banner / follow-up hint a stale box
306
+ // shows above the repainted composer. A live modal's selection cursor sits
307
+ // flush against its buttons with no such prose between them (and neither the
308
+ // cursor line nor the composer's own footer e.g. claude's "? for shortcuts"
309
+ // — is a separator), so an active modal is never misread as stale.
310
+ //
311
+ // Footer exclusion is load-bearing: without it, a live approval modal whose
312
+ // frame also carries the settled composer's footer (question + buttons +
313
+ // "? for shortcuts" + "❯") gets misread as a stale box — the footer line
314
+ // counts as fake "separator prose" — and waiting_approval flips to idle, so
315
+ // the modal is missed and the worker wedges. The footer belongs to the
316
+ // composer, not to any leftover box.
309
317
  const question = compile(modalSpec.questionPattern, modalSpec.questionFlags ?? 'i');
310
318
  const variants = (modalSpec.questionVariants ?? []).map((v) => compile(v.regex, v.flags ?? 'i'));
311
319
  const buttonFlags = modalSpec.buttonFlags && modalSpec.buttonFlags.includes('m')
@@ -324,7 +332,12 @@ function modalSupersededBySettledPrompt(
324
332
  const settledPromptLineRe = compile(settledSpec.regex, (settledSpec.flags ?? 'm').includes('m') ? (settledSpec.flags ?? 'm') : `${settledSpec.flags ?? ''}m`);
325
333
  const isSettledLine = (line: string): boolean => {
326
334
  settledPromptLineRe.lastIndex = 0;
327
- return settledPromptLineRe.test(line);
335
+ if (settledPromptLineRe.test(line)) return true;
336
+ // The composer's own declared footer (e.g. claude's "? for shortcuts") is
337
+ // part of the settled prompt block, not separator prose. Excluding it keeps
338
+ // a live approval modal that also renders the composer footer from being
339
+ // misread as a stale box.
340
+ return settled.footers.some((f) => f.test(line));
328
341
  };
329
342
  return below.some((line) => line.trim() !== '' && !isModalCueLine(line) && !isSettledLine(line));
330
343
  }
@@ -42,7 +42,7 @@ import { TEXT_ONLY_MESSAGE_INPUT_SUPPORT } from '../providers/provider-input-sup
42
42
  * coordinator↔worker mesh data/completion path (mesh-event-forwarding) is
43
43
  * unaffected.
44
44
  */
45
- function isCoordinatorSpawnedHiddenWorker(settings: Record<string, any> | undefined): boolean {
45
+ export function isCoordinatorSpawnedHiddenWorker(settings: Record<string, any> | undefined): boolean {
46
46
  if (!settings) return false;
47
47
  return settings.launchedByCoordinator === true
48
48
  && typeof settings.meshNodeFor === 'string'
@@ -56,7 +56,7 @@ function isCoordinatorSpawnedHiddenWorker(settings: Record<string, any> | undefi
56
56
  * hidden, OR when the user manually hid it (userHidden). userHidden === false is an
57
57
  * explicit un-hide that overrides the policy/worker default until daemon restart.
58
58
  */
59
- function resolveSurfaceHidden(settings: Record<string, any> | undefined): boolean {
59
+ export function resolveSurfaceHidden(settings: Record<string, any> | undefined): boolean {
60
60
  if (!settings) return false;
61
61
  if (settings.userHidden === true) return true;
62
62
  if (settings.userHidden === false) return false;
@@ -68,13 +68,37 @@ function resolveSurfaceHidden(settings: Record<string, any> | undefined): boolea
68
68
  * list) when the user muted it, OR a coordinator-spawned worker defaults muted.
69
69
  * userMuted === false is an explicit un-mute overriding the worker default.
70
70
  */
71
- function resolveMuted(settings: Record<string, any> | undefined): boolean {
71
+ export function resolveMuted(settings: Record<string, any> | undefined): boolean {
72
72
  if (!settings) return false;
73
73
  if (settings.userMuted === true) return true;
74
74
  if (settings.userMuted === false) return false;
75
75
  return isCoordinatorSpawnedHiddenWorker(settings);
76
76
  }
77
77
 
78
+ /**
79
+ * Pure resolver for a mesh-spawned worker session's dashboard hide+mute state,
80
+ * shared by the standalone/local `buildSessionEntries` path (via the wrappers
81
+ * above, which read the values off a full settings object) AND the cloud daemon's
82
+ * synthetic remote-mesh-session mirror path in daemon-cloud
83
+ * (`appendMeshOwnedSessionsToSnapshot`), which has no full ProviderState — only
84
+ * the mesh attribution fields. Keeping both paths on this ONE helper guarantees a
85
+ * remote worker surfaced on the cloud dashboard hides+mutes identically to a local
86
+ * worker on standalone. A valid `userHidden`/`userMuted` boolean is an explicit
87
+ * per-session override and wins over the policy/worker default.
88
+ */
89
+ export function resolveSpawnedSessionHideMute(input: {
90
+ launchedByCoordinator?: unknown;
91
+ meshNodeFor?: unknown;
92
+ spawnedSessionVisibility?: unknown;
93
+ userHidden?: unknown;
94
+ userMuted?: unknown;
95
+ }): { surfaceHidden: boolean; muted: boolean } {
96
+ return {
97
+ surfaceHidden: resolveSurfaceHidden(input as Record<string, any>),
98
+ muted: resolveMuted(input as Record<string, any>),
99
+ };
100
+ }
101
+
78
102
  export type SessionEntryProfile = 'full' | 'live' | 'metadata';
79
103
 
80
104
  export interface SessionEntryBuildOptions {
@@ -383,8 +407,12 @@ function buildCliSession(state: CliProviderState, options: SessionEntryBuildOpti
383
407
  settings: state.settings,
384
408
  ...(coordinator && { coordinator }),
385
409
  ...(meshQueueStats && { meshQueueStats }),
386
- ...(resolveSurfaceHidden(state.settings) && { surfaceHidden: true }),
387
- ...(resolveMuted(state.settings) && { muted: true }),
410
+ // Emit these booleans explicitly (including false) so an un-hide/un-mute clears a
411
+ // previously-true value downstream. Consumers merge with `?? existing` and copy only
412
+ // `!== undefined` fields, so an absent field on false never overwrote a prior true —
413
+ // the toggle-off direction silently stuck. See session-entry-merge.ts.
414
+ surfaceHidden: resolveSurfaceHidden(state.settings),
415
+ muted: resolveMuted(state.settings),
388
416
  };
389
417
  }
390
418
 
@@ -426,8 +454,10 @@ function buildAcpSession(state: AcpProviderState, options: SessionEntryBuildOpti
426
454
  settings: state.settings,
427
455
  ...(coordinator && { coordinator }),
428
456
  ...(meshQueueStats && { meshQueueStats }),
429
- ...(resolveSurfaceHidden(state.settings) && { surfaceHidden: true }),
430
- ...(resolveMuted(state.settings) && { muted: true }),
457
+ // Emit explicitly (including false) so un-hide/un-mute clears a prior true downstream —
458
+ // see buildCliSession above and session-entry-merge.ts.
459
+ surfaceHidden: resolveSurfaceHidden(state.settings),
460
+ muted: resolveMuted(state.settings),
431
461
  };
432
462
  }
433
463