@adhdev/daemon-core 0.8.22 → 0.8.24

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.
Files changed (35) hide show
  1. package/dist/agent-stream/types.d.ts +3 -0
  2. package/dist/cli-adapter-types.d.ts +3 -0
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +71 -11
  4. package/dist/commands/stream-commands.d.ts +1 -0
  5. package/dist/config/config.d.ts +6 -0
  6. package/dist/index.js +1162 -307
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1162 -307
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/providers/cli-provider-instance.d.ts +6 -0
  11. package/dist/providers/contracts.d.ts +59 -1
  12. package/dist/providers/control-effects.d.ts +4 -0
  13. package/dist/providers/extension-provider-instance.d.ts +9 -0
  14. package/dist/providers/ide-provider-instance.d.ts +8 -0
  15. package/dist/shared-types.d.ts +2 -1
  16. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  17. package/package.json +3 -2
  18. package/src/agent-stream/forward.ts +2 -0
  19. package/src/agent-stream/provider-adapter.ts +5 -15
  20. package/src/agent-stream/types.ts +4 -0
  21. package/src/cli-adapter-types.ts +3 -0
  22. package/src/cli-adapters/provider-cli-adapter.ts +399 -49
  23. package/src/commands/handler.ts +1 -0
  24. package/src/commands/stream-commands.ts +99 -8
  25. package/src/config/config.d.ts +1 -0
  26. package/src/config/config.ts +9 -0
  27. package/src/launch.ts +57 -11
  28. package/src/providers/cli-provider-instance.ts +148 -2
  29. package/src/providers/contracts.ts +65 -2
  30. package/src/providers/control-effects.ts +114 -0
  31. package/src/providers/extension-provider-instance.ts +163 -3
  32. package/src/providers/ide-provider-instance.ts +181 -2
  33. package/src/shared-types.d.ts +1 -0
  34. package/src/shared-types.ts +2 -1
  35. package/src/status/snapshot.ts +1 -0
package/dist/index.mjs CHANGED
@@ -79,7 +79,8 @@ function normalizeConfig(raw) {
79
79
  providerSettings: isPlainObject(parsed.providerSettings) ? parsed.providerSettings : {},
80
80
  ideSettings: isPlainObject(parsed.ideSettings) ? parsed.ideSettings : {},
81
81
  disableUpstream: asBoolean(parsed.disableUpstream, DEFAULT_CONFIG.disableUpstream ?? false),
82
- providerDir: asOptionalString(parsed.providerDir)
82
+ providerDir: asOptionalString(parsed.providerDir),
83
+ terminalSizingMode: parsed.terminalSizingMode === "fit" ? "fit" : "measured"
83
84
  };
84
85
  }
85
86
  function generateMachineId() {
@@ -223,7 +224,8 @@ var init_config = __esm({
223
224
  registeredMachineId: void 0,
224
225
  providerSettings: {},
225
226
  ideSettings: {},
226
- disableUpstream: false
227
+ disableUpstream: false,
228
+ terminalSizingMode: "measured"
227
229
  };
228
230
  MACHINE_ID_PREFIX = "mach_";
229
231
  }
@@ -811,6 +813,53 @@ function stripTerminalNoise(str) {
811
813
  function sanitizeTerminalText(str) {
812
814
  return stripTerminalNoise(stripAnsi(str));
813
815
  }
816
+ function splitCliScreenLines(text) {
817
+ return String(text || "").replace(/\u0007/g, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n").map((line) => line.replace(/\s+$/, ""));
818
+ }
819
+ function isPromptLikeCliLine(line) {
820
+ const trimmed = String(line || "").trim();
821
+ if (!trimmed) return false;
822
+ return /^[❯›>]\s*(?:$|\S.*)$/.test(trimmed);
823
+ }
824
+ function buildCliScreenSnapshot(text) {
825
+ const normalizedText = String(text || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
826
+ const rawLines = splitCliScreenLines(normalizedText);
827
+ const lines = rawLines.map((line, index, arr) => {
828
+ const trimmed = String(line || "").trim();
829
+ return {
830
+ index,
831
+ fromTop: index,
832
+ fromBottom: arr.length - index - 1,
833
+ text: line,
834
+ trimmed,
835
+ isEmpty: trimmed.length === 0
836
+ };
837
+ });
838
+ const nonEmptyLines = lines.filter((line) => !line.isEmpty);
839
+ const firstNonEmptyLine = nonEmptyLines[0] ?? null;
840
+ const lastNonEmptyLine = nonEmptyLines[nonEmptyLines.length - 1] ?? null;
841
+ let promptLineIndex = -1;
842
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
843
+ if (isPromptLikeCliLine(lines[i].text)) {
844
+ promptLineIndex = i;
845
+ break;
846
+ }
847
+ }
848
+ return {
849
+ text: normalizedText,
850
+ lineCount: lines.length,
851
+ lines,
852
+ nonEmptyLines,
853
+ firstNonEmptyLineIndex: firstNonEmptyLine?.index ?? -1,
854
+ lastNonEmptyLineIndex: lastNonEmptyLine?.index ?? -1,
855
+ firstNonEmptyLine,
856
+ lastNonEmptyLine,
857
+ promptLineIndex,
858
+ promptLine: promptLineIndex >= 0 ? lines[promptLineIndex] : null,
859
+ linesAbovePrompt: promptLineIndex >= 0 ? lines.slice(0, promptLineIndex) : [...lines],
860
+ linesBelowPrompt: promptLineIndex >= 0 ? lines.slice(promptLineIndex + 1) : []
861
+ };
862
+ }
814
863
  function computeTerminalQueryTail(buffer) {
815
864
  const prefixes = ["\x1B[6n", "\x1B[?6n"];
816
865
  const maxLength = prefixes.reduce((n, value) => Math.max(n, value.length), 0) - 1;
@@ -1049,7 +1098,9 @@ var init_provider_cli_adapter = __esm({
1049
1098
  ready = false;
1050
1099
  startupBuffer = "";
1051
1100
  startupParseGate = false;
1101
+ startupSettleTimer = null;
1052
1102
  spawnAt = 0;
1103
+ startupFirstOutputAt = 0;
1053
1104
  // PTY I/O
1054
1105
  onPtyDataCallback = null;
1055
1106
  pendingOutputParseBuffer = "";
@@ -1090,6 +1141,7 @@ var init_provider_cli_adapter = __esm({
1090
1141
  statusHistory = [];
1091
1142
  // ─── CLI Scripts (script-based parsing) ───
1092
1143
  cliScripts;
1144
+ runtimeSettings = {};
1093
1145
  /** Full accumulated ANSI-stripped PTY output */
1094
1146
  accumulatedBuffer = "";
1095
1147
  /** Full accumulated raw PTY output (with ANSI) */
@@ -1104,14 +1156,15 @@ var init_provider_cli_adapter = __esm({
1104
1156
  traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1105
1157
  static MAX_TRACE_ENTRIES = 250;
1106
1158
  providerResolutionMeta;
1107
- static IDLE_FINISH_CONFIRM_MS = 900;
1159
+ static IDLE_FINISH_CONFIRM_MS = 2e3;
1160
+ static STATUS_ACTIVITY_HOLD_MS = 2e3;
1108
1161
  static FINISH_RETRY_DELAY_MS = 300;
1109
1162
  static MAX_FINISH_RETRIES = 2;
1110
1163
  syncMessageViews() {
1111
1164
  this.messages = [...this.committedMessages];
1112
1165
  this.structuredMessages = [...this.committedMessages];
1113
1166
  }
1114
- normalizeParsedMessages(parsedMessages) {
1167
+ hydrateParsedMessages(parsedMessages, scope) {
1115
1168
  const referenceMessages = [...this.committedMessages];
1116
1169
  const usedReferenceIndexes = /* @__PURE__ */ new Set();
1117
1170
  const now = Date.now();
@@ -1144,13 +1197,30 @@ var init_provider_cli_adapter = __esm({
1144
1197
  const content = typeof message.content === "string" ? message.content : String(message.content || "");
1145
1198
  const parsedTimestamp = typeof message.timestamp === "number" && Number.isFinite(message.timestamp) ? message.timestamp : void 0;
1146
1199
  const referenceTimestamp = parsedTimestamp ?? findReferenceTimestamp(role, content, index);
1200
+ const fallbackTimestamp = role === "user" ? scope?.startedAt || now : this.lastOutputAt || scope?.startedAt || now;
1201
+ const timestamp = referenceTimestamp ?? fallbackTimestamp;
1147
1202
  return {
1203
+ ...message,
1148
1204
  role,
1149
1205
  content,
1150
- timestamp: referenceTimestamp ?? now
1206
+ timestamp,
1207
+ receivedAt: typeof message.receivedAt === "number" && Number.isFinite(message.receivedAt) ? message.receivedAt : timestamp
1151
1208
  };
1152
1209
  });
1153
1210
  }
1211
+ normalizeParsedMessages(parsedMessages, scope) {
1212
+ return this.hydrateParsedMessages(parsedMessages, scope).map((message) => ({
1213
+ role: message.role,
1214
+ content: message.content,
1215
+ timestamp: message.timestamp,
1216
+ receivedAt: message.receivedAt,
1217
+ kind: message.kind,
1218
+ id: message.id,
1219
+ index: message.index,
1220
+ meta: message.meta,
1221
+ senderName: message.senderName
1222
+ }));
1223
+ }
1154
1224
  sliceFromOffset(text, start) {
1155
1225
  if (!text) return "";
1156
1226
  if (!Number.isFinite(start) || start <= 0) return text;
@@ -1160,14 +1230,20 @@ var init_provider_cli_adapter = __esm({
1160
1230
  buildParseInput(baseMessages, partialResponse, scope) {
1161
1231
  const buffer = scope ? this.sliceFromOffset(this.accumulatedBuffer, scope.bufferStart) || this.accumulatedBuffer : this.accumulatedBuffer;
1162
1232
  const rawBuffer = scope ? this.sliceFromOffset(this.accumulatedRawBuffer, scope.rawBufferStart) || this.accumulatedRawBuffer : this.accumulatedRawBuffer;
1233
+ const screenText = this.terminalScreen.getText();
1234
+ const recentBuffer = buffer.slice(-1e3) || this.recentOutputBuffer;
1163
1235
  return {
1164
1236
  buffer,
1165
1237
  rawBuffer,
1166
- recentBuffer: buffer.slice(-1e3) || this.recentOutputBuffer,
1167
- screenText: this.terminalScreen.getText(),
1238
+ recentBuffer,
1239
+ screenText,
1240
+ screen: buildCliScreenSnapshot(screenText),
1241
+ bufferScreen: buildCliScreenSnapshot(buffer),
1242
+ recentScreen: buildCliScreenSnapshot(recentBuffer),
1168
1243
  messages: [...baseMessages],
1169
1244
  partialResponse,
1170
- promptText: scope?.prompt || ""
1245
+ promptText: scope?.prompt || "",
1246
+ settings: { ...this.runtimeSettings }
1171
1247
  };
1172
1248
  }
1173
1249
  setStatus(status, trigger) {
@@ -1273,6 +1349,9 @@ var init_provider_cli_adapter = __esm({
1273
1349
  const scriptNames = Object.keys(scripts).filter((k) => typeof scripts[k] === "function");
1274
1350
  LOG.info("CLI", `[${this.cliType}] CLI scripts injected: [${scriptNames.join(", ")}]`);
1275
1351
  }
1352
+ updateRuntimeSettings(settings) {
1353
+ this.runtimeSettings = { ...settings };
1354
+ }
1276
1355
  // ─── Lifecycle ─────────────────────────────────
1277
1356
  setServerConn(serverConn) {
1278
1357
  this.serverConn = serverConn;
@@ -1309,7 +1388,7 @@ var init_provider_cli_adapter = __esm({
1309
1388
  let shellArgs;
1310
1389
  const useShellUnix = !isWin && (!!spawnConfig.shell || !path7.isAbsolute(binaryPath) || isScriptBinary(binaryPath) || !looksLikeMachOOrElf(binaryPath));
1311
1390
  const isCmdShim = isWin && /\.(cmd|bat)$/i.test(binaryPath);
1312
- const useShellWin = isCmdShim || !path7.isAbsolute(binaryPath) || isScriptBinary(binaryPath);
1391
+ const useShellWin = !!spawnConfig.shell || isCmdShim || !path7.isAbsolute(binaryPath) || isScriptBinary(binaryPath);
1313
1392
  const useShell = isWin ? useShellWin : useShellUnix;
1314
1393
  if (useShell) {
1315
1394
  if (!spawnConfig.shell && !isWin) {
@@ -1407,6 +1486,11 @@ var init_provider_cli_adapter = __esm({
1407
1486
  this.spawnAt = Date.now();
1408
1487
  this.startupParseGate = true;
1409
1488
  this.startupBuffer = "";
1489
+ this.startupFirstOutputAt = 0;
1490
+ if (this.startupSettleTimer) {
1491
+ clearTimeout(this.startupSettleTimer);
1492
+ this.startupSettleTimer = null;
1493
+ }
1410
1494
  this.terminalScreen.reset(24, 80);
1411
1495
  this.pendingTerminalQueryTail = "";
1412
1496
  this.currentTurnScope = null;
@@ -1420,7 +1504,8 @@ var init_provider_cli_adapter = __esm({
1420
1504
  this.recordTrace("ready", {
1421
1505
  runtimeMeta: this.getRuntimeMetadata()
1422
1506
  });
1423
- this.setStatus("idle", "pty_ready");
1507
+ this.setStatus("starting", "pty_ready");
1508
+ this.scheduleStartupSettleCheck();
1424
1509
  this.onStatusChange?.();
1425
1510
  }
1426
1511
  // ─── Output Handling ────────────────────────────
@@ -1435,6 +1520,9 @@ var init_provider_cli_adapter = __esm({
1435
1520
  this.lastScreenSnapshot = normalizedScreenSnapshot;
1436
1521
  this.lastScreenChangeAt = now;
1437
1522
  }
1523
+ if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
1524
+ this.startupFirstOutputAt = now;
1525
+ }
1438
1526
  if (this.idleFinishCandidate && (rawData.length > 0 || cleanData.length > 0)) {
1439
1527
  this.clearIdleFinishCandidate("new_output");
1440
1528
  }
@@ -1445,6 +1533,9 @@ var init_provider_cli_adapter = __esm({
1445
1533
  cleanPreview: this.summarizeTraceText(cleanData, 300),
1446
1534
  screenText: this.summarizeTraceText(this.terminalScreen.getText(), 1200)
1447
1535
  });
1536
+ if (this.startupParseGate) {
1537
+ this.scheduleStartupSettleCheck();
1538
+ }
1448
1539
  if (this.isWaitingForResponse && cleanData) {
1449
1540
  this.responseBuffer = (this.responseBuffer + cleanData).slice(-8e3);
1450
1541
  }
@@ -1458,27 +1549,51 @@ var init_provider_cli_adapter = __esm({
1458
1549
  this.recentOutputBuffer = (this.recentOutputBuffer + cleanData).slice(-1e3);
1459
1550
  this.accumulatedBuffer = (this.accumulatedBuffer + cleanData).slice(-_ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
1460
1551
  this.accumulatedRawBuffer = (this.accumulatedRawBuffer + rawData).slice(-_ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
1461
- if (this.startupParseGate) {
1462
- this.startupBuffer += cleanData;
1463
- const elapsed = Date.now() - this.spawnAt;
1464
- const screenText = this.terminalScreen.getText() || "";
1465
- const startupModal = this.getStartupConfirmationModal(screenText);
1466
- const scriptStatus = startupModal ? "waiting_approval" : this.runDetectStatus(this.startupBuffer);
1467
- const hasInteractivePrompt = this.looksLikeVisibleIdlePrompt(screenText);
1468
- const startupStableMs = this.lastScreenChangeAt ? now - this.lastScreenChangeAt : 0;
1469
- const isReady = (scriptStatus === "idle" || scriptStatus === "waiting_approval") && hasInteractivePrompt && startupStableMs >= 700 || !!startupModal && startupStableMs >= 700 || elapsed > 8e3 || this.startupBuffer.length > 12e3;
1470
- if (isReady) {
1471
- this.startupParseGate = false;
1472
- this.ready = true;
1473
- LOG.info(
1474
- "CLI",
1475
- `[${this.cliType}] Startup ready (${elapsed}ms, scriptStatus=${scriptStatus}, prompt=${hasInteractivePrompt}, stableMs=${startupStableMs}) providerDir=${this.providerResolutionMeta.providerDir || "-"} scriptDir=${this.providerResolutionMeta.scriptDir || "-"} scriptsPath=${this.providerResolutionMeta.scriptsPath || "-"}`
1476
- );
1477
- this.onStatusChange?.();
1478
- }
1479
- }
1552
+ this.resolveStartupState("output");
1480
1553
  this.scheduleSettle();
1481
1554
  }
1555
+ resolveStartupState(trigger) {
1556
+ if (!this.startupParseGate) return;
1557
+ const now = Date.now();
1558
+ const screenText = this.terminalScreen.getText() || "";
1559
+ const normalizedScreen = normalizeScreenSnapshot(screenText);
1560
+ const hasStartupOutput = !!this.startupFirstOutputAt || !!normalizedScreen.trim();
1561
+ if (!hasStartupOutput) return;
1562
+ const stableMs = this.lastScreenChangeAt ? now - this.lastScreenChangeAt : 0;
1563
+ if (stableMs < 2e3) return;
1564
+ const startupModal = this.getStartupConfirmationModal(screenText);
1565
+ this.startupParseGate = false;
1566
+ if (this.startupSettleTimer) {
1567
+ clearTimeout(this.startupSettleTimer);
1568
+ this.startupSettleTimer = null;
1569
+ }
1570
+ this.ready = true;
1571
+ if (startupModal) {
1572
+ this.activeModal = startupModal;
1573
+ this.setStatus("waiting_approval", `startup_ready:${trigger}`);
1574
+ } else {
1575
+ this.setStatus("idle", `startup_ready:${trigger}`);
1576
+ }
1577
+ LOG.info(
1578
+ "CLI",
1579
+ `[${this.cliType}] Startup settled (${trigger}, stableMs=${stableMs}, modal=${!!startupModal}) providerDir=${this.providerResolutionMeta.providerDir || "-"} scriptDir=${this.providerResolutionMeta.scriptDir || "-"} scriptsPath=${this.providerResolutionMeta.scriptsPath || "-"}`
1580
+ );
1581
+ this.onStatusChange?.();
1582
+ }
1583
+ scheduleStartupSettleCheck() {
1584
+ if (!this.startupParseGate) return;
1585
+ if (this.startupSettleTimer) clearTimeout(this.startupSettleTimer);
1586
+ const now = Date.now();
1587
+ const stableMs = this.lastScreenChangeAt ? now - this.lastScreenChangeAt : 0;
1588
+ const delayMs = Math.max(250, 2050 - stableMs);
1589
+ this.startupSettleTimer = setTimeout(() => {
1590
+ this.startupSettleTimer = null;
1591
+ this.resolveStartupState("startup_timer");
1592
+ if (this.startupParseGate && Date.now() - this.spawnAt < 1e4) {
1593
+ this.scheduleStartupSettleCheck();
1594
+ }
1595
+ }, delayMs);
1596
+ }
1482
1597
  scheduleSettle() {
1483
1598
  if (this.settleTimer) clearTimeout(this.settleTimer);
1484
1599
  const settleEpoch = this.responseEpoch;
@@ -1520,6 +1635,43 @@ var init_provider_cli_adapter = __esm({
1520
1635
  if (!text.trim()) return false;
1521
1636
  return /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(text) || /⏎\s+send/i.test(text) || /\?\s*for\s*shortcuts/i.test(text) || /Type your message(?:\s+or\s+@path\/to\/file)?/i.test(text) || /workspace\s*\(\/directory\)/i.test(text) || /for\s*shortcuts/i.test(text);
1522
1637
  }
1638
+ findLastMatchingLineIndex(lines, predicate) {
1639
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
1640
+ if (predicate(lines[index])) return index;
1641
+ }
1642
+ return -1;
1643
+ }
1644
+ looksLikeClaudeGeneratingLine(line) {
1645
+ const trimmed = String(line || "").trim();
1646
+ if (!trimmed) return false;
1647
+ if (/esc to (cancel|interrupt|stop)/i.test(trimmed)) return true;
1648
+ if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+\s+\S+.*\b(?:thinking|thought for \d+s?)\b/i.test(trimmed)) return true;
1649
+ if (/^[✻✶✳✢✽⠂⠐⠒⠓⠦⠴⠶⠷⠿]+\s+[A-Z][A-Za-z-]{3,}ing\b.*(?:…|\.{3})/u.test(trimmed)) return true;
1650
+ if (/^[⏺•]\s+(?:Reading|Writing|Editing|Searching|Inspecting|Planning|Analyzing|Synthesizing|Drafting|Running|Listing|Scanning|Matching)\b.*(?:…|\.{3})/i.test(trimmed)) {
1651
+ return /ctrl\+o to expand/i.test(trimmed) || /\b\d+\s+(?:file|files|pattern|patterns|director(?:y|ies)|match|matches|result|results)\b/i.test(trimmed);
1652
+ }
1653
+ return false;
1654
+ }
1655
+ detectClaudeGeneratingOverride(screenText, tail) {
1656
+ if (this.cliType !== "claude-cli") return false;
1657
+ const source = sanitizeTerminalText(screenText || tail || "");
1658
+ if (!source.trim()) return false;
1659
+ const allLines = source.split(/\r\n|\n|\r/g).map((line) => line.trim()).filter(Boolean);
1660
+ if (allLines.length === 0) return false;
1661
+ const recentLines = allLines.slice(-12);
1662
+ const promptIndex = this.findLastMatchingLineIndex(recentLines, (line) => /^[❯›>]\s*$/.test(line));
1663
+ const activeRegion = promptIndex >= 0 ? recentLines.slice(Math.max(0, promptIndex - 2), promptIndex) : recentLines;
1664
+ if (activeRegion.length === 0) return false;
1665
+ return activeRegion.some((line) => this.looksLikeClaudeGeneratingLine(line));
1666
+ }
1667
+ refineDetectedStatus(status, tail, screenText) {
1668
+ if (this.startupParseGate) {
1669
+ return this.getStartupConfirmationModal(screenText || "") ? "waiting_approval" : "starting";
1670
+ }
1671
+ if (status === "waiting_approval") return status;
1672
+ if (this.detectClaudeGeneratingOverride(screenText || "", tail)) return "generating";
1673
+ return status;
1674
+ }
1523
1675
  looksLikeVisibleAssistantCandidate(screenText) {
1524
1676
  const lines = sanitizeTerminalText(String(screenText || "")).split(/\r\n|\n|\r/g);
1525
1677
  for (const line of lines) {
@@ -1554,6 +1706,11 @@ var init_provider_cli_adapter = __esm({
1554
1706
  const screenStableMs = this.lastScreenChangeAt ? now - this.lastScreenChangeAt : 0;
1555
1707
  return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
1556
1708
  }
1709
+ hasRecentInteractiveActivity(now) {
1710
+ const quietForMs = this.lastNonEmptyOutputAt ? now - this.lastNonEmptyOutputAt : Number.MAX_SAFE_INTEGER;
1711
+ const screenStableMs = this.lastScreenChangeAt ? now - this.lastScreenChangeAt : Number.MAX_SAFE_INTEGER;
1712
+ return quietForMs < _ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS || screenStableMs < _ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS;
1713
+ }
1557
1714
  getStartupConfirmationModal(screenText) {
1558
1715
  const text = sanitizeTerminalText(String(screenText || ""));
1559
1716
  if (!text.trim()) return null;
@@ -1581,13 +1738,14 @@ var init_provider_cli_adapter = __esm({
1581
1738
  const startedAt = Date.now();
1582
1739
  let loggedWait = false;
1583
1740
  while (Date.now() - startedAt < maxWaitMs) {
1741
+ this.resolveStartupState("interactive_wait");
1584
1742
  const screenText = this.terminalScreen.getText() || "";
1585
1743
  const hasPrompt = this.looksLikeVisibleIdlePrompt(screenText);
1586
1744
  const stableMs = this.lastScreenChangeAt ? Date.now() - this.lastScreenChangeAt : 0;
1587
1745
  const recentlyOutput = this.lastNonEmptyOutputAt ? Date.now() - this.lastNonEmptyOutputAt : Number.MAX_SAFE_INTEGER;
1588
1746
  const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
1589
1747
  const startupLikelyActive = /Welcome back|Tips for getting|Recent activity|Claude Code v\d/i.test(screenText);
1590
- const interactiveReady = hasPrompt && stableMs >= 700 && recentlyOutput >= 350 && status !== "starting" && status !== "generating";
1748
+ const interactiveReady = hasPrompt && stableMs >= 700 && recentlyOutput >= 350 && status !== "generating";
1591
1749
  if (interactiveReady) {
1592
1750
  if (loggedWait) {
1593
1751
  LOG.info(
@@ -1626,6 +1784,10 @@ var init_provider_cli_adapter = __esm({
1626
1784
  }
1627
1785
  const tail = this.settledBuffer;
1628
1786
  const screenText = this.terminalScreen.getText() || "";
1787
+ this.resolveStartupState("settled");
1788
+ if (this.startupParseGate) {
1789
+ return;
1790
+ }
1629
1791
  const startupModal = this.getStartupConfirmationModal(screenText);
1630
1792
  const modal = this.runParseApproval(tail) || startupModal;
1631
1793
  const rawScriptStatus = this.runDetectStatus(tail);
@@ -1688,6 +1850,28 @@ var init_provider_cli_adapter = __esm({
1688
1850
  } else {
1689
1851
  clearPendingScriptStatus();
1690
1852
  }
1853
+ const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
1854
+ const shouldHoldGenerating = scriptStatus === "idle" && this.isWaitingForResponse && !modal && recentInteractiveActivity;
1855
+ if (shouldHoldGenerating) {
1856
+ this.clearIdleFinishCandidate("hold_generating_recent_activity");
1857
+ this.setStatus("generating", "recent_activity_hold");
1858
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
1859
+ this.idleTimeout = setTimeout(() => {
1860
+ if (this.isWaitingForResponse && this.currentStatus !== "waiting_approval") {
1861
+ this.finishResponse();
1862
+ }
1863
+ }, this.timeouts.generatingIdle);
1864
+ this.recordTrace("hold_generating_recent_activity", {
1865
+ scriptStatus,
1866
+ recentInteractiveActivity,
1867
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1868
+ lastScreenChangeAt: this.lastScreenChangeAt,
1869
+ holdMs: _ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS,
1870
+ ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer)
1871
+ });
1872
+ this.onStatusChange?.();
1873
+ return;
1874
+ }
1691
1875
  if (scriptStatus === "waiting_approval") {
1692
1876
  this.clearIdleFinishCandidate("waiting_approval");
1693
1877
  const inCooldown = this.lastApprovalResolvedAt && Date.now() - this.lastApprovalResolvedAt < this.timeouts.approvalCooldown;
@@ -1765,8 +1949,8 @@ var init_provider_cli_adapter = __esm({
1765
1949
  const screenStableMs = this.lastScreenChangeAt ? now - this.lastScreenChangeAt : 0;
1766
1950
  const hasAssistantTurn = !!lastParsedAssistant;
1767
1951
  const assistantLength = lastParsedAssistant?.content?.length || 0;
1768
- const idleQuietThresholdMs = Math.max(220, this.timeouts.outputSettle);
1769
- const idleStableThresholdMs = Math.max(120, Math.min(220, this.timeouts.outputSettle));
1952
+ const idleQuietThresholdMs = Math.max(2e3, this.timeouts.outputSettle);
1953
+ const idleStableThresholdMs = 2e3;
1770
1954
  const idleReady = visibleIdlePrompt && !modal && hasAssistantTurn && quietForMs >= idleQuietThresholdMs && screenStableMs >= idleStableThresholdMs;
1771
1955
  const candidate = this.idleFinishCandidate;
1772
1956
  const candidateQuiet = !!candidate && candidate.responseEpoch === this.responseEpoch && candidate.lastOutputAt === this.lastOutputAt && candidate.lastScreenChangeAt === this.lastScreenChangeAt && assistantLength >= candidate.assistantLength && now - candidate.armedAt >= _ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS;
@@ -1880,7 +2064,7 @@ var init_provider_cli_adapter = __esm({
1880
2064
  this.currentTurnScope
1881
2065
  );
1882
2066
  if (parsed && Array.isArray(parsed.messages)) {
1883
- this.committedMessages = this.normalizeParsedMessages(parsed.messages);
2067
+ this.committedMessages = this.normalizeParsedMessages(parsed.messages, this.currentTurnScope);
1884
2068
  const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
1885
2069
  if (promptForTrim) {
1886
2070
  const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === "assistant");
@@ -1917,11 +2101,15 @@ var init_provider_cli_adapter = __esm({
1917
2101
  runDetectStatus(text) {
1918
2102
  if (!this.cliScripts?.detectStatus) return null;
1919
2103
  try {
1920
- return this.cliScripts.detectStatus({
2104
+ const screenText = this.terminalScreen.getText();
2105
+ const status = this.cliScripts.detectStatus({
1921
2106
  tail: text.slice(-500),
1922
- screenText: this.terminalScreen.getText(),
1923
- rawBuffer: this.accumulatedRawBuffer
2107
+ screenText,
2108
+ rawBuffer: this.accumulatedRawBuffer,
2109
+ screen: buildCliScreenSnapshot(screenText),
2110
+ tailScreen: buildCliScreenSnapshot(text.slice(-500))
1924
2111
  });
2112
+ return this.refineDetectedStatus(status, text, screenText || "");
1925
2113
  } catch (e) {
1926
2114
  LOG.warn("CLI", `[${this.cliType}] detectStatus error: ${e.message}`);
1927
2115
  return null;
@@ -1930,11 +2118,16 @@ var init_provider_cli_adapter = __esm({
1930
2118
  runParseApproval(tail) {
1931
2119
  if (!this.cliScripts?.parseApproval) return null;
1932
2120
  try {
2121
+ const screenText = this.terminalScreen.getText();
2122
+ const buffer = screenText || this.accumulatedBuffer;
1933
2123
  return this.cliScripts.parseApproval({
1934
- buffer: this.terminalScreen.getText() || this.accumulatedBuffer,
1935
- screenText: this.terminalScreen.getText(),
2124
+ buffer,
2125
+ screenText,
1936
2126
  rawBuffer: this.accumulatedRawBuffer,
1937
- tail
2127
+ tail,
2128
+ screen: buildCliScreenSnapshot(screenText),
2129
+ bufferScreen: buildCliScreenSnapshot(buffer),
2130
+ tailScreen: buildCliScreenSnapshot(tail)
1938
2131
  });
1939
2132
  } catch (e) {
1940
2133
  LOG.warn("CLI", `[${this.cliType}] parseApproval error: ${e.message}`);
@@ -1950,6 +2143,21 @@ var init_provider_cli_adapter = __esm({
1950
2143
  activeModal: this.activeModal
1951
2144
  };
1952
2145
  }
2146
+ seedCommittedMessages(messages) {
2147
+ const normalized = (Array.isArray(messages) ? messages : []).filter((message) => message && (message.role === "user" || message.role === "assistant")).map((message) => ({
2148
+ role: message.role,
2149
+ content: typeof message.content === "string" ? message.content : String(message.content || ""),
2150
+ timestamp: typeof message.timestamp === "number" && Number.isFinite(message.timestamp) ? message.timestamp : void 0,
2151
+ receivedAt: typeof message.receivedAt === "number" && Number.isFinite(message.receivedAt) ? message.receivedAt : void 0,
2152
+ kind: typeof message.kind === "string" ? message.kind : void 0,
2153
+ id: typeof message.id === "string" ? message.id : void 0,
2154
+ index: typeof message.index === "number" ? message.index : void 0,
2155
+ meta: message.meta && typeof message.meta === "object" ? { ...message.meta } : void 0,
2156
+ senderName: typeof message.senderName === "string" ? message.senderName : void 0
2157
+ }));
2158
+ this.committedMessages = normalized;
2159
+ this.syncMessageViews();
2160
+ }
1953
2161
  /**
1954
2162
  * Script-based full parse — returns ReadChatResult.
1955
2163
  * Called by command handler / dashboard for rich content rendering.
@@ -1960,12 +2168,20 @@ var init_provider_cli_adapter = __esm({
1960
2168
  this.responseBuffer,
1961
2169
  this.currentTurnScope
1962
2170
  );
2171
+ const shouldPreferCommittedMessages = !this.currentTurnScope && this.currentStatus === "idle" && !this.activeModal;
1963
2172
  if (parsed && Array.isArray(parsed.messages)) {
2173
+ const hydratedMessages = shouldPreferCommittedMessages ? this.committedMessages.map((message, index) => ({
2174
+ ...message,
2175
+ id: message.id || `msg_${index}`,
2176
+ index: typeof message.index === "number" ? message.index : index,
2177
+ kind: message.kind || "standard",
2178
+ receivedAt: typeof message.receivedAt === "number" ? message.receivedAt : message.timestamp
2179
+ })) : this.hydrateParsedMessages(parsed.messages, this.currentTurnScope);
1964
2180
  return {
1965
2181
  id: parsed.id || "cli_session",
1966
2182
  status: parsed.status || this.currentStatus,
1967
2183
  title: parsed.title || this.cliName,
1968
- messages: parsed.messages,
2184
+ messages: hydratedMessages,
1969
2185
  activeModal: parsed.activeModal ?? this.activeModal,
1970
2186
  providerSessionId: typeof parsed.providerSessionId === "string" ? parsed.providerSessionId : void 0
1971
2187
  };
@@ -1986,11 +2202,30 @@ var init_provider_cli_adapter = __esm({
1986
2202
  activeModal: this.activeModal
1987
2203
  };
1988
2204
  }
2205
+ async invokeScript(scriptName, args) {
2206
+ const fn = this.cliScripts?.[scriptName];
2207
+ if (typeof fn !== "function") {
2208
+ throw new Error(`CLI script '${scriptName}' not available`);
2209
+ }
2210
+ const input = this.buildParseInput(
2211
+ this.committedMessages,
2212
+ this.responseBuffer,
2213
+ this.currentTurnScope
2214
+ );
2215
+ return await Promise.resolve(fn({
2216
+ ...input,
2217
+ args: args && typeof args === "object" ? { ...args } : {}
2218
+ }));
2219
+ }
1989
2220
  parseCurrentTranscript(baseMessages, partialResponse, scope) {
1990
2221
  if (!this.cliScripts?.parseOutput) return null;
1991
2222
  try {
1992
2223
  const input = this.buildParseInput(baseMessages, partialResponse, scope);
1993
2224
  const parsed = this.cliScripts.parseOutput(input);
2225
+ const refinedStatus = this.refineDetectedStatus(typeof parsed?.status === "string" ? parsed.status : null, input.recentBuffer, input.screenText);
2226
+ if (parsed && refinedStatus && parsed.status !== refinedStatus) {
2227
+ parsed.status = refinedStatus;
2228
+ }
1994
2229
  const promptForTrim = scope?.prompt || getLastUserPromptText(baseMessages);
1995
2230
  if (parsed && Array.isArray(parsed.messages) && promptForTrim) {
1996
2231
  const lastAssistant = [...parsed.messages].reverse().find((message) => message?.role === "assistant" && typeof message.content === "string");
@@ -2037,12 +2272,23 @@ ${data.message || ""}`.trim();
2037
2272
  if (this.startupParseGate) {
2038
2273
  const deadline = Date.now() + 1e4;
2039
2274
  while (this.startupParseGate && Date.now() < deadline) {
2275
+ this.resolveStartupState("send_wait");
2040
2276
  await new Promise((resolve9) => setTimeout(resolve9, 50));
2041
2277
  }
2042
2278
  }
2279
+ await this.waitForInteractivePrompt();
2280
+ if (!this.ready) {
2281
+ this.resolveStartupState("send_precheck");
2282
+ const screenText = this.terminalScreen.getText() || "";
2283
+ const hasPrompt = this.looksLikeVisibleIdlePrompt(screenText);
2284
+ if (hasPrompt && this.currentStatus === "idle") {
2285
+ this.ready = true;
2286
+ this.startupParseGate = false;
2287
+ LOG.info("CLI", `[${this.cliType}] sendMessage recovered idle prompt readiness`);
2288
+ }
2289
+ }
2043
2290
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
2044
2291
  if (this.isWaitingForResponse) return;
2045
- await this.waitForInteractivePrompt();
2046
2292
  const blockingModal = this.activeModal || this.getStartupConfirmationModal(this.terminalScreen.getText() || "");
2047
2293
  if (blockingModal || this.currentStatus === "waiting_approval") {
2048
2294
  throw new Error(`${this.cliName} is awaiting confirmation before it can accept a prompt`);
@@ -2086,8 +2332,6 @@ ${data.message || ""}`.trim();
2086
2332
  }
2087
2333
  this.responseEpoch += 1;
2088
2334
  this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
2089
- this.setStatus("generating", "sendMessage");
2090
- this.onStatusChange?.();
2091
2335
  const startResponseTimeout = () => {
2092
2336
  if (this.responseTimeout) clearTimeout(this.responseTimeout);
2093
2337
  this.responseTimeout = setTimeout(() => {
@@ -2106,7 +2350,7 @@ ${data.message || ""}`.trim();
2106
2350
  const retrySubmitIfStuck = (attempt) => {
2107
2351
  this.submitRetryTimer = null;
2108
2352
  if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
2109
- if (this.currentStatus !== "generating") return;
2353
+ if (this.currentStatus === "waiting_approval") return;
2110
2354
  if ((this.responseBuffer || "").trim()) return;
2111
2355
  const screenText = this.terminalScreen.getText();
2112
2356
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
@@ -2141,7 +2385,7 @@ ${data.message || ""}`.trim();
2141
2385
  this.submitRetryTimer = setTimeout(() => {
2142
2386
  this.submitRetryTimer = null;
2143
2387
  if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
2144
- if (this.currentStatus !== "generating") return;
2388
+ if (this.currentStatus === "waiting_approval") return;
2145
2389
  if ((this.responseBuffer || "").trim()) return;
2146
2390
  const screenText = this.terminalScreen.getText();
2147
2391
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
@@ -4497,199 +4741,94 @@ var StatusMonitor = class {
4497
4741
  }
4498
4742
  };
4499
4743
 
4500
- // src/providers/extension-provider-instance.ts
4501
- var ExtensionProviderInstance = class {
4502
- type;
4503
- category = "extension";
4504
- provider;
4505
- context = null;
4506
- settings = {};
4507
- events = [];
4508
- // status
4509
- currentStatus = "idle";
4510
- agentStreams = [];
4511
- messages = [];
4512
- activeModal = null;
4513
- currentModel = "";
4514
- currentMode = "";
4515
- controlValues = {};
4516
- lastAgentStatus = "idle";
4517
- generatingStartedAt = 0;
4518
- monitor;
4519
- // meta
4520
- instanceId;
4521
- ideType = "";
4522
- chatId = null;
4523
- chatTitle = null;
4524
- agentName = "";
4525
- extensionId = "";
4526
- constructor(provider) {
4527
- this.type = provider.type;
4528
- this.provider = provider;
4529
- this.instanceId = crypto.randomUUID();
4530
- this.monitor = new StatusMonitor();
4531
- }
4532
- // ─── Lifecycle ──────────────────────────────────
4533
- async init(context) {
4534
- this.context = context;
4535
- this.settings = context.settings || {};
4536
- this.monitor.updateConfig({
4537
- approvalAlert: this.settings.approvalAlert !== false,
4538
- longGeneratingAlert: this.settings.longGeneratingAlert !== false,
4539
- longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180
4540
- });
4541
- }
4542
- async onTick() {
4543
- if (!this.context?.cdp?.isConnected) return;
4544
- }
4545
- getState() {
4546
- return {
4547
- type: this.type,
4548
- name: this.provider.name,
4549
- category: "extension",
4550
- status: this.currentStatus,
4551
- activeChat: this.messages.length > 0 ? {
4552
- id: this.chatId || this.instanceId,
4553
- title: this.chatTitle || this.agentName || this.provider.name,
4554
- status: this.currentStatus,
4555
- messages: this.messages,
4556
- activeModal: this.activeModal,
4557
- inputContent: ""
4558
- } : null,
4559
- currentModel: this.currentModel || void 0,
4560
- currentPlan: this.currentMode || void 0,
4561
- controlValues: this.controlValues,
4562
- providerControls: this.provider.controls,
4563
- agentStreams: this.agentStreams,
4564
- instanceId: this.instanceId,
4565
- lastUpdated: Date.now(),
4566
- settings: this.settings,
4567
- pendingEvents: this.flushEvents()
4568
- };
4569
- }
4570
- onEvent(event, data) {
4571
- if (event === "stream_update") {
4572
- if (data?.streams) this.agentStreams = data.streams;
4573
- if (data?.messages) this.messages = data.messages;
4574
- if (data?.activeModal !== void 0) this.activeModal = data.activeModal;
4575
- if (data?.model) this.currentModel = data.model;
4576
- if (data?.mode) this.currentMode = data.mode;
4577
- if (data?.controlValues) this.controlValues = data.controlValues;
4578
- if (typeof data?.sessionId === "string" && data.sessionId.trim()) this.chatId = data.sessionId;
4579
- if (typeof data?.title === "string" && data.title.trim()) this.chatTitle = data.title;
4580
- if (typeof data?.agentName === "string" && data.agentName.trim()) this.agentName = data.agentName;
4581
- if (typeof data?.extensionId === "string" && data.extensionId.trim()) this.extensionId = data.extensionId;
4582
- if (data?.status) {
4583
- const newStatus = data.status;
4584
- this.detectTransition(newStatus, data);
4585
- this.currentStatus = newStatus;
4586
- }
4587
- } else if (event === "stream_reset") {
4588
- this.resetStreamState();
4589
- } else if (event === "extension_connected") {
4590
- this.ideType = data?.ideType || "";
4744
+ // src/providers/control-effects.ts
4745
+ function extractProviderControlValues(controls, data) {
4746
+ if (!data || typeof data !== "object") return void 0;
4747
+ const values = {};
4748
+ const explicit = data.controlValues;
4749
+ if (explicit && typeof explicit === "object") {
4750
+ for (const [key, value] of Object.entries(explicit)) {
4751
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
4752
+ values[key] = value;
4753
+ }
4754
+ }
4755
+ }
4756
+ for (const ctrl of controls || []) {
4757
+ if (!ctrl.readFrom) continue;
4758
+ const rawValue = data[ctrl.readFrom];
4759
+ if (rawValue === void 0 || rawValue === null) continue;
4760
+ values[ctrl.id] = normalizeControlValue(rawValue);
4761
+ }
4762
+ if (data.model !== void 0 && values.model === void 0) values.model = normalizeControlValue(data.model);
4763
+ if (data.mode !== void 0 && values.mode === void 0) values.mode = normalizeControlValue(data.mode);
4764
+ return Object.keys(values).length > 0 ? values : void 0;
4765
+ }
4766
+ function normalizeProviderEffects(data) {
4767
+ const rawEffects = Array.isArray(data?.effects) ? data.effects : [];
4768
+ const effects = [];
4769
+ for (const raw of rawEffects) {
4770
+ if (!raw || typeof raw !== "object") continue;
4771
+ const type = raw.type;
4772
+ if (type === "message" && raw.message && typeof raw.message === "object") {
4773
+ const content = raw.message.content;
4774
+ if (typeof content !== "string" && !Array.isArray(content)) continue;
4775
+ effects.push({
4776
+ type: "message",
4777
+ id: typeof raw.id === "string" ? raw.id : void 0,
4778
+ when: raw.when === "turn_completed" ? "turn_completed" : "immediate",
4779
+ persist: raw.persist !== false,
4780
+ message: {
4781
+ role: raw.message.role === "assistant" || raw.message.role === "user" ? raw.message.role : "system",
4782
+ content,
4783
+ kind: typeof raw.message.kind === "string" ? raw.message.kind : void 0,
4784
+ senderName: typeof raw.message.senderName === "string" ? raw.message.senderName : void 0
4785
+ }
4786
+ });
4787
+ continue;
4591
4788
  }
4592
- }
4593
- dispose() {
4594
- this.agentStreams = [];
4595
- this.messages = [];
4596
- this.monitor.reset();
4597
- }
4598
- /** Query UUID instanceId */
4599
- getInstanceId() {
4600
- return this.instanceId;
4601
- }
4602
- // ─── status transition detect ──────────────────────────────
4603
- detectTransition(newStatus, data) {
4604
- const now = Date.now();
4605
- const agentStatus = newStatus === "streaming" || newStatus === "generating" ? "generating" : newStatus === "waiting_approval" ? "waiting_approval" : "idle";
4606
- const lastMsg = Array.isArray(data?.messages) && data.messages.length > 0 ? data.messages[data.messages.length - 1] : null;
4607
- const progressFingerprint = agentStatus === "generating" ? `${lastMsg?.role || ""}:${typeof lastMsg?.content === "string" ? lastMsg.content : JSON.stringify(lastMsg?.content || "")}`.slice(-2e3) : void 0;
4608
- if (agentStatus !== this.lastAgentStatus) {
4609
- if (this.lastAgentStatus === "idle" && agentStatus === "generating") {
4610
- this.generatingStartedAt = now;
4611
- this.pushEvent({
4612
- event: "agent:generating_started",
4613
- chatTitle: this.resolveChatTitle(data),
4614
- timestamp: now,
4615
- ideType: this.ideType || this.type,
4616
- agentType: this.type,
4617
- agentName: this.agentName || this.provider.name,
4618
- extensionId: this.extensionId || this.type
4619
- });
4620
- } else if (agentStatus === "waiting_approval") {
4621
- if (!this.generatingStartedAt) this.generatingStartedAt = now;
4622
- this.pushEvent({
4623
- event: "agent:waiting_approval",
4624
- chatTitle: this.resolveChatTitle(data),
4625
- timestamp: now,
4626
- ideType: this.ideType || this.type,
4627
- agentType: this.type,
4628
- agentName: this.agentName || this.provider.name,
4629
- extensionId: this.extensionId || this.type,
4630
- modalMessage: data?.activeModal?.message,
4631
- modalButtons: data?.activeModal?.buttons
4632
- });
4633
- } else if (agentStatus === "idle" && (this.lastAgentStatus === "generating" || this.lastAgentStatus === "waiting_approval")) {
4634
- const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1e3) : 0;
4635
- this.pushEvent({
4636
- event: "agent:generating_completed",
4637
- chatTitle: this.resolveChatTitle(data),
4638
- duration,
4639
- timestamp: now,
4640
- ideType: this.ideType || this.type,
4641
- agentType: this.type,
4642
- agentName: this.agentName || this.provider.name,
4643
- extensionId: this.extensionId || this.type
4644
- });
4645
- this.generatingStartedAt = 0;
4646
- }
4647
- this.lastAgentStatus = agentStatus;
4789
+ if (type === "toast" && raw.toast && typeof raw.toast.message === "string") {
4790
+ effects.push({
4791
+ type: "toast",
4792
+ id: typeof raw.id === "string" ? raw.id : void 0,
4793
+ when: raw.when === "turn_completed" ? "turn_completed" : "immediate",
4794
+ persist: raw.persist !== false,
4795
+ toast: {
4796
+ level: raw.toast.level === "success" || raw.toast.level === "warning" ? raw.toast.level : "info",
4797
+ message: raw.toast.message
4798
+ }
4799
+ });
4800
+ continue;
4648
4801
  }
4649
- const agentKey = `${this.type}:ext`;
4650
- const monitorEvents = this.monitor.check(agentKey, agentStatus, now, progressFingerprint);
4651
- for (const me of monitorEvents) {
4652
- this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
4802
+ if (type === "notification" && raw.notification && typeof raw.notification.body === "string") {
4803
+ effects.push({
4804
+ type: "notification",
4805
+ id: typeof raw.id === "string" ? raw.id : void 0,
4806
+ when: raw.when === "turn_completed" ? "turn_completed" : "immediate",
4807
+ persist: raw.persist !== false,
4808
+ notification: {
4809
+ title: typeof raw.notification.title === "string" ? raw.notification.title : void 0,
4810
+ body: raw.notification.body,
4811
+ level: raw.notification.level === "success" || raw.notification.level === "warning" ? raw.notification.level : "info",
4812
+ channels: Array.isArray(raw.notification.channels) ? raw.notification.channels.filter((channel) => channel === "bubble" || channel === "toast" || channel === "browser") : void 0,
4813
+ preferenceKey: raw.notification.preferenceKey === "disconnect" || raw.notification.preferenceKey === "completion" || raw.notification.preferenceKey === "approval" || raw.notification.preferenceKey === "browser" ? raw.notification.preferenceKey : void 0,
4814
+ bubbleContent: typeof raw.notification.bubbleContent === "string" || Array.isArray(raw.notification.bubbleContent) ? raw.notification.bubbleContent : void 0
4815
+ }
4816
+ });
4653
4817
  }
4654
4818
  }
4655
- pushEvent(event) {
4656
- this.events.push(event);
4657
- if (this.events.length > 50) this.events = this.events.slice(-50);
4819
+ return effects;
4820
+ }
4821
+ function normalizeControlValue(value) {
4822
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
4823
+ return value;
4658
4824
  }
4659
- flushEvents() {
4660
- const events = [...this.events];
4661
- this.events = [];
4662
- return events;
4825
+ if (value && typeof value === "object") {
4826
+ if (typeof value.label === "string") return value.label;
4827
+ if (typeof value.name === "string") return value.name;
4828
+ if (typeof value.id === "string") return value.id;
4663
4829
  }
4664
- resolveChatTitle(data) {
4665
- const title = typeof data?.title === "string" && data.title.trim() ? data.title.trim() : this.chatTitle;
4666
- return title || this.agentName || this.provider.name;
4667
- }
4668
- resetStreamState() {
4669
- if (this.currentStatus !== "idle") {
4670
- this.detectTransition("idle", {
4671
- title: this.chatTitle,
4672
- agentName: this.agentName,
4673
- extensionId: this.extensionId,
4674
- messages: this.messages
4675
- });
4676
- }
4677
- this.agentStreams = [];
4678
- this.messages = [];
4679
- this.activeModal = null;
4680
- this.currentModel = "";
4681
- this.currentMode = "";
4682
- this.controlValues = {};
4683
- this.currentStatus = "idle";
4684
- this.chatId = null;
4685
- this.chatTitle = null;
4686
- this.agentName = "";
4687
- this.extensionId = "";
4688
- this.lastAgentStatus = "idle";
4689
- this.generatingStartedAt = 0;
4690
- this.monitor.reset();
4691
- }
4692
- };
4830
+ return String(value);
4831
+ }
4693
4832
 
4694
4833
  // src/config/chat-history.ts
4695
4834
  import * as fs3 from "fs";
@@ -4916,54 +5055,383 @@ function listSavedHistorySessions(agentType, options = {}) {
4916
5055
  files.push(file);
4917
5056
  groupedFiles.set(historySessionId, files);
4918
5057
  }
4919
- const summaries = [];
4920
- for (const [historySessionId, files] of groupedFiles.entries()) {
4921
- let messageCount = 0;
4922
- let firstMessageAt = 0;
4923
- let lastMessageAt = 0;
4924
- let sessionTitle = "";
4925
- let preview = "";
4926
- for (const file of files.sort()) {
4927
- const filePath = path5.join(dir, file);
4928
- const content = fs3.readFileSync(filePath, "utf-8");
4929
- const lines = content.split("\n").filter(Boolean);
4930
- for (const line of lines) {
4931
- let parsed = null;
4932
- try {
4933
- parsed = JSON.parse(line);
4934
- } catch {
4935
- parsed = null;
4936
- }
4937
- if (!parsed || parsed.historySessionId !== historySessionId) continue;
4938
- messageCount += 1;
4939
- if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
4940
- if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
4941
- if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
4942
- if (parsed.role !== "system" && parsed.content.trim()) preview = parsed.content.trim();
4943
- }
5058
+ const summaries = [];
5059
+ for (const [historySessionId, files] of groupedFiles.entries()) {
5060
+ let messageCount = 0;
5061
+ let firstMessageAt = 0;
5062
+ let lastMessageAt = 0;
5063
+ let sessionTitle = "";
5064
+ let preview = "";
5065
+ for (const file of files.sort()) {
5066
+ const filePath = path5.join(dir, file);
5067
+ const content = fs3.readFileSync(filePath, "utf-8");
5068
+ const lines = content.split("\n").filter(Boolean);
5069
+ for (const line of lines) {
5070
+ let parsed = null;
5071
+ try {
5072
+ parsed = JSON.parse(line);
5073
+ } catch {
5074
+ parsed = null;
5075
+ }
5076
+ if (!parsed || parsed.historySessionId !== historySessionId) continue;
5077
+ messageCount += 1;
5078
+ if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
5079
+ if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
5080
+ if (parsed.sessionTitle) sessionTitle = parsed.sessionTitle;
5081
+ if (parsed.role !== "system" && parsed.content.trim()) preview = parsed.content.trim();
5082
+ }
5083
+ }
5084
+ if (messageCount === 0 || !lastMessageAt) continue;
5085
+ summaries.push({
5086
+ historySessionId,
5087
+ sessionTitle: sessionTitle || void 0,
5088
+ messageCount,
5089
+ firstMessageAt,
5090
+ lastMessageAt,
5091
+ preview: preview || void 0
5092
+ });
5093
+ }
5094
+ summaries.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
5095
+ const offset = Math.max(0, options.offset || 0);
5096
+ const limit = Math.max(1, options.limit || 30);
5097
+ const sliced = summaries.slice(offset, offset + limit);
5098
+ return {
5099
+ sessions: sliced,
5100
+ hasMore: summaries.length > offset + limit
5101
+ };
5102
+ } catch {
5103
+ return { sessions: [], hasMore: false };
5104
+ }
5105
+ }
5106
+
5107
+ // src/providers/extension-provider-instance.ts
5108
+ var ExtensionProviderInstance = class {
5109
+ type;
5110
+ category = "extension";
5111
+ provider;
5112
+ context = null;
5113
+ settings = {};
5114
+ events = [];
5115
+ // status
5116
+ currentStatus = "idle";
5117
+ agentStreams = [];
5118
+ messages = [];
5119
+ activeModal = null;
5120
+ currentModel = "";
5121
+ currentMode = "";
5122
+ controlValues = {};
5123
+ appliedEffectKeys = /* @__PURE__ */ new Set();
5124
+ runtimeMessages = [];
5125
+ lastAgentStatus = "idle";
5126
+ generatingStartedAt = 0;
5127
+ monitor;
5128
+ historyWriter;
5129
+ // meta
5130
+ instanceId;
5131
+ ideType = "";
5132
+ chatId = null;
5133
+ chatTitle = null;
5134
+ agentName = "";
5135
+ extensionId = "";
5136
+ constructor(provider) {
5137
+ this.type = provider.type;
5138
+ this.provider = provider;
5139
+ this.instanceId = crypto.randomUUID();
5140
+ this.monitor = new StatusMonitor();
5141
+ this.historyWriter = new ChatHistoryWriter();
5142
+ }
5143
+ // ─── Lifecycle ──────────────────────────────────
5144
+ async init(context) {
5145
+ this.context = context;
5146
+ this.settings = context.settings || {};
5147
+ this.monitor.updateConfig({
5148
+ approvalAlert: this.settings.approvalAlert !== false,
5149
+ longGeneratingAlert: this.settings.longGeneratingAlert !== false,
5150
+ longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180
5151
+ });
5152
+ }
5153
+ async onTick() {
5154
+ if (!this.context?.cdp?.isConnected) return;
5155
+ }
5156
+ getState() {
5157
+ return {
5158
+ type: this.type,
5159
+ name: this.provider.name,
5160
+ category: "extension",
5161
+ status: this.currentStatus,
5162
+ activeChat: this.messages.length > 0 || this.runtimeMessages.length > 0 ? {
5163
+ id: this.chatId || this.instanceId,
5164
+ title: this.chatTitle || this.agentName || this.provider.name,
5165
+ status: this.currentStatus,
5166
+ messages: this.mergeConversationMessages(this.messages),
5167
+ activeModal: this.activeModal,
5168
+ inputContent: ""
5169
+ } : null,
5170
+ currentModel: this.currentModel || void 0,
5171
+ currentPlan: this.currentMode || void 0,
5172
+ controlValues: this.controlValues,
5173
+ providerControls: this.provider.controls,
5174
+ agentStreams: this.agentStreams,
5175
+ instanceId: this.instanceId,
5176
+ lastUpdated: Date.now(),
5177
+ settings: this.settings,
5178
+ pendingEvents: this.flushEvents()
5179
+ };
5180
+ }
5181
+ onEvent(event, data) {
5182
+ if (event === "stream_update") {
5183
+ if (data?.streams) this.agentStreams = data.streams;
5184
+ if (data?.messages) this.messages = data.messages;
5185
+ if (data?.activeModal !== void 0) this.activeModal = data.activeModal;
5186
+ if (data?.model) this.currentModel = data.model;
5187
+ if (data?.mode) this.currentMode = data.mode;
5188
+ const controlValues = extractProviderControlValues(this.provider.controls, data) || data?.controlValues;
5189
+ if (controlValues) this.controlValues = controlValues;
5190
+ if (typeof data?.sessionId === "string" && data.sessionId.trim()) this.chatId = data.sessionId;
5191
+ if (typeof data?.title === "string" && data.title.trim()) this.chatTitle = data.title;
5192
+ if (typeof data?.agentName === "string" && data.agentName.trim()) this.agentName = data.agentName;
5193
+ if (typeof data?.extensionId === "string" && data.extensionId.trim()) this.extensionId = data.extensionId;
5194
+ if (data?.status) {
5195
+ const newStatus = data.status;
5196
+ this.detectTransition(newStatus, data);
5197
+ this.currentStatus = newStatus;
5198
+ }
5199
+ } else if (event === "stream_reset") {
5200
+ this.resetStreamState();
5201
+ } else if (event === "extension_connected") {
5202
+ this.ideType = data?.ideType || "";
5203
+ } else if (event === "provider_state_patch" && data && typeof data === "object") {
5204
+ this.applyProviderResponse(data, { phase: "immediate" });
5205
+ }
5206
+ }
5207
+ dispose() {
5208
+ this.agentStreams = [];
5209
+ this.messages = [];
5210
+ this.monitor.reset();
5211
+ this.appliedEffectKeys.clear();
5212
+ this.runtimeMessages = [];
5213
+ }
5214
+ updateSettings(newSettings) {
5215
+ this.settings = { ...newSettings };
5216
+ this.monitor.updateConfig({
5217
+ approvalAlert: this.settings.approvalAlert !== false,
5218
+ longGeneratingAlert: this.settings.longGeneratingAlert !== false,
5219
+ longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180
5220
+ });
5221
+ }
5222
+ /** Query UUID instanceId */
5223
+ getInstanceId() {
5224
+ return this.instanceId;
5225
+ }
5226
+ // ─── status transition detect ──────────────────────────────
5227
+ detectTransition(newStatus, data) {
5228
+ const now = Date.now();
5229
+ const agentStatus = newStatus === "streaming" || newStatus === "generating" ? "generating" : newStatus === "waiting_approval" ? "waiting_approval" : "idle";
5230
+ const lastMsg = Array.isArray(data?.messages) && data.messages.length > 0 ? data.messages[data.messages.length - 1] : null;
5231
+ const progressFingerprint = agentStatus === "generating" ? `${lastMsg?.role || ""}:${typeof lastMsg?.content === "string" ? lastMsg.content : JSON.stringify(lastMsg?.content || "")}`.slice(-2e3) : void 0;
5232
+ const previousStatus = this.lastAgentStatus;
5233
+ if (agentStatus !== this.lastAgentStatus) {
5234
+ if (this.lastAgentStatus === "idle" && agentStatus === "generating") {
5235
+ this.generatingStartedAt = now;
5236
+ this.pushEvent({
5237
+ event: "agent:generating_started",
5238
+ chatTitle: this.resolveChatTitle(data),
5239
+ timestamp: now,
5240
+ ideType: this.ideType || this.type,
5241
+ agentType: this.type,
5242
+ agentName: this.agentName || this.provider.name,
5243
+ extensionId: this.extensionId || this.type
5244
+ });
5245
+ } else if (agentStatus === "waiting_approval") {
5246
+ if (!this.generatingStartedAt) this.generatingStartedAt = now;
5247
+ this.pushEvent({
5248
+ event: "agent:waiting_approval",
5249
+ chatTitle: this.resolveChatTitle(data),
5250
+ timestamp: now,
5251
+ ideType: this.ideType || this.type,
5252
+ agentType: this.type,
5253
+ agentName: this.agentName || this.provider.name,
5254
+ extensionId: this.extensionId || this.type,
5255
+ modalMessage: data?.activeModal?.message,
5256
+ modalButtons: data?.activeModal?.buttons
5257
+ });
5258
+ } else if (agentStatus === "idle" && (this.lastAgentStatus === "generating" || this.lastAgentStatus === "waiting_approval")) {
5259
+ const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1e3) : 0;
5260
+ this.pushEvent({
5261
+ event: "agent:generating_completed",
5262
+ chatTitle: this.resolveChatTitle(data),
5263
+ duration,
5264
+ timestamp: now,
5265
+ ideType: this.ideType || this.type,
5266
+ agentType: this.type,
5267
+ agentName: this.agentName || this.provider.name,
5268
+ extensionId: this.extensionId || this.type
5269
+ });
5270
+ this.generatingStartedAt = 0;
5271
+ }
5272
+ this.lastAgentStatus = agentStatus;
5273
+ }
5274
+ this.applyProviderResponse(data, {
5275
+ phase: agentStatus === "idle" && (previousStatus === "generating" || previousStatus === "waiting_approval") ? "turn_completed" : "immediate"
5276
+ });
5277
+ const agentKey = `${this.type}:ext`;
5278
+ const monitorEvents = this.monitor.check(agentKey, agentStatus, now, progressFingerprint);
5279
+ for (const me of monitorEvents) {
5280
+ this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
5281
+ }
5282
+ }
5283
+ pushEvent(event) {
5284
+ this.events.push(event);
5285
+ if (this.events.length > 50) this.events = this.events.slice(-50);
5286
+ }
5287
+ applyProviderResponse(data, options) {
5288
+ if (!data || typeof data !== "object") return;
5289
+ const controlValues = extractProviderControlValues(this.provider.controls, data);
5290
+ if (controlValues) this.controlValues = { ...this.controlValues, ...controlValues };
5291
+ const effects = normalizeProviderEffects(data);
5292
+ for (const effect of effects) {
5293
+ const effectWhen = effect.when || "immediate";
5294
+ if (effectWhen === "turn_completed" && options.phase !== "turn_completed") continue;
5295
+ if (effectWhen === "immediate" && options.phase === "turn_completed") continue;
5296
+ const effectKey = this.getEffectDedupKey(effect);
5297
+ if (this.appliedEffectKeys.has(effectKey)) continue;
5298
+ this.appliedEffectKeys.add(effectKey);
5299
+ if (effect.persist !== false) {
5300
+ const persisted = this.getPersistedEffectContent(effect);
5301
+ if (persisted) this.appendRuntimeSystemMessage(persisted, effectKey);
5302
+ }
5303
+ if (effect.type === "message" && effect.message) {
5304
+ this.pushEvent({
5305
+ event: "provider:message",
5306
+ timestamp: Date.now(),
5307
+ content: typeof effect.message.content === "string" ? effect.message.content : JSON.stringify(effect.message.content),
5308
+ role: effect.message.role || "system",
5309
+ kind: effect.message.kind,
5310
+ senderName: effect.message.senderName
5311
+ });
5312
+ } else if (effect.type === "toast" && effect.toast) {
5313
+ this.pushEvent({
5314
+ event: "provider:toast",
5315
+ effectId: effect.id || effectKey,
5316
+ timestamp: Date.now(),
5317
+ message: effect.toast.message,
5318
+ level: effect.toast.level || "info"
5319
+ });
5320
+ } else if (effect.type === "notification" && effect.notification) {
5321
+ this.pushEvent({
5322
+ event: "provider:notification",
5323
+ effectId: effect.id || effectKey,
5324
+ timestamp: Date.now(),
5325
+ title: effect.notification.title,
5326
+ message: effect.notification.body,
5327
+ content: typeof effect.notification.bubbleContent === "string" ? effect.notification.bubbleContent : effect.notification.body,
5328
+ level: effect.notification.level || "info",
5329
+ channels: effect.notification.channels || ["toast"],
5330
+ preferenceKey: effect.notification.preferenceKey
5331
+ });
5332
+ }
5333
+ }
5334
+ }
5335
+ appendRuntimeSystemMessage(content, dedupKey, receivedAt = Date.now()) {
5336
+ const normalizedContent = String(content || "").trim();
5337
+ if (!normalizedContent) return;
5338
+ if (this.runtimeMessages.some((entry) => entry.key === dedupKey)) return;
5339
+ this.runtimeMessages.push({
5340
+ key: dedupKey,
5341
+ message: {
5342
+ role: "system",
5343
+ senderName: "System",
5344
+ content: normalizedContent,
5345
+ receivedAt,
5346
+ timestamp: receivedAt
5347
+ }
5348
+ });
5349
+ if (this.runtimeMessages.length > 50) this.runtimeMessages = this.runtimeMessages.slice(-50);
5350
+ this.historyWriter.appendNewMessages(
5351
+ this.type,
5352
+ [{
5353
+ role: "system",
5354
+ senderName: "System",
5355
+ content: normalizedContent,
5356
+ kind: "system",
5357
+ receivedAt,
5358
+ historyDedupKey: dedupKey
5359
+ }],
5360
+ this.chatTitle || this.agentName || this.provider.name,
5361
+ this.instanceId,
5362
+ this.chatId || this.instanceId
5363
+ );
5364
+ }
5365
+ mergeConversationMessages(messages) {
5366
+ if (this.runtimeMessages.length === 0) return messages;
5367
+ return [...messages, ...this.runtimeMessages.map((entry) => entry.message)].map((message, index) => ({ message, index })).sort((a, b) => {
5368
+ const aTime = a.message.receivedAt || a.message.timestamp || 0;
5369
+ const bTime = b.message.receivedAt || b.message.timestamp || 0;
5370
+ if (aTime !== bTime) return aTime - bTime;
5371
+ return a.index - b.index;
5372
+ }).map((entry) => entry.message);
5373
+ }
5374
+ getPersistedEffectContent(effect) {
5375
+ if (effect.type === "message") {
5376
+ return typeof effect.message?.content === "string" ? effect.message.content : JSON.stringify(effect.message?.content || "");
5377
+ }
5378
+ if (effect.type === "toast") {
5379
+ return effect.toast?.message || null;
5380
+ }
5381
+ if (effect.type === "notification") {
5382
+ if (typeof effect.notification?.bubbleContent === "string") return effect.notification.bubbleContent;
5383
+ if (typeof effect.notification?.title === "string" && effect.notification.title.trim()) {
5384
+ return `${effect.notification.title}
5385
+ ${effect.notification.body || ""}`.trim();
4944
5386
  }
4945
- if (messageCount === 0 || !lastMessageAt) continue;
4946
- summaries.push({
4947
- historySessionId,
4948
- sessionTitle: sessionTitle || void 0,
4949
- messageCount,
4950
- firstMessageAt,
4951
- lastMessageAt,
4952
- preview: preview || void 0
5387
+ return effect.notification?.body || null;
5388
+ }
5389
+ return null;
5390
+ }
5391
+ getEffectDedupKey(effect) {
5392
+ if (effect.id) return `provider_effect:${effect.id}`;
5393
+ if (effect.type === "message") {
5394
+ return `provider_effect:message:${typeof effect.message?.content === "string" ? effect.message.content : JSON.stringify(effect.message?.content || "")}`;
5395
+ }
5396
+ if (effect.type === "notification") {
5397
+ return `provider_effect:notification:${effect.notification?.title || ""}:${effect.notification?.body || ""}`;
5398
+ }
5399
+ return `provider_effect:toast:${effect.toast?.message || ""}`;
5400
+ }
5401
+ flushEvents() {
5402
+ const events = [...this.events];
5403
+ this.events = [];
5404
+ return events;
5405
+ }
5406
+ resolveChatTitle(data) {
5407
+ const title = typeof data?.title === "string" && data.title.trim() ? data.title.trim() : this.chatTitle;
5408
+ return title || this.agentName || this.provider.name;
5409
+ }
5410
+ resetStreamState() {
5411
+ if (this.currentStatus !== "idle") {
5412
+ this.detectTransition("idle", {
5413
+ title: this.chatTitle,
5414
+ agentName: this.agentName,
5415
+ extensionId: this.extensionId,
5416
+ messages: this.messages
4953
5417
  });
4954
5418
  }
4955
- summaries.sort((a, b) => b.lastMessageAt - a.lastMessageAt);
4956
- const offset = Math.max(0, options.offset || 0);
4957
- const limit = Math.max(1, options.limit || 30);
4958
- const sliced = summaries.slice(offset, offset + limit);
4959
- return {
4960
- sessions: sliced,
4961
- hasMore: summaries.length > offset + limit
4962
- };
4963
- } catch {
4964
- return { sessions: [], hasMore: false };
5419
+ this.agentStreams = [];
5420
+ this.messages = [];
5421
+ this.activeModal = null;
5422
+ this.currentModel = "";
5423
+ this.currentMode = "";
5424
+ this.controlValues = {};
5425
+ this.currentStatus = "idle";
5426
+ this.chatId = null;
5427
+ this.chatTitle = null;
5428
+ this.agentName = "";
5429
+ this.extensionId = "";
5430
+ this.lastAgentStatus = "idle";
5431
+ this.generatingStartedAt = 0;
5432
+ this.monitor.reset();
4965
5433
  }
4966
- }
5434
+ };
4967
5435
 
4968
5436
  // src/providers/ide-provider-instance.ts
4969
5437
  init_logger();
@@ -4984,6 +5452,8 @@ var IdeProviderInstance = class {
4984
5452
  monitor;
4985
5453
  historyWriter;
4986
5454
  autoApproveBusy = false;
5455
+ appliedEffectKeys = /* @__PURE__ */ new Set();
5456
+ runtimeMessages = [];
4987
5457
  // IDE meta
4988
5458
  ideVersion = "";
4989
5459
  instanceId;
@@ -5044,7 +5514,7 @@ var IdeProviderInstance = class {
5044
5514
  id: this.cachedChat.id || "active_session",
5045
5515
  title: this.cachedChat.title || this.type,
5046
5516
  status: this.cachedChat.status || this.currentStatus,
5047
- messages: this.cachedChat.messages || [],
5517
+ messages: this.mergeConversationMessages(this.cachedChat.messages || []),
5048
5518
  activeModal: this.cachedChat.activeModal || null,
5049
5519
  inputContent: this.cachedChat.inputContent || ""
5050
5520
  } : null,
@@ -5084,6 +5554,13 @@ var IdeProviderInstance = class {
5084
5554
  for (const ext of this.extensions.values()) {
5085
5555
  ext.onEvent("stream_reset");
5086
5556
  }
5557
+ } else if (event === "provider_state_patch" && data && typeof data === "object") {
5558
+ const extType = typeof data.extensionType === "string" ? data.extensionType : "";
5559
+ if (extType && this.extensions.has(extType)) {
5560
+ this.extensions.get(extType).onEvent("provider_state_patch", data);
5561
+ } else {
5562
+ this.applyProviderResponse(data, { phase: "immediate" });
5563
+ }
5087
5564
  }
5088
5565
  }
5089
5566
  dispose() {
@@ -5091,11 +5568,21 @@ var IdeProviderInstance = class {
5091
5568
  this.lastAgentStatuses.clear();
5092
5569
  this.generatingStartedAt.clear();
5093
5570
  this.monitor.reset();
5571
+ this.appliedEffectKeys.clear();
5572
+ this.runtimeMessages = [];
5094
5573
  for (const ext of this.extensions.values()) {
5095
5574
  ext.dispose();
5096
5575
  }
5097
5576
  this.extensions.clear();
5098
5577
  }
5578
+ updateSettings(newSettings) {
5579
+ this.settings = { ...newSettings };
5580
+ this.monitor.updateConfig({
5581
+ approvalAlert: this.settings.approvalAlert !== false,
5582
+ longGeneratingAlert: this.settings.longGeneratingAlert !== false,
5583
+ longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180
5584
+ });
5585
+ }
5099
5586
  // ─── Extension manage ─────────────────────────────
5100
5587
  /** Extension Instance add */
5101
5588
  async addExtension(provider, settings) {
@@ -5208,6 +5695,8 @@ var IdeProviderInstance = class {
5208
5695
  raw.messages = raw.messages.filter((m) => !hiddenKinds.has(m.kind));
5209
5696
  }
5210
5697
  }
5698
+ const controlValues = extractProviderControlValues(this.provider.controls, raw);
5699
+ if (controlValues) raw.controlValues = controlValues;
5211
5700
  this.cachedChat = { ...raw, activeModal };
5212
5701
  this.detectAgentTransitions(raw, now);
5213
5702
  if (raw.messages?.length > 0) {
@@ -5274,6 +5763,9 @@ var IdeProviderInstance = class {
5274
5763
  }
5275
5764
  this.lastAgentStatuses.set(agentKey, agentStatus);
5276
5765
  }
5766
+ this.applyProviderResponse(chatData, {
5767
+ phase: agentStatus === "idle" && (lastStatus === "generating" || lastStatus === "waiting_approval") ? "turn_completed" : "immediate"
5768
+ });
5277
5769
  if (agentStatus === "waiting_approval" && this.settings.autoApprove && !this.autoApproveBusy) {
5278
5770
  this.autoApproveViaScript(chatData);
5279
5771
  }
@@ -5286,6 +5778,136 @@ var IdeProviderInstance = class {
5286
5778
  this.events.push(event);
5287
5779
  if (this.events.length > 50) this.events = this.events.slice(-50);
5288
5780
  }
5781
+ applyProviderResponse(data, options) {
5782
+ if (!data || typeof data !== "object") return;
5783
+ const controlValues = extractProviderControlValues(this.provider.controls, data);
5784
+ if (controlValues) {
5785
+ this.cachedChat = {
5786
+ ...this.cachedChat || {},
5787
+ ...data,
5788
+ controlValues: { ...this.cachedChat?.controlValues || {}, ...controlValues }
5789
+ };
5790
+ }
5791
+ const effects = normalizeProviderEffects(data);
5792
+ for (const effect of effects) {
5793
+ const effectWhen = effect.when || "immediate";
5794
+ if (effectWhen === "turn_completed" && options.phase !== "turn_completed") continue;
5795
+ if (effectWhen === "immediate" && options.phase === "turn_completed") continue;
5796
+ const effectKey = this.getEffectDedupKey(effect);
5797
+ if (this.appliedEffectKeys.has(effectKey)) continue;
5798
+ this.appliedEffectKeys.add(effectKey);
5799
+ if (effect.persist !== false) {
5800
+ const persisted = this.getPersistedEffectContent(effect);
5801
+ if (persisted) this.appendRuntimeSystemMessage(persisted, effectKey);
5802
+ }
5803
+ if (effect.type === "message" && effect.message) {
5804
+ this.pushEvent({
5805
+ event: "provider:message",
5806
+ timestamp: Date.now(),
5807
+ content: typeof effect.message.content === "string" ? effect.message.content : JSON.stringify(effect.message.content),
5808
+ role: effect.message.role || "system",
5809
+ kind: effect.message.kind,
5810
+ senderName: effect.message.senderName
5811
+ });
5812
+ } else if (effect.type === "toast" && effect.toast) {
5813
+ this.pushEvent({
5814
+ event: "provider:toast",
5815
+ effectId: effect.id || effectKey,
5816
+ timestamp: Date.now(),
5817
+ message: effect.toast.message,
5818
+ level: effect.toast.level || "info"
5819
+ });
5820
+ } else if (effect.type === "notification" && effect.notification) {
5821
+ this.pushEvent({
5822
+ event: "provider:notification",
5823
+ effectId: effect.id || effectKey,
5824
+ timestamp: Date.now(),
5825
+ title: effect.notification.title,
5826
+ message: effect.notification.body,
5827
+ content: typeof effect.notification.bubbleContent === "string" ? effect.notification.bubbleContent : effect.notification.body,
5828
+ level: effect.notification.level || "info",
5829
+ channels: effect.notification.channels || ["toast"],
5830
+ preferenceKey: effect.notification.preferenceKey
5831
+ });
5832
+ }
5833
+ }
5834
+ }
5835
+ appendRuntimeSystemMessage(content, dedupKey, receivedAt = Date.now()) {
5836
+ const normalizedContent = String(content || "").trim();
5837
+ if (!normalizedContent) return;
5838
+ if (this.runtimeMessages.some((entry) => entry.key === dedupKey)) return;
5839
+ if (!this.cachedChat) {
5840
+ this.cachedChat = {
5841
+ id: "active_session",
5842
+ title: this.provider.name,
5843
+ status: this.currentStatus,
5844
+ messages: [],
5845
+ activeModal: null,
5846
+ inputContent: ""
5847
+ };
5848
+ }
5849
+ this.runtimeMessages.push({
5850
+ key: dedupKey,
5851
+ message: {
5852
+ role: "system",
5853
+ senderName: "System",
5854
+ content: normalizedContent,
5855
+ receivedAt,
5856
+ timestamp: receivedAt
5857
+ }
5858
+ });
5859
+ if (this.runtimeMessages.length > 50) this.runtimeMessages = this.runtimeMessages.slice(-50);
5860
+ this.historyWriter.appendNewMessages(
5861
+ this.type,
5862
+ [{
5863
+ role: "system",
5864
+ senderName: "System",
5865
+ content: normalizedContent,
5866
+ kind: "system",
5867
+ receivedAt,
5868
+ historyDedupKey: dedupKey
5869
+ }],
5870
+ this.cachedChat?.title || this.provider.name,
5871
+ this.instanceId,
5872
+ this.cachedChat?.id || this.instanceId
5873
+ );
5874
+ }
5875
+ mergeConversationMessages(messages) {
5876
+ if (this.runtimeMessages.length === 0) return messages;
5877
+ return [...messages, ...this.runtimeMessages.map((entry) => entry.message)].map((message, index) => ({ message, index })).sort((a, b) => {
5878
+ const aTime = a.message.receivedAt || a.message.timestamp || 0;
5879
+ const bTime = b.message.receivedAt || b.message.timestamp || 0;
5880
+ if (aTime !== bTime) return aTime - bTime;
5881
+ return a.index - b.index;
5882
+ }).map((entry) => entry.message);
5883
+ }
5884
+ getPersistedEffectContent(effect) {
5885
+ if (effect.type === "message") {
5886
+ return typeof effect.message?.content === "string" ? effect.message.content : JSON.stringify(effect.message?.content || "");
5887
+ }
5888
+ if (effect.type === "toast") {
5889
+ return effect.toast?.message || null;
5890
+ }
5891
+ if (effect.type === "notification") {
5892
+ if (typeof effect.notification?.bubbleContent === "string") return effect.notification.bubbleContent;
5893
+ if (typeof effect.notification?.title === "string" && effect.notification.title.trim()) {
5894
+ return `${effect.notification.title}
5895
+ ${effect.notification.body || ""}`.trim();
5896
+ }
5897
+ return effect.notification?.body || null;
5898
+ }
5899
+ return null;
5900
+ }
5901
+ getEffectDedupKey(effect) {
5902
+ if (effect.id) return `provider_effect:${effect.id}`;
5903
+ if (effect.type === "message") {
5904
+ return `provider_effect:message:${typeof effect.message?.content === "string" ? effect.message.content : JSON.stringify(effect.message?.content || "")}`;
5905
+ }
5906
+ if (effect.type === "notification") {
5907
+ return `provider_effect:notification:${effect.notification?.title || ""}:${effect.notification?.body || ""}`;
5908
+ }
5909
+ return `provider_effect:toast:${effect.toast?.message || ""}`;
5910
+ }
5289
5911
  flushEvents() {
5290
5912
  const events = [...this.events];
5291
5913
  this.events = [];
@@ -7359,7 +7981,56 @@ function handleSetProviderSetting(h, args) {
7359
7981
  }
7360
7982
  return { success: false, error: `Failed to set ${providerType}.${key} \u2014 invalid key, value, or not a public setting` };
7361
7983
  }
7362
- async function handleExtensionScript(h, args, scriptName) {
7984
+ function normalizeProviderScriptArgs(args) {
7985
+ const normalizedArgs = { ...args || {} };
7986
+ for (const key of ["mode", "model", "message", "action", "button", "text", "sessionId", "value"]) {
7987
+ if (key in normalizedArgs && !(key.toUpperCase() in normalizedArgs)) {
7988
+ normalizedArgs[key.toUpperCase()] = normalizedArgs[key];
7989
+ }
7990
+ }
7991
+ return normalizedArgs;
7992
+ }
7993
+ function parseScriptResult(result) {
7994
+ if (typeof result === "string") {
7995
+ try {
7996
+ const parsed = JSON.parse(result);
7997
+ if (parsed && typeof parsed === "object" && parsed.success === false) {
7998
+ return { success: false, payload: parsed };
7999
+ }
8000
+ return { success: true, payload: parsed };
8001
+ } catch {
8002
+ return { success: true, payload: { result } };
8003
+ }
8004
+ }
8005
+ if (result && typeof result === "object" && result.success === false) {
8006
+ return { success: false, payload: result };
8007
+ }
8008
+ return { success: true, payload: result };
8009
+ }
8010
+ function getCliScriptCommand(payload) {
8011
+ if (!payload || typeof payload !== "object") return null;
8012
+ if (typeof payload.sendMessage === "string" && payload.sendMessage.trim()) {
8013
+ return { type: "send_message", text: payload.sendMessage.trim() };
8014
+ }
8015
+ const command = payload.command;
8016
+ if (!command || typeof command !== "object") return null;
8017
+ if (command.type !== "send_message") return null;
8018
+ const text = typeof command.text === "string" ? command.text.trim() : typeof command.message === "string" ? command.message.trim() : "";
8019
+ if (!text) return null;
8020
+ return { type: "send_message", text };
8021
+ }
8022
+ function applyProviderPatch(h, args, payload) {
8023
+ if (!payload || typeof payload !== "object") return;
8024
+ const targetSessionId = typeof args?.targetSessionId === "string" ? args.targetSessionId.trim() : "";
8025
+ const targetSession = targetSessionId ? h.ctx.sessionRegistry?.get(targetSessionId) : void 0;
8026
+ const instanceKey = targetSession?.instanceKey || targetSessionId;
8027
+ if (!instanceKey) return;
8028
+ h.ctx.instanceManager?.sendEvent(instanceKey, "provider_state_patch", {
8029
+ ...payload,
8030
+ extensionType: targetSession?.transport === "cdp-webview" ? targetSession.providerType : void 0
8031
+ });
8032
+ }
8033
+ async function executeProviderScript(h, args, scriptName) {
7363
8034
  const { agentType, ideType } = args || {};
7364
8035
  if (!agentType) return { success: false, error: "agentType is required" };
7365
8036
  const loader = h.ctx.providerLoader;
@@ -7372,13 +8043,29 @@ async function handleExtensionScript(h, args, scriptName) {
7372
8043
  if (!provider.scripts?.[actualScriptName]) {
7373
8044
  return { success: false, error: `Script '${actualScriptName}' not available for ${agentType}` };
7374
8045
  }
7375
- const scriptFn = provider.scripts[actualScriptName];
7376
- const normalizedArgs = { ...args };
7377
- for (const key of ["mode", "model", "message", "action", "button", "text", "sessionId"]) {
7378
- if (key in normalizedArgs && !(key.toUpperCase() in normalizedArgs)) {
7379
- normalizedArgs[key.toUpperCase()] = normalizedArgs[key];
8046
+ const normalizedArgs = normalizeProviderScriptArgs(args);
8047
+ if (provider.category === "cli") {
8048
+ const adapter = h.getCliAdapter(args?.targetSessionId || agentType);
8049
+ if (!adapter?.invokeScript) {
8050
+ return { success: false, error: `CLI adapter does not support script '${actualScriptName}'` };
8051
+ }
8052
+ try {
8053
+ const raw = await adapter.invokeScript(actualScriptName, normalizedArgs);
8054
+ const parsed = parseScriptResult(raw);
8055
+ if (!parsed.success) {
8056
+ return { success: false, ...parsed.payload || {} };
8057
+ }
8058
+ const cliCommand = getCliScriptCommand(parsed.payload);
8059
+ if (cliCommand?.type === "send_message" && cliCommand.text) {
8060
+ await adapter.sendMessage(cliCommand.text);
8061
+ }
8062
+ applyProviderPatch(h, args, parsed.payload);
8063
+ return { success: true, ...parsed.payload && typeof parsed.payload === "object" ? parsed.payload : { result: parsed.payload } };
8064
+ } catch (e) {
8065
+ return { success: false, error: `Script execution failed: ${e.message}` };
7380
8066
  }
7381
8067
  }
8068
+ const scriptFn = provider.scripts[actualScriptName];
7382
8069
  const scriptCode = scriptFn(normalizedArgs);
7383
8070
  if (!scriptCode) return { success: false, error: `Script '${actualScriptName}' returned null` };
7384
8071
  const cdpKey = provider.category === "ide" ? h.currentSession?.cdpManagerKey || h.currentManagerKey || agentType : h.currentSession?.cdpManagerKey || h.currentManagerKey || ideType;
@@ -7432,16 +8119,29 @@ async function handleExtensionScript(h, args, scriptName) {
7432
8119
  if (typeof result === "string") {
7433
8120
  try {
7434
8121
  const parsed = JSON.parse(result);
8122
+ applyProviderPatch(h, args, parsed);
8123
+ if (parsed && typeof parsed === "object" && parsed.success === false) {
8124
+ return { success: false, ...parsed };
8125
+ }
7435
8126
  return { success: true, ...parsed };
7436
8127
  } catch {
7437
8128
  return { success: true, result };
7438
8129
  }
7439
8130
  }
8131
+ applyProviderPatch(h, args, result);
7440
8132
  return { success: true, result };
7441
8133
  } catch (e) {
7442
8134
  return { success: false, error: `Script execution failed: ${e.message}` };
7443
8135
  }
7444
8136
  }
8137
+ async function handleExtensionScript(h, args, scriptName) {
8138
+ return executeProviderScript(h, args, scriptName);
8139
+ }
8140
+ async function handleProviderScript(h, args) {
8141
+ const scriptName = typeof args?.scriptName === "string" ? args.scriptName.trim() : "";
8142
+ if (!scriptName) return { success: false, error: "scriptName is required" };
8143
+ return executeProviderScript(h, args, scriptName);
8144
+ }
7445
8145
  function handleGetIdeExtensions(h, args) {
7446
8146
  const { ideType } = args || {};
7447
8147
  const loader = h.ctx.providerLoader;
@@ -7941,6 +8641,8 @@ var DaemonCommandHandler = class {
7941
8641
  case "set_ide_extension":
7942
8642
  return handleSetIdeExtension(this, args);
7943
8643
  // ─── Extension Model / Mode Control (stream-commands.ts) ──────────
8644
+ case "invoke_provider_script":
8645
+ return handleProviderScript(this, args);
7944
8646
  case "list_extension_models":
7945
8647
  return handleExtensionScript(this, args, "listModels");
7946
8648
  case "set_extension_model":
@@ -8117,6 +8819,8 @@ var CliProviderInstance = class {
8117
8819
  generatingDebounceTimer = null;
8118
8820
  generatingDebouncePending = null;
8119
8821
  lastApprovalEventAt = 0;
8822
+ controlValues = {};
8823
+ appliedEffectKeys = /* @__PURE__ */ new Set();
8120
8824
  historyWriter;
8121
8825
  runtimeMessages = [];
8122
8826
  instanceId;
@@ -8129,6 +8833,7 @@ var CliProviderInstance = class {
8129
8833
  async init(context) {
8130
8834
  this.context = context;
8131
8835
  this.settings = context.settings || {};
8836
+ this.adapter.updateRuntimeSettings?.(this.settings);
8132
8837
  this.monitor.updateConfig({
8133
8838
  approvalAlert: this.settings.approvalAlert !== false,
8134
8839
  longGeneratingAlert: this.settings.longGeneratingAlert !== false,
@@ -8144,6 +8849,21 @@ var CliProviderInstance = class {
8144
8849
  this.detectStatusTransition();
8145
8850
  });
8146
8851
  await this.adapter.spawn();
8852
+ if (this.providerSessionId) {
8853
+ const restoredHistory = readChatHistory(this.type, 0, 200, this.providerSessionId);
8854
+ if (restoredHistory.messages.length > 0) {
8855
+ this.adapter.seedCommittedMessages(
8856
+ restoredHistory.messages.map((message) => ({
8857
+ role: message.role,
8858
+ content: message.content,
8859
+ timestamp: message.receivedAt,
8860
+ receivedAt: message.receivedAt,
8861
+ kind: message.kind,
8862
+ senderName: message.senderName
8863
+ }))
8864
+ );
8865
+ }
8866
+ }
8147
8867
  if (this.providerSessionId && this.launchMode === "resume") {
8148
8868
  const resumedAt = Date.now();
8149
8869
  this.historyWriter.appendSystemMarker(
@@ -8224,6 +8944,12 @@ var CliProviderInstance = class {
8224
8944
  }
8225
8945
  const runtime = this.adapter.getRuntimeMetadata();
8226
8946
  const parsedMessages = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages : [];
8947
+ const controlValues = extractProviderControlValues(this.provider.controls, parsedStatus);
8948
+ if (controlValues) {
8949
+ this.controlValues = controlValues;
8950
+ } else if (Object.keys(this.controlValues).length > 0) {
8951
+ this.controlValues = {};
8952
+ }
8227
8953
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
8228
8954
  const dirName = this.workingDir.split("/").filter(Boolean).pop() || "session";
8229
8955
  if (parsedMessages.length > 0) {
@@ -8244,6 +8970,7 @@ var CliProviderInstance = class {
8244
8970
  );
8245
8971
  }
8246
8972
  }
8973
+ this.applyProviderResponse(parsedStatus, { phase: "immediate" });
8247
8974
  return {
8248
8975
  type: this.type,
8249
8976
  name: this.provider.name,
@@ -8273,8 +9000,7 @@ var CliProviderInstance = class {
8273
9000
  attachedClients: runtime.attachedClients || []
8274
9001
  } : void 0,
8275
9002
  resume: this.provider.resume,
8276
- controlValues: void 0,
8277
- // CLI controls not yet wired from stream
9003
+ controlValues: this.controlValues,
8278
9004
  providerControls: this.provider.controls
8279
9005
  };
8280
9006
  }
@@ -8285,6 +9011,15 @@ var CliProviderInstance = class {
8285
9011
  getPresentationMode() {
8286
9012
  return this.presentationMode;
8287
9013
  }
9014
+ updateSettings(newSettings) {
9015
+ this.settings = { ...newSettings };
9016
+ this.adapter.updateRuntimeSettings?.(this.settings);
9017
+ this.monitor.updateConfig({
9018
+ approvalAlert: this.settings.approvalAlert !== false,
9019
+ longGeneratingAlert: this.settings.longGeneratingAlert !== false,
9020
+ longGeneratingThresholdSec: this.settings.longGeneratingThresholdSec || 180
9021
+ });
9022
+ }
8288
9023
  onEvent(event, data) {
8289
9024
  if (event === "send_message" && data?.text) {
8290
9025
  void this.adapter.sendMessage(data.text).catch((e) => {
@@ -8296,22 +9031,27 @@ var CliProviderInstance = class {
8296
9031
  void this.adapter.resolveAction(data).catch((e) => {
8297
9032
  LOG.warn("CLI", `[${this.type}] resolve_action failed: ${e?.message || e}`);
8298
9033
  });
9034
+ } else if (event === "provider_state_patch" && data && typeof data === "object") {
9035
+ this.applyProviderResponse(data, { phase: "immediate" });
8299
9036
  }
8300
9037
  }
8301
9038
  dispose() {
8302
9039
  this.adapter.shutdown();
8303
9040
  this.monitor.reset();
9041
+ this.appliedEffectKeys.clear();
8304
9042
  }
8305
9043
  completedDebounceTimer = null;
8306
9044
  completedDebouncePending = null;
8307
9045
  detectStatusTransition() {
8308
9046
  const now = Date.now();
8309
9047
  const adapterStatus = this.adapter.getStatus();
9048
+ const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
8310
9049
  const newStatus = adapterStatus.status;
8311
9050
  const dirName = this.workingDir.split("/").filter(Boolean).pop() || "session";
8312
9051
  const chatTitle = `${this.provider.name} \xB7 ${dirName}`;
8313
9052
  const partial = this.adapter.getPartialResponse();
8314
9053
  const progressFingerprint = newStatus === "generating" ? `${partial || ""}::${adapterStatus.messages.at(-1)?.content || ""}`.slice(-2e3) : void 0;
9054
+ const previousStatus = this.lastStatus;
8315
9055
  if (newStatus !== this.lastStatus) {
8316
9056
  LOG.info("CLI", `[${this.type}] status: ${this.lastStatus} \u2192 ${newStatus}`);
8317
9057
  if (this.lastStatus === "idle" && newStatus === "generating") {
@@ -8404,6 +9144,9 @@ var CliProviderInstance = class {
8404
9144
  }
8405
9145
  this.lastStatus = newStatus;
8406
9146
  }
9147
+ this.applyProviderResponse(parsedStatus, {
9148
+ phase: newStatus === "idle" && (previousStatus === "generating" || previousStatus === "waiting_approval") ? "turn_completed" : "immediate"
9149
+ });
8407
9150
  const agentKey = `${this.type}:cli`;
8408
9151
  const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
8409
9152
  for (const me of monitorEvents) {
@@ -8419,6 +9162,88 @@ var CliProviderInstance = class {
8419
9162
  this.events = [];
8420
9163
  return events;
8421
9164
  }
9165
+ applyProviderResponse(data, options) {
9166
+ if (!data || typeof data !== "object") return;
9167
+ const controlValues = extractProviderControlValues(this.provider.controls, data);
9168
+ if (controlValues) {
9169
+ this.controlValues = { ...this.controlValues, ...controlValues };
9170
+ }
9171
+ const effects = normalizeProviderEffects(data);
9172
+ for (const effect of effects) {
9173
+ const effectWhen = effect.when || "immediate";
9174
+ if (effectWhen === "turn_completed" && options.phase !== "turn_completed") continue;
9175
+ if (effectWhen === "immediate" && options.phase === "turn_completed") continue;
9176
+ const effectKey = this.getEffectDedupKey(effect);
9177
+ if (this.appliedEffectKeys.has(effectKey)) continue;
9178
+ this.appliedEffectKeys.add(effectKey);
9179
+ if (effect.persist !== false) {
9180
+ const persisted = this.getPersistedEffectContent(effect);
9181
+ if (persisted) this.appendRuntimeSystemMessage(persisted, effectKey);
9182
+ }
9183
+ if (effect.type === "message" && effect.message) {
9184
+ const content = typeof effect.message.content === "string" ? effect.message.content : JSON.stringify(effect.message.content);
9185
+ this.pushEvent({
9186
+ event: "provider:message",
9187
+ timestamp: Date.now(),
9188
+ content,
9189
+ role: effect.message.role || "system",
9190
+ kind: effect.message.kind,
9191
+ senderName: effect.message.senderName
9192
+ });
9193
+ } else if (effect.type === "toast" && effect.toast) {
9194
+ this.pushEvent({
9195
+ event: "provider:toast",
9196
+ effectId: effect.id || effectKey,
9197
+ timestamp: Date.now(),
9198
+ message: effect.toast.message,
9199
+ level: effect.toast.level || "info"
9200
+ });
9201
+ } else if (effect.type === "notification" && effect.notification) {
9202
+ this.pushEvent({
9203
+ event: "provider:notification",
9204
+ effectId: effect.id || effectKey,
9205
+ timestamp: Date.now(),
9206
+ title: effect.notification.title,
9207
+ message: effect.notification.body,
9208
+ content: typeof effect.notification.bubbleContent === "string" ? effect.notification.bubbleContent : effect.notification.body,
9209
+ level: effect.notification.level || "info",
9210
+ channels: effect.notification.channels || ["toast"],
9211
+ preferenceKey: effect.notification.preferenceKey
9212
+ });
9213
+ }
9214
+ }
9215
+ if (this.appliedEffectKeys.size > 200) {
9216
+ this.appliedEffectKeys = new Set(Array.from(this.appliedEffectKeys).slice(-100));
9217
+ }
9218
+ }
9219
+ getEffectDedupKey(effect) {
9220
+ if (effect.id) return `provider_effect:${effect.id}`;
9221
+ if (effect.type === "message") {
9222
+ const content = typeof effect.message?.content === "string" ? effect.message.content : JSON.stringify(effect.message?.content || "");
9223
+ return `provider_effect:message:${content}`;
9224
+ }
9225
+ if (effect.type === "notification") {
9226
+ return `provider_effect:notification:${effect.notification?.title || ""}:${effect.notification?.body || ""}`;
9227
+ }
9228
+ return `provider_effect:toast:${effect.toast?.message || ""}`;
9229
+ }
9230
+ getPersistedEffectContent(effect) {
9231
+ if (effect.type === "message") {
9232
+ return typeof effect.message?.content === "string" ? effect.message.content : JSON.stringify(effect.message?.content || "");
9233
+ }
9234
+ if (effect.type === "toast") {
9235
+ return effect.toast?.message || null;
9236
+ }
9237
+ if (effect.type === "notification") {
9238
+ if (typeof effect.notification?.bubbleContent === "string") return effect.notification.bubbleContent;
9239
+ if (typeof effect.notification?.title === "string" && effect.notification.title.trim()) {
9240
+ return `${effect.notification.title}
9241
+ ${effect.notification.body || ""}`.trim();
9242
+ }
9243
+ return effect.notification?.body || null;
9244
+ }
9245
+ return null;
9246
+ }
8422
9247
  // ─── Adapter access (backward compat) ──────────────────
8423
9248
  getAdapter() {
8424
9249
  return this.adapter;
@@ -11196,6 +12021,22 @@ function getMacAppIdentifiers() {
11196
12021
  function getWinProcessNames() {
11197
12022
  return getProviderLoader().getWinProcessNames();
11198
12023
  }
12024
+ function getProviderMeta(ideId) {
12025
+ return getProviderLoader().getMeta(ideId);
12026
+ }
12027
+ function getPreferredLaunchMethod(ideId, platform9) {
12028
+ const prefer = getProviderMeta(ideId)?.launch?.prefer;
12029
+ const value = prefer?.[platform9];
12030
+ return value === "cli" || value === "app" || value === "auto" ? value : "auto";
12031
+ }
12032
+ function getCdpStartupTimeoutMs(ideId) {
12033
+ const value = getProviderMeta(ideId)?.launch?.cdpStartupTimeoutMs;
12034
+ if (typeof value !== "number" || !Number.isFinite(value)) return 15e3;
12035
+ return Math.max(1e3, Math.floor(value));
12036
+ }
12037
+ function escapeForAppleScript(value) {
12038
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
12039
+ }
11199
12040
  async function findFreePort(ports) {
11200
12041
  for (const port2 of ports) {
11201
12042
  const free = await checkPortFree(port2);
@@ -11248,12 +12089,12 @@ async function killIdeProcess(ideId) {
11248
12089
  try {
11249
12090
  if (plat === "darwin" && appName) {
11250
12091
  try {
11251
- execSync4(`osascript -e 'tell application "${appName}" to quit' 2>/dev/null`, {
12092
+ execSync4(`osascript -e 'tell application "${escapeForAppleScript(appName)}" to quit' 2>/dev/null`, {
11252
12093
  timeout: 5e3
11253
12094
  });
11254
12095
  } catch {
11255
12096
  try {
11256
- execSync4(`pkill -f "${appName}" 2>/dev/null`);
12097
+ execSync4(`pkill -x "${appName}" 2>/dev/null`, { timeout: 5e3 });
11257
12098
  } catch {
11258
12099
  }
11259
12100
  }
@@ -11283,7 +12124,7 @@ async function killIdeProcess(ideId) {
11283
12124
  }
11284
12125
  if (plat === "darwin" && appName) {
11285
12126
  try {
11286
- execSync4(`pkill -9 -f "${appName}" 2>/dev/null`);
12127
+ execSync4(`pkill -9 -x "${appName}" 2>/dev/null`, { timeout: 5e3 });
11287
12128
  } catch {
11288
12129
  }
11289
12130
  } else if (plat === "win32" && winProcesses) {
@@ -11306,8 +12147,23 @@ function isIdeRunning(ideId) {
11306
12147
  if (plat === "darwin") {
11307
12148
  const appName = getMacAppIdentifiers()[ideId];
11308
12149
  if (!appName) return false;
11309
- const result = execSync4(`pgrep -f "${appName}" 2>/dev/null`, { encoding: "utf-8" });
11310
- return result.trim().length > 0;
12150
+ try {
12151
+ const result = execSync4(`pgrep -x "${appName}" 2>/dev/null`, {
12152
+ encoding: "utf-8",
12153
+ timeout: 3e3
12154
+ });
12155
+ return result.trim().length > 0;
12156
+ } catch {
12157
+ const result = execSync4(
12158
+ `osascript -e 'tell application "System Events" to count (every process whose name is "${escapeForAppleScript(appName)}")'`,
12159
+ {
12160
+ encoding: "utf-8",
12161
+ timeout: 3e3,
12162
+ stdio: ["pipe", "pipe", "pipe"]
12163
+ }
12164
+ );
12165
+ return Number.parseInt(result.trim() || "0", 10) > 0;
12166
+ }
11311
12167
  } else if (plat === "win32") {
11312
12168
  const winProcesses = getWinProcessNames()[ideId];
11313
12169
  if (!winProcesses) return false;
@@ -11456,7 +12312,8 @@ async function launchWithCdp(options = {}) {
11456
12312
  await launchLinux(targetIde, port, workspace, options.newWindow);
11457
12313
  }
11458
12314
  let cdpReady = false;
11459
- for (let i = 0; i < 30; i++) {
12315
+ const waitDeadline = Date.now() + getCdpStartupTimeoutMs(targetIde.id);
12316
+ while (Date.now() < waitDeadline) {
11460
12317
  await new Promise((r) => setTimeout(r, 500));
11461
12318
  if (await isCdpActive(port)) {
11462
12319
  cdpReady = true;
@@ -11485,14 +12342,18 @@ async function launchWithCdp(options = {}) {
11485
12342
  }
11486
12343
  async function launchMacOS(ide, port, workspace, newWindow) {
11487
12344
  const appName = getMacAppIdentifiers()[ide.id];
12345
+ const preferredMethod = getPreferredLaunchMethod(ide.id, "darwin");
11488
12346
  const args = ["--remote-debugging-port=" + port];
11489
12347
  if (newWindow) args.push("--new-window");
11490
12348
  if (workspace) args.push(workspace);
11491
- if (appName) {
12349
+ const canUseCli = !!ide.cliCommand;
12350
+ const canUseAppLauncher = !!appName;
12351
+ const useAppLauncher = preferredMethod === "app" ? canUseAppLauncher : preferredMethod === "cli" ? false : !canUseCli && canUseAppLauncher;
12352
+ if (!useAppLauncher && ide.cliCommand) {
12353
+ spawn2(ide.cliCommand, args, { detached: true, stdio: "ignore" }).unref();
12354
+ } else if (appName) {
11492
12355
  const openArgs = ["-a", appName, "--args", ...args];
11493
12356
  spawn2("open", openArgs, { detached: true, stdio: "ignore" }).unref();
11494
- } else if (ide.cliCommand) {
11495
- spawn2(ide.cliCommand, args, { detached: true, stdio: "ignore" }).unref();
11496
12357
  } else {
11497
12358
  throw new Error(`No app identifier or CLI for ${ide.displayName}`);
11498
12359
  }
@@ -11803,6 +12664,7 @@ function buildStatusSnapshot(options) {
11803
12664
  workspaces: wsState.workspaces,
11804
12665
  defaultWorkspaceId: wsState.defaultWorkspaceId,
11805
12666
  defaultWorkspacePath: wsState.defaultWorkspacePath,
12667
+ terminalSizingMode: cfg.terminalSizingMode || "measured",
11806
12668
  recentLaunches: buildRecentLaunches(recentActivity),
11807
12669
  terminalBackend,
11808
12670
  availableProviders: buildAvailableProviders(options.providerLoader)
@@ -12650,19 +13512,10 @@ var ProviderStreamAdapter = class {
12650
13512
  mode: data.mode,
12651
13513
  activeModal: data.activeModal
12652
13514
  };
12653
- if (this.provider.controls?.length) {
12654
- const cv = {};
12655
- for (const ctrl of this.provider.controls) {
12656
- if (!ctrl.readFrom) continue;
12657
- const val = data[ctrl.readFrom];
12658
- if (val !== void 0 && val !== null) {
12659
- cv[ctrl.id] = typeof val === "object" ? val.name || val.id || String(val) : val;
12660
- }
12661
- }
12662
- if (data.model && !cv["model"]) cv["model"] = data.model;
12663
- if (data.mode && !cv["mode"]) cv["mode"] = data.mode;
12664
- if (Object.keys(cv).length > 0) state.controlValues = cv;
12665
- }
13515
+ const controlValues = extractProviderControlValues(this.provider.controls, data);
13516
+ if (controlValues) state.controlValues = controlValues;
13517
+ const effects = normalizeProviderEffects(data);
13518
+ if (effects.length > 0) state.effects = effects;
12666
13519
  if (state.messages.length > 0) {
12667
13520
  this.lastSuccessState = state;
12668
13521
  }
@@ -13188,6 +14041,8 @@ function forwardAgentStreamsToIdeInstance(instanceManager, ideType, streams) {
13188
14041
  activeModal: stream.activeModal || null,
13189
14042
  model: stream.model || void 0,
13190
14043
  mode: stream.mode || void 0,
14044
+ controlValues: stream.controlValues || void 0,
14045
+ effects: stream.effects || void 0,
13191
14046
  sessionId: stream.sessionId || stream.instanceId || void 0,
13192
14047
  title: stream.title || stream.agentName || void 0,
13193
14048
  agentType: stream.agentType || void 0,