@adhdev/daemon-core 0.6.75 → 0.6.77

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.6.75",
3
+ "version": "0.6.77",
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",
@@ -146,6 +146,25 @@ function stripAnsi(str: string): string {
146
146
  .replace(/ +/g, ' ');
147
147
  }
148
148
 
149
+ function stripTerminalNoise(str: string): string {
150
+ return String(str || '')
151
+ // Remove remaining C0/C1 control chars except newlines/tabs.
152
+ // eslint-disable-next-line no-control-regex
153
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
154
+ // Drop common terminal negotiation/report fragments that can remain after ANSI stripping.
155
+ .replace(/(^|[\s([])(?:\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
156
+ .replace(/(^|[\s([])(?:\[\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
157
+ .replace(/(^|[\s([])(?:\d{1,4};\?)(?=$|[\s)\]])/g, '$1')
158
+ .replace(/\r+/g, '\n')
159
+ .replace(/[ \t]+\n/g, '\n')
160
+ .replace(/\n{3,}/g, '\n\n')
161
+ .replace(/ {2,}/g, ' ');
162
+ }
163
+
164
+ function sanitizeTerminalText(str: string): string {
165
+ return stripTerminalNoise(stripAnsi(str));
166
+ }
167
+
149
168
  function findBinary(name: string): string {
150
169
  const isWin = os.platform() === 'win32';
151
170
  try {
@@ -370,6 +389,9 @@ export class ProviderCliAdapter implements CliAdapter {
370
389
  // Approval state machine
371
390
  private approvalTransitionBuffer: string = '';
372
391
  private approvalExitTimeout: NodeJS.Timeout | null = null;
392
+ private pendingScriptStatus: 'generating' | 'waiting_approval' | null = null;
393
+ private pendingScriptStatusSince = 0;
394
+ private pendingScriptStatusTimer: NodeJS.Timeout | null = null;
373
395
 
374
396
  // Output settle debounce — fires after PTY output goes quiet
375
397
  private settleTimer: NodeJS.Timeout | null = null;
@@ -454,6 +476,7 @@ export class ProviderCliAdapter implements CliAdapter {
454
476
  private readonly sendDelayMs: number;
455
477
  private readonly sendKey: string;
456
478
  private readonly submitStrategy: 'wait_for_echo' | 'immediate';
479
+ private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 1000;
457
480
 
458
481
  constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
459
482
  this.provider = provider;
@@ -645,7 +668,7 @@ export class ProviderCliAdapter implements CliAdapter {
645
668
  private handleOutput(rawData: string): void {
646
669
  this.terminalScreen.write(rawData);
647
670
  this.terminalHistory = mergeTerminalHistory(this.terminalHistory, this.terminalScreen.getText());
648
- const cleanData = stripAnsi(rawData);
671
+ const cleanData = sanitizeTerminalText(rawData);
649
672
 
650
673
  if (this.isWaitingForResponse && cleanData) {
651
674
  this.responseBuffer = (this.responseBuffer + cleanData).slice(-8000);
@@ -764,26 +787,45 @@ export class ProviderCliAdapter implements CliAdapter {
764
787
 
765
788
  const prevStatus = this.currentStatus;
766
789
 
767
- if (scriptStatus === 'waiting_approval') {
768
- // Auto-accept startup safety dialogs (e.g., "Claude Code'll be able to read, edit, and execute")
769
- const modalMessage = modal?.message || '';
770
- const screenText = this.terminalScreen.getText() || this.accumulatedBuffer;
771
- const autoAcceptPatterns = [
772
- /be able to read, edit, and execute/i,
773
- /Security guide/i,
774
- /Enter to confirm/i,
775
- /Quick safety check/i,
776
- /Do you trust the files/i,
777
- /Is this a project/i,
778
- ];
779
- if (autoAcceptPatterns.some(p => p.test(modalMessage) || p.test(screenText))) {
780
- LOG.info('CLI', `[${this.cliType}] Auto-accepting startup dialog: ${modalMessage.slice(0, 80)}`);
781
- setTimeout(() => this.ptyProcess?.write('\r'), 200);
782
- this.lastApprovalResolvedAt = Date.now();
783
- this.activeModal = null;
790
+ const clearPendingScriptStatus = () => {
791
+ this.pendingScriptStatus = null;
792
+ this.pendingScriptStatusSince = 0;
793
+ if (this.pendingScriptStatusTimer) {
794
+ clearTimeout(this.pendingScriptStatusTimer);
795
+ this.pendingScriptStatusTimer = null;
796
+ }
797
+ };
798
+ const armPendingScriptStatus = (delayMs: number) => {
799
+ if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
800
+ this.pendingScriptStatusTimer = setTimeout(() => {
801
+ this.pendingScriptStatusTimer = null;
802
+ this.settledBuffer = this.recentOutputBuffer;
803
+ this.evaluateSettled();
804
+ }, delayMs);
805
+ };
806
+ const shouldDebouncePromotion = (status: string) =>
807
+ prevStatus === 'idle'
808
+ && !this.isWaitingForResponse
809
+ && !this.currentTurnScope
810
+ && (status === 'generating' || status === 'waiting_approval');
811
+
812
+ if (shouldDebouncePromotion(scriptStatus)) {
813
+ if (this.pendingScriptStatus !== scriptStatus) {
814
+ this.pendingScriptStatus = scriptStatus as 'generating' | 'waiting_approval';
815
+ this.pendingScriptStatusSince = now;
816
+ armPendingScriptStatus(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
817
+ return;
818
+ }
819
+ const elapsed = now - this.pendingScriptStatusSince;
820
+ if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
821
+ armPendingScriptStatus(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
784
822
  return;
785
823
  }
824
+ } else {
825
+ clearPendingScriptStatus();
826
+ }
786
827
 
828
+ if (scriptStatus === 'waiting_approval') {
787
829
  const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
788
830
  if (!inCooldown) {
789
831
  this.isWaitingForResponse = true;
@@ -800,6 +842,16 @@ export class ProviderCliAdapter implements CliAdapter {
800
842
  }
801
843
 
802
844
  if (scriptStatus === 'generating') {
845
+ const screenText = this.terminalScreen.getText() || this.accumulatedBuffer;
846
+ const noActiveTurn = !this.currentTurnScope;
847
+ const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(screenText)
848
+ || (/accept edits on/i.test(screenText)
849
+ && (/Update available!/i.test(screenText)
850
+ || /\/effort/i.test(screenText)
851
+ || /^.*➜\s+\S+/m.test(screenText)));
852
+ if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome) {
853
+ return;
854
+ }
803
855
  if (prevStatus === 'waiting_approval') {
804
856
  // Transitioned out of approval → generating
805
857
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
@@ -1242,7 +1294,7 @@ export class ProviderCliAdapter implements CliAdapter {
1242
1294
  committedMessages: this.committedMessages.slice(-20),
1243
1295
  structuredMessages: this.structuredMessages.slice(-20),
1244
1296
  messageCount: this.committedMessages.length,
1245
- screenText: this.terminalScreen.getText().slice(-4000),
1297
+ screenText: sanitizeTerminalText(this.terminalScreen.getText()).slice(-4000),
1246
1298
  terminalHistory: this.terminalHistory.slice(-8000),
1247
1299
  currentTurnScope: this.currentTurnScope,
1248
1300
  startupBuffer: this.startupBuffer.slice(-4000),
@@ -1251,6 +1303,7 @@ export class ProviderCliAdapter implements CliAdapter {
1251
1303
  accumulatedBufferLength: this.accumulatedBuffer.length,
1252
1304
  accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
1253
1305
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
1306
+ sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
1254
1307
  responseBuffer: this.responseBuffer.slice(-1000),
1255
1308
  isWaitingForResponse: this.isWaitingForResponse,
1256
1309
  activeModal: this.activeModal,
@@ -168,8 +168,24 @@ export class DaemonCommandHandler implements CommandHelpers {
168
168
  getCliAdapter(type?: string): any | null {
169
169
  const target = type || this._currentIdeType;
170
170
  if (!target || !this._ctx.adapters) return null;
171
+ // Normalize composite transport IDs:
172
+ // standalone_xxx:cli:<uuid> -> <uuid>
173
+ // daemon:acp:<uuid> -> <uuid>
174
+ let normalizedTarget = target;
175
+ const colonIdx = normalizedTarget.lastIndexOf(':');
176
+ if (colonIdx >= 0) normalizedTarget = normalizedTarget.substring(colonIdx + 1);
177
+
178
+ const direct = this._ctx.adapters.get(normalizedTarget);
179
+ if (direct) return direct;
180
+
171
181
  for (const [key, adapter] of this._ctx.adapters.entries()) {
172
- if ((adapter as any).cliType === target || key.startsWith(target)) {
182
+ if (
183
+ (adapter as any).cliType === target
184
+ || (adapter as any).cliType === normalizedTarget
185
+ || key === normalizedTarget
186
+ || key.startsWith(target)
187
+ || key.startsWith(normalizedTarget)
188
+ ) {
173
189
  return adapter;
174
190
  }
175
191
  }
@@ -1895,22 +1895,25 @@ export class DevServer {
1895
1895
  return path.join(scriptsDir, versions[0]);
1896
1896
  }
1897
1897
 
1898
- private resolveAutoImplWritableProviderDir(category: ProviderCategory, type: string, requestedDir?: string): string | null {
1898
+ private resolveAutoImplWritableProviderDir(
1899
+ category: ProviderCategory,
1900
+ type: string,
1901
+ requestedDir?: string,
1902
+ ): { dir: string | null; reason?: string } {
1899
1903
  const canonicalUserDir = path.resolve(this.providerLoader.getUserProviderDir(category, type));
1900
1904
  const desiredDir = requestedDir ? path.resolve(requestedDir) : canonicalUserDir;
1901
-
1902
- if (desiredDir !== canonicalUserDir) {
1903
- return null;
1905
+ const upstreamRoot = path.resolve(this.providerLoader.getUpstreamDir());
1906
+ if (desiredDir === upstreamRoot || desiredDir.startsWith(`${upstreamRoot}${path.sep}`)) {
1907
+ return { dir: null, reason: `Refusing to write into upstream provider directory: ${desiredDir}` };
1904
1908
  }
1905
1909
 
1906
- const userRoot = path.resolve(this.providerLoader.getUserDir());
1907
- if (desiredDir !== userRoot && !desiredDir.startsWith(`${userRoot}${path.sep}`)) {
1908
- return null;
1910
+ if (path.basename(desiredDir) !== type) {
1911
+ return { dir: null, reason: `Requested writable provider directory must end with '${type}': ${desiredDir}` };
1909
1912
  }
1910
1913
 
1911
1914
  const sourceDir = this.findProviderDir(type);
1912
1915
  if (!sourceDir) {
1913
- return null;
1916
+ return { dir: null, reason: `Provider source directory not found for '${type}'` };
1914
1917
  }
1915
1918
 
1916
1919
  if (!fs.existsSync(desiredDir)) {
@@ -1921,7 +1924,7 @@ export class DevServer {
1921
1924
 
1922
1925
  const providerJson = path.join(desiredDir, 'provider.json');
1923
1926
  if (!fs.existsSync(providerJson)) {
1924
- return null;
1927
+ return { dir: null, reason: `provider.json not found in writable provider directory: ${desiredDir}` };
1925
1928
  }
1926
1929
 
1927
1930
  try {
@@ -1930,11 +1933,14 @@ export class DevServer {
1930
1933
  providerData.disableUpstream = true;
1931
1934
  fs.writeFileSync(providerJson, JSON.stringify(providerData, null, 2));
1932
1935
  }
1933
- } catch {
1934
- return null;
1936
+ } catch (error) {
1937
+ return {
1938
+ dir: null,
1939
+ reason: `Failed to update provider.json in writable provider directory: ${(error as Error).message}`,
1940
+ };
1935
1941
  }
1936
1942
 
1937
- return desiredDir;
1943
+ return { dir: desiredDir };
1938
1944
  }
1939
1945
 
1940
1946
  private loadAutoImplReferenceScripts(referenceType: string | null): Record<string, string> {
@@ -1975,13 +1981,14 @@ export class DevServer {
1975
1981
  const provider = this.providerLoader.resolve(type);
1976
1982
  if (!provider) { this.json(res, 404, { error: `Provider not found: ${type}` }); return; }
1977
1983
 
1978
- const providerDir = this.resolveAutoImplWritableProviderDir(provider.category, type, requestedProviderDir);
1979
- if (!providerDir) {
1984
+ const writableProvider = this.resolveAutoImplWritableProviderDir(provider.category, type, requestedProviderDir);
1985
+ if (!writableProvider.dir) {
1980
1986
  this.json(res, 409, {
1981
- error: `Auto-implement only writes to the canonical user provider directory for '${type}'.`,
1987
+ error: writableProvider.reason || `Auto-implement only writes to the canonical user provider directory for '${type}'.`,
1982
1988
  });
1983
1989
  return;
1984
1990
  }
1991
+ const providerDir = writableProvider.dir;
1985
1992
 
1986
1993
  try {
1987
1994
  // 1. Collect DOM context
@@ -2529,14 +2536,16 @@ export class DevServer {
2529
2536
  lines.push('| Status | When to use | How to detect |');
2530
2537
  lines.push('|---|---|---|');
2531
2538
  lines.push('| `idle` | AI is NOT generating, no approval needed | Default state. No stop button, no spinners, no approval pills/buttons |');
2532
- lines.push('| `generating` | AI is actively streaming/thinking | ANY of: (1) Stop/Cancel button visible, (2) CSS animation (animate-spin/pulse/bounce), (3) floating state text like Thinking/Generating/Sailing, (4) streaming indicator class |');
2539
+ lines.push('| `generating` | AI is actively streaming/thinking | ANY of: (1) Submit button icon SVG changes (e.g. arrow→stop square, fill="none"→fill="currentColor"), (2) Stop/Cancel button visible, (3) CSS animation, (4) Structural markers (aria-labels that only appear during generation) |');
2533
2540
  lines.push('| `waiting_approval` | AI stopped and needs user action | Actionable buttons like Run/Skip/Accept/Reject are visible AND clickable |');
2534
2541
  lines.push('');
2535
2542
  lines.push('### ⚠️ Status Detection Gotchas (MUST READ!)');
2536
- lines.push('1. **FALSE POSITIVES from old messages**: Chat history may contain text like "Command Awaiting Approval" from PAST turns. If you search the entire chat panel for this text, you will get false matches from parent divs whose innerText includes ALL child text. ONLY match small leaf elements (under 80 chars) or use explicit button/pill selectors.');
2537
- lines.push('2. **Awaiting Approval pill without actions**: Some IDEs show a floating pill/banner saying "Awaiting Approval" that is just a scroll-to indicator (not an actual approval dialog). If this pill exists but NO actionable buttons (Run/Skip/Accept/Reject) exist anywhere in the panel, the status should be `idle`, NOT `waiting_approval`.');
2538
- lines.push('3. **generating detection must be multi-signal**: Do NOT rely on just one indicator. Check ALL of: stop buttons, CSS animations, floating state labels, streaming classes. IDEs differ widely.');
2539
- lines.push('4. **activeModal must include actions**: When `status` is `waiting_approval`, the `activeModal` object MUST include a non-empty `actions` array listing the button labels. If you cannot find any action buttons, the status is NOT `waiting_approval`.');
2543
+ lines.push('1. **DO NOT rely on button text/labels in the user\'s language.** OS locale may be Korean, Japanese, etc. Button text like "Cancel" or "Stop" will be localized. Instead, detect STRUCTURAL indicators: SVG icon changes, CSS classes, aria-labels from the extension\'s own React/Radix UI (which stay in English regardless of OS locale).');
2544
+ lines.push('2. **Use sendMessage to CREATE a generating state, then CAPTURE the DOM.** Send a LONG prompt (e.g. "Write an extremely detailed 5000-word essay...") so the AI takes 10+ seconds. Then periodically capture the DOM during generation to find which elements appear/change. Compare idle vs generating DOM snapshots to find reliable structural markers.');
2545
+ lines.push('3. **Look for SVG icon changes in the submit button.** Many IDEs change the submit button icon from an arrow (send) to a square (stop) during generation. Check the SVG `fill` attribute or path data.');
2546
+ lines.push('4. **FALSE POSITIVES from old messages**: Chat history may contain text like "Command Awaiting Approval" from PAST turns. ONLY match small leaf elements (under 80 chars) or use explicit button/pill selectors.');
2547
+ lines.push('5. **Awaiting Approval pill without actions**: Some IDEs show a floating pill/banner that is just a scroll-to indicator. If NO actionable buttons exist, the status should be `idle`, NOT `waiting_approval`.');
2548
+ lines.push('6. **activeModal must include actions**: When `status` is `waiting_approval`, the `activeModal` object MUST include a non-empty `actions` array.');
2540
2549
  lines.push('');
2541
2550
 
2542
2551
  lines.push('## Action');
@@ -2599,10 +2608,10 @@ export class DevServer {
2599
2608
  lines.push(`echo "$RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); r=d.get('result',d); r=json.loads(r) if isinstance(r,str) else r; assert r.get('status')=='idle', f'Expected idle, got {r.get(chr(34)+chr(115)+chr(116)+chr(97)+chr(116)+chr(117)+chr(115)+chr(34))}'; print('Step 1 PASS: status=idle')"`);
2600
2609
  lines.push('```');
2601
2610
  lines.push('');
2602
- lines.push('### Step 2: Send a message that triggers generation');
2611
+ lines.push('### Step 2: Send a LONG message that triggers extended generation (10+ seconds)');
2603
2612
  lines.push('```bash');
2604
- lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "sendMessage", "type": "${type}", "ideType": "${type}", "args": {"message": "Say hello in one word"}}'`);
2605
- lines.push('sleep 2');
2613
+ lines.push(`curl -sS -X POST http://127.0.0.1:${DEV_SERVER_PORT}/api/scripts/run -H "Content-Type: application/json" -d '{"script": "sendMessage", "type": "${type}", "ideType": "${type}", "args": {"message": "Write an extremely detailed 5000-word essay about the history of artificial intelligence from Alan Turing to 2025. Be very thorough and verbose."}}'`);
2614
+ lines.push('sleep 3');
2606
2615
  lines.push('```');
2607
2616
  lines.push('');
2608
2617
  lines.push('### Step 3: Check generating OR completed');
@@ -2749,6 +2758,9 @@ export class DevServer {
2749
2758
  lines.push('8. Keep exports compatible with the existing `scripts.js` router (`module.exports = function ...`).');
2750
2759
  lines.push('9. Do not rewrite unrelated provider config. Only touch the scripts needed for this task unless a tiny supporting change is required.');
2751
2760
  lines.push('10. When the verification API returns `instanceId`, keep using that exact instance for follow-up `send`, `resolve`, `raw`, and `stop` calls. Do not assume type-only routing is safe if multiple sessions exist.');
2761
+ lines.push('11. Do NOT repeatedly dump the same target files. Read the target scripts once, reproduce the bug, then move directly to patching.');
2762
+ lines.push('12. If the user instructions include concrete screen text, raw PTY snippets, or a specific repro, treat that as the primary acceptance criteria.');
2763
+ lines.push('13. After the first successful live repro, stop broad diagnosis. Edit the scripts, reload, and verify. Do not burn tokens on repeated re-inspection without code changes.');
2752
2764
  lines.push('');
2753
2765
 
2754
2766
  lines.push('## Task');
@@ -2792,6 +2804,9 @@ export class DevServer {
2792
2804
  lines.push('');
2793
2805
  lines.push('Use `resolve` when the parsed modal buttons are correct. Use `raw` when the CLI expects a literal keystroke like `1`, `y`, or Enter. Repeat until idle.');
2794
2806
  lines.push('');
2807
+ lines.push('### Patch Discipline');
2808
+ lines.push('Once the repro is confirmed, immediately edit the target files. Avoid loops where you keep re-reading long files or re-running the same debug commands without changing code.');
2809
+ lines.push('');
2795
2810
  lines.push('### 5. Verify the side effects outside the CLI');
2796
2811
  lines.push('```bash');
2797
2812
  lines.push('test -f tmp/adhdev_provider_fix_test.py');
@@ -16,12 +16,18 @@ export interface CLIInfo {
16
16
  displayName: string;
17
17
  icon: string;
18
18
  command: string;
19
+ versionCommand?: string;
19
20
  installed: boolean;
20
21
  version?: string;
21
22
  path?: string;
22
23
  category?: string;
23
24
  }
24
25
 
26
+ function parseVersion(raw: string): string {
27
+ const match = raw.match(/v?(\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?)/);
28
+ return match ? match[1] : raw.split('\n')[0].slice(0, 100);
29
+ }
30
+
25
31
  /** Run a shell command with timeout, returning stdout or null on failure */
26
32
  function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
27
33
  return new Promise((resolve) => {
@@ -62,11 +68,18 @@ export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIIn
62
68
  // Get version (parallel with other checks)
63
69
  let version: string | undefined;
64
70
  try {
65
- const versionResult = await execAsync(`${cli.command} --version 2>/dev/null`, 3000);
66
- if (versionResult) {
67
- // Extract version number (e.g. "gemini v1.2.3" → "1.2.3")
68
- const match = versionResult.match(/(\d+\.\d+[\.\d]*)/);
69
- version = match ? match[1] : versionResult.split('\n')[0].slice(0, 30);
71
+ const versionCommands = [
72
+ cli.versionCommand,
73
+ `${cli.command} --version 2>/dev/null`,
74
+ `${cli.command} -V 2>/dev/null`,
75
+ `${cli.command} -v 2>/dev/null`,
76
+ ].filter((v): v is string => !!v);
77
+ for (const versionCommand of versionCommands) {
78
+ const versionResult = await execAsync(versionCommand, 3000);
79
+ if (versionResult) {
80
+ version = parseVersion(versionResult);
81
+ break;
82
+ }
70
83
  }
71
84
  } catch { }
72
85
 
@@ -85,25 +85,6 @@ export class CliProviderInstance implements ProviderInstance {
85
85
 
86
86
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
87
87
 
88
- // Preserve rich message objects, just truncate extremely long text strings
89
- const recentMessages = adapterStatus.messages.slice(-50).map((m: any) => {
90
- const content = typeof m.content === 'string' && m.content.length > 8000
91
- ? m.content.slice(0, 8000) + '\n... (truncated)'
92
- : m.content;
93
- return { ...m, content };
94
- });
95
-
96
- // generating during partial response add
97
- // Save history
98
- if (recentMessages.length > 0) {
99
- const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
100
- this.historyWriter.appendNewMessages(
101
- this.type,
102
- recentMessages,
103
- `${this.provider.name} · ${dirName}`,
104
- this.instanceId,
105
- );
106
- }
107
88
  if (adapterStatus.terminalHistory?.trim()) {
108
89
  this.historyWriter.appendTerminalHistory(
109
90
  this.type,
@@ -118,12 +99,12 @@ export class CliProviderInstance implements ProviderInstance {
118
99
  name: this.provider.name,
119
100
  category: 'cli',
120
101
  status: adapterStatus.status,
121
- mode: (this.settings.mode as 'terminal' | 'chat') || 'terminal',
102
+ mode: 'terminal',
122
103
  activeChat: {
123
104
  id: `${this.type}_${this.workingDir}`,
124
105
  title: `${this.provider.name} · ${dirName}`,
125
106
  status: adapterStatus.status,
126
- messages: recentMessages,
107
+ messages: [],
127
108
  activeModal: adapterStatus.activeModal,
128
109
  terminalHistory: adapterStatus.terminalHistory,
129
110
  inputContent: '',
@@ -138,11 +119,15 @@ export class CliProviderInstance implements ProviderInstance {
138
119
 
139
120
  onEvent(event: string, data?: any): void {
140
121
  if (event === 'send_message' && data?.text) {
141
- this.adapter.sendMessage(data.text);
122
+ void this.adapter.sendMessage(data.text).catch((e: any) => {
123
+ LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
124
+ });
142
125
  } else if (event === 'server_connected' && data?.serverConn) {
143
126
  this.adapter.setServerConn(data.serverConn);
144
127
  } else if (event === 'resolve_action' && data) {
145
- this.adapter.resolveAction(data);
128
+ void this.adapter.resolveAction(data).catch((e: any) => {
129
+ LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
130
+ });
146
131
  }
147
132
  }
148
133
 
@@ -237,16 +237,23 @@ export class ProviderLoader {
237
237
  * Build CLI/ACP detection list (replaces cli-detector)
238
238
  * Dynamically generated from provider.js spawn.command.
239
239
  */
240
- getCliDetectionList(): { id: string; displayName: string; icon: string; command: string; category: string }[] {
241
- const result: { id: string; displayName: string; icon: string; command: string; category: string }[] = [];
240
+ getCliDetectionList(): { id: string; displayName: string; icon: string; command: string; category: string; versionCommand?: string }[] {
241
+ const result: { id: string; displayName: string; icon: string; command: string; category: string; versionCommand?: string }[] = [];
242
242
  for (const p of this.providers.values()) {
243
243
  if ((p.category === 'cli' || p.category === 'acp') && p.spawn?.command) {
244
+ const verCmdConfig = (p as any).versionCommand;
245
+ const versionCommand = typeof verCmdConfig === 'object' && verCmdConfig !== null
246
+ ? verCmdConfig[process.platform]
247
+ : verCmdConfig;
244
248
  result.push({
245
249
  id: p.type,
246
250
  displayName: p.displayName || p.name,
247
251
  icon: p.icon || '🔧',
248
252
  command: p.spawn.command,
249
253
  category: p.category,
254
+ ...(typeof versionCommand === 'string' && versionCommand.trim()
255
+ ? { versionCommand: versionCommand.trim() }
256
+ : {}),
250
257
  });
251
258
  }
252
259
  }
@@ -75,7 +75,7 @@ export interface ManagedCliEntry {
75
75
  cliType: string;
76
76
  cliName: string;
77
77
  status: string;
78
- mode: 'terminal' | 'chat';
78
+ mode: 'terminal';
79
79
  workspace: string;
80
80
  activeChat: _ActiveChatData | null;
81
81
  }
@@ -156,7 +156,7 @@ export function buildManagedClis(
156
156
  cliType: s.type,
157
157
  cliName: s.name,
158
158
  status: s.status,
159
- mode: s.mode as 'terminal' | 'chat',
159
+ mode: 'terminal' as const,
160
160
  workspace: s.workspace || '',
161
161
  activeChat: s.activeChat,
162
162
  }));