@adhdev/daemon-core 0.9.82-rc.317 → 0.9.82-rc.319

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.
@@ -52,6 +52,7 @@ import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport }
52
52
  import type { ProviderInstance, ProviderState, AcpProviderState, ProviderErrorReason, ProviderEvent, InstanceContext, SessionModalState } from './provider-instance.js';
53
53
  import { StatusMonitor } from './status-monitor.js';
54
54
  import { buildLegacyModelModeSummaryMetadata } from './summary-metadata.js';
55
+ import { workingDirBasename } from './working-dir.js';
55
56
  import {
56
57
  buildAssistantChatMessage,
57
58
  buildChatMessage,
@@ -345,7 +346,7 @@ export class AcpProviderInstance implements ProviderInstance {
345
346
  }
346
347
 
347
348
  getSessionModalState(): SessionModalState {
348
- const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
349
+ const dirName = workingDirBasename(this.workingDir);
349
350
  return {
350
351
  id: this.instanceId,
351
352
  status: this.currentStatus,
@@ -358,7 +359,7 @@ export class AcpProviderInstance implements ProviderInstance {
358
359
  }
359
360
 
360
361
  getState(): AcpProviderState {
361
- const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
362
+ const dirName = workingDirBasename(this.workingDir);
362
363
 
363
364
  const recentMessages = normalizeChatMessages(this.messages.map(m => {
364
365
  const content = m.content;
@@ -1504,7 +1505,7 @@ export class AcpProviderInstance implements ProviderInstance {
1504
1505
  private detectStatusTransition(): void {
1505
1506
  const now = Date.now();
1506
1507
  const newStatus = this.currentStatus;
1507
- const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
1508
+ const dirName = workingDirBasename(this.workingDir);
1508
1509
  const chatTitle = `${this.provider.name} · ${dirName}`;
1509
1510
  const progressFingerprint = newStatus === 'generating'
1510
1511
  ? `${this.partialContent}::${JSON.stringify(this.partialBlocks)}::${JSON.stringify(this.activeToolCalls.map(t => ({ name: t.name, status: t.status })))}`.slice(-2000)
@@ -28,6 +28,7 @@ import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.
28
28
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
29
29
  import { normalizeProviderSessionId } from './provider-session-id.js';
30
30
  import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
31
+ import { workingDirBasename } from './working-dir.js';
31
32
 
32
33
  type PersistableCliHistoryMessage = {
33
34
  role: string;
@@ -685,7 +686,7 @@ export class CliProviderInstance implements ProviderInstance {
685
686
  }))
686
687
  : mergedMessages;
687
688
 
688
- const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
689
+ const dirName = workingDirBasename(this.workingDir);
689
690
  const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
690
691
  ? parsedStatus.status.trim()
691
692
  : undefined;
@@ -827,7 +828,7 @@ export class CliProviderInstance implements ProviderInstance {
827
828
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
828
829
  const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
829
830
  const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
830
- const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
831
+ const dirName = workingDirBasename(this.workingDir);
831
832
  return {
832
833
  // Honor the caller-supplied sessionId — InstanceMgr rejects the
833
834
  // projection when projected.id !== requested sessionId, and
@@ -1541,7 +1542,7 @@ export class CliProviderInstance implements ProviderInstance {
1541
1542
  // transcript shape does NOT override the FSM's busy/idle decision.
1542
1543
  const autoApproveHoldIdle = this.autoApproveBusy && rawStatus === 'idle';
1543
1544
  const newStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus;
1544
- const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
1545
+ const dirName = workingDirBasename(this.workingDir);
1545
1546
  const chatTitle = `${this.provider.name} · ${dirName}`;
1546
1547
  const partial = this.adapter.getPartialResponse();
1547
1548
  const progressFingerprint = newStatus === 'generating'
@@ -2053,7 +2054,7 @@ export class CliProviderInstance implements ProviderInstance {
2053
2054
  receivedAt: normalizedMessage.receivedAt || normalizedMessage.timestamp,
2054
2055
  historyDedupKey: dedupKey,
2055
2056
  }],
2056
- this.adapter.getScriptParsedStatus?.()?.title || this.workingDir.split('/').filter(Boolean).pop() || 'session',
2057
+ this.adapter.getScriptParsedStatus?.()?.title || workingDirBasename(this.workingDir),
2057
2058
  this.instanceId,
2058
2059
  this.providerSessionId,
2059
2060
  );
@@ -25,6 +25,16 @@ export interface TerminalAdapterOpts {
25
25
  args?: string[];
26
26
  cwd?: string;
27
27
  env?: Record<string, string>;
28
+ /**
29
+ * When true, `env` is already a COMPLETE, sanitized environment (the spawn
30
+ * planner merged + stripped process.env already) and must be passed to the
31
+ * PTY verbatim — NOT overlaid on top of process.env. Overlaying would
32
+ * re-introduce the npm_/PNPM_/parent-session keys the planner explicitly
33
+ * stripped, so the spec path's spawn env would diverge from the legacy
34
+ * path's. Defaults to false (legacy overlay behaviour) for any caller that
35
+ * still passes a partial env.
36
+ */
37
+ envIsComplete?: boolean;
28
38
  cols?: number;
29
39
  rows?: number;
30
40
  /** Coalesce screen snapshots: emit on_screen_changed at most this often. */
@@ -76,9 +86,12 @@ export class TerminalAdapter {
76
86
  }
77
87
 
78
88
  start(): void {
89
+ const env = this.opts.envIsComplete
90
+ ? ((this.opts.env ?? {}) as Record<string, string>)
91
+ : ({ ...process.env, ...(this.opts.env ?? {}) } as Record<string, string>);
79
92
  this.pty = this.factory.spawn(this.opts.binary, this.opts.args ?? [], {
80
93
  cwd: this.opts.cwd ?? process.cwd(),
81
- env: { ...process.env, ...(this.opts.env ?? {}) } as Record<string, string>,
94
+ env,
82
95
  cols: this.cols,
83
96
  rows: this.rows,
84
97
  });
@@ -22,6 +22,7 @@ import * as fs from 'node:fs';
22
22
  import * as os from 'node:os';
23
23
  import * as path from 'node:path';
24
24
  import { TerminalAdapter, type TerminalAdapterOpts } from './adapter.js';
25
+ import { resolveCliSpawnPlanFromParts } from '../../cli-adapters/provider-cli-runtime.js';
25
26
  import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
26
27
  import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
27
28
  import {
@@ -389,15 +390,35 @@ export class FsmDriver implements ISpecDriver {
389
390
  }
390
391
 
391
392
  private buildAdapterOpts(): TerminalAdapterOpts {
392
- const baseArgs = this.spec.spawn_args ?? [];
393
- const extra = this.opts.extraCliArgs ?? [];
393
+ // Single-source spawn resolution: route the spec's binary/args/env
394
+ // through the SAME planner the legacy ProviderCliAdapter uses
395
+ // (resolveCliSpawnPlanFromParts). This gives the spec/FSM path
396
+ // findBinary (PATH + npm-global / Node-dir fallback so an off-PATH
397
+ // `codex`/`claude` resolves), `{{workingDir}}` token substitution, shell
398
+ // wrapping for script-shims / non-absolute / non-native binaries, and a
399
+ // sanitized env with TERMINAL_CWD — none of which it had when it passed
400
+ // `this.spec.binary` straight to the PTY.
401
+ const cols = this.opts.cols ?? DEFAULT_SESSION_HOST_COLS;
402
+ const rows = this.opts.rows ?? DEFAULT_SESSION_HOST_ROWS;
403
+ const plan = resolveCliSpawnPlanFromParts({
404
+ command: this.spec.binary,
405
+ baseArgs: this.spec.spawn_args ?? [],
406
+ baseEnv: this.spec.env ?? {},
407
+ workingDir: this.opts.workingDir,
408
+ extraArgs: this.opts.extraCliArgs ?? [],
409
+ extraEnv: this.opts.extraEnv ?? {},
410
+ geometry: { cols, rows },
411
+ });
394
412
  return {
395
- binary: this.spec.binary,
396
- args: [...baseArgs, ...extra],
397
- cwd: this.opts.workingDir,
398
- env: { ...(this.spec.env ?? {}), ...(this.opts.extraEnv ?? {}) },
399
- cols: this.opts.cols ?? DEFAULT_SESSION_HOST_COLS,
400
- rows: this.opts.rows ?? DEFAULT_SESSION_HOST_ROWS,
413
+ binary: plan.shellCmd,
414
+ args: plan.shellArgs,
415
+ cwd: plan.ptyOptions.cwd,
416
+ // plan.ptyOptions.env is already a complete, sanitized environment
417
+ // pass it verbatim, do not overlay process.env (see envIsComplete).
418
+ env: plan.ptyOptions.env,
419
+ envIsComplete: true,
420
+ cols,
421
+ rows,
401
422
  transportFactory: this.opts.transportFactory,
402
423
  };
403
424
  }
@@ -16,6 +16,7 @@ import * as os from 'os';
16
16
  import { platform } from 'os';
17
17
  import type { ProviderLoader } from './provider-loader.js';
18
18
  import type { ProviderModule } from './contracts.js';
19
+ import { isKnownWin32GuiExe, readWin32IdeVersionFromDisk } from '../detection/win32-ide-version.js';
19
20
 
20
21
  // ─── Types ──────────────────────────────────────
21
22
 
@@ -226,6 +227,10 @@ export async function detectAllVersions(
226
227
  ): Promise<ProviderVersionInfo[]> {
227
228
  const results: ProviderVersionInfo[] = [];
228
229
  const currentOs = platform() as string;
230
+ // Map of provider type → GUI exe names (win32), used to refuse spawning a
231
+ // GUI executable for version detection (which would boot the IDE window).
232
+ const win32ProcessNames: Record<string, string[]> =
233
+ typeof loader.getWinProcessNames === 'function' ? loader.getWinProcessNames() : {};
229
234
 
230
235
  for (const provider of loader.getAll()) {
231
236
  const info: ProviderVersionInfo = {
@@ -258,12 +263,29 @@ export async function detectAllVersions(
258
263
  info.path = appPath || null;
259
264
  info.binary = resolvedBin || null;
260
265
 
261
- // Version: try CLI first, then plist
262
- if (resolvedBin) {
263
- info.version = await getVersion(resolvedBin, versionCommand);
264
- }
265
- if (!info.version && appPath) {
266
- info.version = await getMacAppVersion(appPath);
266
+ // Version detection for IDEs must avoid spawning the GUI executable.
267
+ // On Windows the resolved binary is frequently the GUI Electron exe
268
+ // (case-insensitive FS), and `<exe> --version` boots the IDE window.
269
+ // Strategy (all spawn-free where possible):
270
+ // 1. win32: read bundled product.json/package.json next to the exe.
271
+ // 2. darwin: read Info.plist via PlistBuddy (read-only).
272
+ // 3. Fallback to `<bin> --version` ONLY when the binary is not a
273
+ // known GUI exe — the safety-net guard (#4).
274
+ if (currentOs === 'win32') {
275
+ info.version = readWin32IdeVersionFromDisk(resolvedBin || appPath || '');
276
+ if (!info.version && resolvedBin && !isKnownWin32GuiExe(resolvedBin, win32ProcessNames)) {
277
+ info.version = await getVersion(resolvedBin, versionCommand);
278
+ }
279
+ } else if (currentOs === 'darwin') {
280
+ if (appPath) info.version = await getMacAppVersion(appPath);
281
+ if (!info.version && resolvedBin) {
282
+ info.version = await getVersion(resolvedBin, versionCommand);
283
+ }
284
+ } else {
285
+ // linux and others: bundled CLI wrappers are real CLIs, safe to exec.
286
+ if (resolvedBin) {
287
+ info.version = await getVersion(resolvedBin, versionCommand);
288
+ }
267
289
  }
268
290
 
269
291
  } else if (provider.category === 'cli' || provider.category === 'acp') {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * working-dir — shared helpers for deriving display names from a session's
3
+ * working directory. Used by CLI/ACP provider instances to build session/tab
4
+ * titles.
5
+ */
6
+
7
+ /**
8
+ * OS-aware basename of a working directory path.
9
+ *
10
+ * Splits on BOTH POSIX (`/`) and Windows (`\`) separators so a win32 path like
11
+ * `D:\gh\adhdev-cloud` resolves to `adhdev-cloud` even when the daemon's own
12
+ * `path.basename` is POSIX-only — and a path that mixes separators still works.
13
+ * Trailing separators and root-only paths fall back to `'session'`, matching
14
+ * the historical `.split('/').filter(Boolean).pop() || 'session'` behavior.
15
+ *
16
+ * Mirrors the web dashboard's `getWorkspaceName` (`ws.split(/[/\\]/)`).
17
+ */
18
+ export function workingDirBasename(p: string): string {
19
+ return (p || '')
20
+ .split(/[\\/]/)
21
+ .filter(Boolean)
22
+ .pop() || 'session';
23
+ }