@khalilgharbaoui/opencode-claude-code-plugin 0.42.3 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1327,7 +1327,7 @@ function formatUsageLimitNote(input) {
1327
1327
  const what = [
1328
1328
  `the Claude account "${account}" is out of usage`,
1329
1329
  window ? ` in ${window}` : "",
1330
- resets ? `, which resets at ${resets}` : "",
1330
+ resets ? `, which resets at ${resets}` : input.resetsText ? `, which resets ${input.resetsText}` : "",
1331
1331
  "."
1332
1332
  ].join("");
1333
1333
  const advice = others ? `Pick a model from ${others} and resend your message, or wait for the window to reset.` : "Wait for the window to reset, or enable extra usage on the account.";
@@ -1341,6 +1341,13 @@ function isAccountLimitError(input) {
1341
1341
  if (typeof text !== "string" || text.length === 0) return false;
1342
1342
  return ACCOUNT_LIMIT_PATTERNS.some((pattern) => pattern.test(text));
1343
1343
  }
1344
+ function limitFactsFromText(text) {
1345
+ const facts = {};
1346
+ if (/\bsession limit\b/i.test(text)) facts.window = "five_hour";
1347
+ const reset = /\bresets\s+([^·\n]{1,60}?)\s*$/im.exec(text);
1348
+ if (reset) facts.resetsText = reset[1].trim();
1349
+ return facts;
1350
+ }
1344
1351
  var lastAccountLimits = /* @__PURE__ */ new Map();
1345
1352
  function rememberAccountLimit(account, facts) {
1346
1353
  const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
@@ -1423,7 +1430,6 @@ async function resolveFailoverSpawn(input) {
1423
1430
  }
1424
1431
  function isAccountFailoverQuestionActive(input) {
1425
1432
  if (input.compactionMode) return false;
1426
- if (input.interactive) return false;
1427
1433
  if (input.childSession) return false;
1428
1434
  if (input.configured !== "ask") return false;
1429
1435
  if (input.candidates.length === 0) return false;
@@ -1723,6 +1729,10 @@ function cliSupportsDontAsk(v) {
1723
1729
  if (!v) return false;
1724
1730
  return gte(v, { major: 2, minor: 1, patch: 263 });
1725
1731
  }
1732
+ function cliSupportsInteractiveBypass(v) {
1733
+ if (!v) return false;
1734
+ return gte(v, { major: 2, minor: 1, patch: 263 });
1735
+ }
1726
1736
  var INTERACTIVE_MEASURED_CLI = "2.1.288";
1727
1737
  var INTERACTIVE_MEASURED = { major: 2, minor: 1, patch: 288 };
1728
1738
  function isUnmeasuredInteractiveCli(v) {
@@ -4559,18 +4569,36 @@ function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAUL
4559
4569
  };
4560
4570
  }
4561
4571
  function interactivePermissionPosture(input) {
4562
- if (!isReadOnlyPermissionMode(input.permissionMode)) {
4563
- return { permissionMode: input.permissionMode, restricted: false, allow: input.allow };
4572
+ if (isReadOnlyPermissionMode(input.permissionMode)) {
4573
+ if (!input.supportsReadOnly) return null;
4574
+ const refused = new Set(READ_ONLY_DISALLOWED_CLI_TOOLS);
4575
+ return {
4576
+ permissionMode: "dontAsk",
4577
+ restricted: true,
4578
+ allow: input.allow.filter(
4579
+ (rule) => !rule.startsWith("mcp__") && !refused.has(rule.split("(")[0].trim())
4580
+ ),
4581
+ bypass: false,
4582
+ disallowed: []
4583
+ };
4564
4584
  }
4565
- if (!input.supportsReadOnly) return null;
4566
- const refused = new Set(READ_ONLY_DISALLOWED_CLI_TOOLS);
4567
- return {
4568
- permissionMode: "dontAsk",
4569
- restricted: true,
4570
- allow: input.allow.filter(
4571
- (rule) => !rule.startsWith("mcp__") && !refused.has(rule.split("(")[0].trim())
4572
- )
4585
+ const unchanged = {
4586
+ permissionMode: input.permissionMode === "bypassPermissions" ? void 0 : input.permissionMode,
4587
+ restricted: false,
4588
+ allow: input.allow,
4589
+ bypass: false,
4590
+ disallowed: []
4573
4591
  };
4592
+ if (input.permissionMode === "plan" || !input.supportsBypass) return unchanged;
4593
+ const perTool = Object.entries(input.controlRequestToolBehaviors ?? {});
4594
+ const named = (behavior) => perTool.filter(([, value]) => value === behavior).map(([tool]) => tool);
4595
+ if (input.skipPermissions !== false || input.permissionMode === "bypassPermissions") {
4596
+ return { ...unchanged, bypass: true };
4597
+ }
4598
+ if ((input.controlRequestBehavior ?? "allow") === "allow") {
4599
+ return { ...unchanged, bypass: true, disallowed: named("deny") };
4600
+ }
4601
+ return { ...unchanged, permissionMode: "dontAsk", allow: named("allow") };
4574
4602
  }
4575
4603
  function isReadOnlyPermissionMode(mode) {
4576
4604
  return mode === READ_ONLY_PERMISSION_MODE;
@@ -5825,7 +5853,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5825
5853
  "interactive transport denied a permission prompt",
5826
5854
  "interactive transport dropped non-text content blocks",
5827
5855
  "interactive turn failed",
5828
- "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
5856
+ "interactiveBypass is ignored: skipPermissions governs the interactive transport too",
5829
5857
  "malformed data URI, skipping file part",
5830
5858
  "no active process to respawn (start watchdog); ending turn",
5831
5859
  "no proxyTools entry was recognised; nothing will be proxied this turn",
@@ -7542,6 +7570,20 @@ function parseModelId(modelId) {
7542
7570
  if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
7543
7571
  return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
7544
7572
  }
7573
+ var WEB_SEARCH_USD = 0.01;
7574
+ function apiCallCostUsd(model, usage) {
7575
+ if (typeof model !== "string" || !usage || typeof usage !== "object") return null;
7576
+ const base = model.replace(/@.*$/, "").replace(/\[1m\]$/i, "").replace(/-\d{8}$/, "");
7577
+ const entry = (usage.speed === "fast" ? defaultModels[`${base}${FAST_SUFFIX}`] : void 0) ?? defaultModels[base];
7578
+ if (!entry) return null;
7579
+ const cost = entry.cost;
7580
+ const count = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
7581
+ const split = usage.cache_creation;
7582
+ const write1h = split ? count(split.ephemeral_1h_input_tokens) : 0;
7583
+ const write5m = split ? count(split.ephemeral_5m_input_tokens) : count(usage.cache_creation_input_tokens);
7584
+ const perMillion = count(usage.input_tokens) * cost.input + count(usage.output_tokens) * cost.output + count(usage.cache_read_input_tokens) * cost.cache.read + write5m * cost.cache.write + write1h * cost.input * 2;
7585
+ return perMillion / 1e6 + count(usage.server_tool_use?.web_search_requests) * WEB_SEARCH_USD;
7586
+ }
7545
7587
 
7546
7588
  // src/agent-models.ts
7547
7589
  var AGENT_DIR_NAMES = ["agents", "agent"];
@@ -8861,6 +8903,7 @@ function isApiCallRecord(rec) {
8861
8903
  }
8862
8904
  var TurnUsageAccumulator = class {
8863
8905
  calls = /* @__PURE__ */ new Map();
8906
+ models = /* @__PURE__ */ new Map();
8864
8907
  lastKey = null;
8865
8908
  /** Feed every transcript record of the turn, in file order. */
8866
8909
  add(rec) {
@@ -8868,8 +8911,24 @@ var TurnUsageAccumulator = class {
8868
8911
  const id = rec.message.id;
8869
8912
  const key = typeof id === "string" && id.length > 0 ? id : `uuid:${rec.uuid ?? this.calls.size}`;
8870
8913
  this.calls.set(key, rec.message.usage);
8914
+ this.models.set(key, rec.message.model);
8871
8915
  this.lastKey = key;
8872
8916
  }
8917
+ /**
8918
+ * The turn's list-price cost over its distinct calls, the headless
8919
+ * `result`'s `total_cost_usd` (h #g208). Null when no call was made or any
8920
+ * call's model is one the catalog cannot price, never a partial sum.
8921
+ */
8922
+ get costUsd() {
8923
+ if (this.calls.size === 0) return null;
8924
+ let total = 0;
8925
+ for (const [key, usage] of this.calls) {
8926
+ const cost = apiCallCostUsd(this.models.get(key), usage);
8927
+ if (cost === null) return null;
8928
+ total += cost;
8929
+ }
8930
+ return total;
8931
+ }
8873
8932
  /** Distinct API calls seen, for diagnostics. */
8874
8933
  get callCount() {
8875
8934
  return this.calls.size;
@@ -9620,11 +9679,29 @@ var ClaudeSession = class {
9620
9679
  async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
9621
9680
  const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
9622
9681
  const usage = new TurnUsageAccumulator();
9682
+ const startedAt = Date.now();
9683
+ let reportedDurationMs = null;
9684
+ let apiError = null;
9623
9685
  const { stopReason, end, denied } = await this.runTurnLoop(
9624
9686
  prompt,
9625
9687
  (raw, rec) => {
9626
9688
  onLine(raw);
9627
9689
  if (rec) usage.add(rec);
9690
+ if (rec?.type === "assistant") {
9691
+ if (rec.isApiErrorMessage === true) {
9692
+ const text = (Array.isArray(rec.message?.content) ? rec.message.content : []).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
9693
+ apiError = {
9694
+ text,
9695
+ ...typeof rec.apiErrorStatus === "number" ? { status: rec.apiErrorStatus } : {},
9696
+ ...typeof rec.error === "string" ? { kind: rec.error } : {}
9697
+ };
9698
+ } else if (rec.message?.model !== "<synthetic>") {
9699
+ apiError = null;
9700
+ }
9701
+ }
9702
+ if (isTurnDurationRecord(rec) && typeof rec.durationMs === "number") {
9703
+ reportedDurationMs = rec.durationMs;
9704
+ }
9628
9705
  },
9629
9706
  timeout,
9630
9707
  onHeartbeat
@@ -9642,6 +9719,9 @@ var ClaudeSession = class {
9642
9719
  usage: usage.turnTotal,
9643
9720
  lastCallUsage: usage.lastCall,
9644
9721
  callCount: usage.callCount,
9722
+ costUsd: usage.costUsd,
9723
+ durationMs: reportedDurationMs ?? Date.now() - startedAt,
9724
+ apiError,
9645
9725
  denied
9646
9726
  };
9647
9727
  }
@@ -9789,13 +9869,19 @@ function interactiveExtraArgs(opts) {
9789
9869
  if (opts.fastMode) {
9790
9870
  flagSettings.fastMode = true;
9791
9871
  }
9872
+ if (opts.bypass) {
9873
+ flagSettings.skipDangerousModePermissionPrompt = true;
9874
+ extraArgs.push("--dangerously-skip-permissions");
9875
+ }
9792
9876
  if (Object.keys(flagSettings).length > 0) {
9793
9877
  extraArgs.push("--settings", JSON.stringify(flagSettings));
9794
9878
  }
9795
9879
  if (opts.permissionMode === "bypassPermissions") {
9796
- log.warn(
9797
- "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
9798
- );
9880
+ if (!opts.bypass) {
9881
+ log.warn(
9882
+ "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
9883
+ );
9884
+ }
9799
9885
  } else if (opts.permissionMode) {
9800
9886
  extraArgs.push("--permission-mode", opts.permissionMode);
9801
9887
  }
@@ -9847,6 +9933,7 @@ function heartbeatFrame(sessionId, openCall, now = Date.now()) {
9847
9933
  }
9848
9934
  function interactiveResultFrame(opts) {
9849
9935
  const completed = opts.end === "stop" && !!opts.stopReason;
9936
+ const apiError = completed && opts.apiError ? opts.apiError : null;
9850
9937
  let result;
9851
9938
  let terminalReason = "completed";
9852
9939
  if (!completed) {
@@ -9864,20 +9951,35 @@ function interactiveResultFrame(opts) {
9864
9951
  return JSON.stringify({
9865
9952
  type: "result",
9866
9953
  subtype: completed ? "success" : "error_during_execution",
9867
- is_error: !completed,
9954
+ is_error: !completed || apiError !== null,
9868
9955
  stop_reason: completed ? opts.stopReason : null,
9869
9956
  terminal_reason: terminalReason,
9870
- result,
9957
+ result: apiError ? apiError.text : result,
9958
+ ...apiError?.status !== void 0 ? { api_error_status: apiError.status } : {},
9871
9959
  session_id: opts.sessionId,
9872
9960
  usage: opts.usage ?? {},
9873
9961
  // The TUI has no `can_use_tool` channel, so a permission dialog is the
9874
9962
  // only way it asks; each one the session denied is reported in the
9875
9963
  // headless field's place, names and ids only (h #g109).
9876
9964
  permission_denials: opts.denials ?? [],
9877
- total_cost_usd: null,
9878
- duration_ms: 0
9965
+ total_cost_usd: opts.costUsd ?? null,
9966
+ duration_ms: opts.durationMs ?? 0
9879
9967
  });
9880
9968
  }
9969
+ function interactiveSpawnEnv2(env) {
9970
+ if (env.CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS !== void 0) return env;
9971
+ return { ...env, CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS: "0" };
9972
+ }
9973
+ function streamFrameFromRecord(raw) {
9974
+ if (!raw.includes('"isApiErrorMessage":true')) return raw;
9975
+ try {
9976
+ const rec = JSON.parse(raw);
9977
+ if (rec?.isApiErrorMessage !== true || rec.is_api_error_message !== void 0) return raw;
9978
+ return JSON.stringify({ ...rec, is_api_error_message: true });
9979
+ } catch {
9980
+ return raw;
9981
+ }
9982
+ }
9881
9983
  function carriesReplyText(line) {
9882
9984
  try {
9883
9985
  const rec = JSON.parse(line);
@@ -9915,6 +10017,7 @@ function spawnInteractiveProcess(opts) {
9915
10017
  ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
9916
10018
  effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
9917
10019
  resumeSessionId: opts.resumeSessionId,
10020
+ ...opts.forkOf ? { forkOf: opts.forkOf } : {},
9918
10021
  env: opts.env,
9919
10022
  spawnPty: opts.spawnPty,
9920
10023
  onScreen: (event) => {
@@ -9946,6 +10049,7 @@ function spawnInteractiveProcess(opts) {
9946
10049
  effort: opts.effort,
9947
10050
  sessionId: session.sessionId,
9948
10051
  resumed: !!opts.resumeSessionId,
10052
+ forkedFrom: opts.forkOf ?? null,
9949
10053
  jsonlPath: session.jsonlPath
9950
10054
  });
9951
10055
  let startPromise = null;
@@ -9975,14 +10079,14 @@ function spawnInteractiveProcess(opts) {
9975
10079
  }
9976
10080
  openCalls.clear();
9977
10081
  denials = [];
9978
- const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
10082
+ const { stopReason, end, usage, lastCallUsage, callCount, costUsd, durationMs, apiError, denied } = await session.tailTurn(
9979
10083
  userMsg,
9980
10084
  (raw) => {
9981
10085
  try {
9982
10086
  openCalls.add(JSON.parse(raw));
9983
10087
  } catch {
9984
10088
  }
9985
- emit2(raw);
10089
+ emit2(streamFrameFromRecord(raw));
9986
10090
  },
9987
10091
  void 0,
9988
10092
  // Only to a listening turn: it feeds that turn's watchdog, and the
@@ -10013,7 +10117,10 @@ function spawnInteractiveProcess(opts) {
10013
10117
  end,
10014
10118
  stopReason,
10015
10119
  usage,
10016
- denials
10120
+ denials,
10121
+ costUsd,
10122
+ durationMs,
10123
+ apiError
10017
10124
  })
10018
10125
  );
10019
10126
  } catch (err) {
@@ -10057,7 +10164,9 @@ function spawnInteractiveProcess(opts) {
10057
10164
  configDir: opts.configDir,
10058
10165
  model: opts.model,
10059
10166
  settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
10060
- extraArgs: interactiveExtraArgs({ ...opts, permissionsAllow: [], permissionMode: "dontAsk" }),
10167
+ // Tool-free whatever the main spawn's posture: `dontAsk` with nothing
10168
+ // allowed, and never the skip flag, which would win over it (h #g210).
10169
+ extraArgs: interactiveExtraArgs({ ...opts, permissionsAllow: [], permissionMode: "dontAsk", bypass: false }),
10061
10170
  ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
10062
10171
  effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
10063
10172
  env: opts.env,
@@ -11162,6 +11271,7 @@ function createTurnState(init) {
11162
11271
  lastCallUsage: void 0,
11163
11272
  resultFailure: void 0,
11164
11273
  accountLimitHit: null,
11274
+ apiErrorTextShown: null,
11165
11275
  usageLimitNote: false,
11166
11276
  accountBlock: null,
11167
11277
  modelRefusal: null,
@@ -12253,11 +12363,21 @@ ${plan}
12253
12363
  state.hasReceivedContent = true;
12254
12364
  }
12255
12365
  const apiErrorReply = msg.is_api_error_message === true;
12256
- const limitErrorReply = apiErrorReply && ctx.usageLimitNoteActive && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
12366
+ const limitFromReply = apiErrorReply && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
12257
12367
  resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
12258
12368
  }));
12259
- if (limitErrorReply) {
12369
+ const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
12370
+ if (limitFromReply) {
12260
12371
  state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
12372
+ const said = limitFactsFromText(
12373
+ msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
12374
+ );
12375
+ const known = state.accountLimitHit;
12376
+ state.accountLimitHit = {
12377
+ ...known,
12378
+ window: known.window ?? said.window,
12379
+ ...known.resetsAt === void 0 && said.resetsText ? { resetsText: said.resetsText } : {}
12380
+ };
12261
12381
  }
12262
12382
  if (hasText && !hasToolUse) {
12263
12383
  startResultFallback(state);
@@ -12268,6 +12388,7 @@ ${plan}
12268
12388
  for (const block2 of msg.message.content) {
12269
12389
  if (block2.type === "text" && block2.text) {
12270
12390
  if (limitErrorReply) continue;
12391
+ if (apiErrorReply) state.apiErrorTextShown = block2.text;
12271
12392
  resetLastVisibleTextBlock(state);
12272
12393
  const blockId = state.startTextBlock();
12273
12394
  state.controller.enqueue({
@@ -12492,7 +12613,8 @@ ${plan}
12492
12613
  state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
12493
12614
  // and says what to run; two notes about one failure is one too many.
12494
12615
  !state.accountBlock && msg.is_error === true;
12495
- if (!state.currentTextId && msg.is_error && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
12616
+ if (!state.currentTextId && msg.is_error && // Already on screen from the reply frame, word for word.
12617
+ !(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
12496
12618
  const errId = state.startTextBlock();
12497
12619
  state.controller.enqueue({
12498
12620
  type: "text-delta",
@@ -13067,12 +13189,12 @@ var ClaudeCodeLanguageModel = class {
13067
13189
  if (turnAbort.aborted) return abortedBeforeWork("selecting the transport");
13068
13190
  const useInteractive = selectedTransport === "interactive";
13069
13191
  const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
13070
- const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
13192
+ const modelFallbackArmed = attempt?.armed === true && !compactionMode;
13071
13193
  const sourceAccount = normalizeAccountName(
13072
13194
  this.config.account ?? DEFAULT_ACCOUNT
13073
13195
  );
13074
13196
  const baseCliPath = this.config.baseCliPath ?? this.config.cliPath;
13075
- let failover = useInteractive || compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
13197
+ let failover = compactionMode ? { cliPath: this.config.cliPath, modelId: effectiveModelId, failedOver: false } : await resolveFailoverSpawn({
13076
13198
  account: sourceAccount,
13077
13199
  baseCliPath,
13078
13200
  cliPath: this.config.cliPath,
@@ -13225,7 +13347,7 @@ var ClaudeCodeLanguageModel = class {
13225
13347
  deleteActiveProcess(sk);
13226
13348
  clearCompression(sk);
13227
13349
  }
13228
- const failoverAnswer = compactionMode || useInteractive || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
13350
+ const failoverAnswer = compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
13229
13351
  sourceAccount,
13230
13352
  candidates: failoverCandidates(this.config.failoverAccounts, sourceAccount)
13231
13353
  });
@@ -13288,7 +13410,7 @@ var ClaudeCodeLanguageModel = class {
13288
13410
  });
13289
13411
  }
13290
13412
  const processForAccount = getActiveProcess(sk);
13291
- if (!compactionMode && !useInteractive && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
13413
+ if (!compactionMode && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
13292
13414
  log.notice("claude process belongs to another account; starting fresh", {
13293
13415
  sessionKey: sk,
13294
13416
  was: processForAccount.cliPath,
@@ -13305,7 +13427,7 @@ var ClaudeCodeLanguageModel = class {
13305
13427
  includeHistoryContext = true;
13306
13428
  }
13307
13429
  let forkFromClaudeSessionId;
13308
- if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && !useInteractive && failoverAnswer?.kind !== "switch") {
13430
+ if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && failoverAnswer?.kind !== "switch") {
13309
13431
  const parent = findForkParent({
13310
13432
  sessionKey: sk,
13311
13433
  prompt: options.prompt,
@@ -13361,7 +13483,7 @@ var ClaudeCodeLanguageModel = class {
13361
13483
  });
13362
13484
  }
13363
13485
  }
13364
- if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
13486
+ if (this.config.forkSessions === true && !compactionMode) {
13365
13487
  recordForkFingerprint(sk, options.prompt, cliPath);
13366
13488
  }
13367
13489
  const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
@@ -13417,6 +13539,7 @@ var ClaudeCodeLanguageModel = class {
13417
13539
  if (useInteractive) reportUnmeasuredInteractiveCli(cliVersion);
13418
13540
  const ptyReadOnly = useInteractive && !compactionMode && !doctor && isReadOnlyPermissionMode(this.config.permissionMode);
13419
13541
  const ptyReadOnlySupported = cliSupportsRestricted(cliVersion) && cliSupportsDontAsk(cliVersion);
13542
+ const ptyBypassSupported = cliSupportsInteractiveBypass(cliVersion);
13420
13543
  if (ptyReadOnly && !ptyReadOnlySupported) {
13421
13544
  throw new Error(
13422
13545
  "The read-only preset on the interactive transport needs Claude Code 2.1.263 or newer (--restricted and --permission-mode dontAsk). Update claude, or use the headless transport."
@@ -13426,14 +13549,13 @@ var ClaudeCodeLanguageModel = class {
13426
13549
  this.config.failoverAccounts,
13427
13550
  sourceAccount
13428
13551
  );
13429
- const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && !useInteractive && // A `doGenerate` caller takes the override and reports the plain
13552
+ const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && // A `doGenerate` caller takes the override and reports the plain
13430
13553
  // rate-limit error, never the form. See `TurnMode`.
13431
13554
  mode !== "generate" && isAccountFailoverQuestionActive({
13432
13555
  configured: this.config.accountFailover,
13433
13556
  candidates: failoverAccounts,
13434
13557
  opencodeHasQuestion: (await loadLiveToolInfo()).hasQuestion,
13435
13558
  compactionMode,
13436
- interactive: !!useInteractive,
13437
13559
  // A subagent follows its parent's account for free, because the
13438
13560
  // override is account-scoped. Asking it would put a form in a session
13439
13561
  // the operator is usually not even looking at.
@@ -13815,7 +13937,7 @@ var ClaudeCodeLanguageModel = class {
13815
13937
  }
13816
13938
  if (interactiveBypassRequested) {
13817
13939
  log.warn(
13818
- "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
13940
+ "interactiveBypass is ignored: skipPermissions governs the interactive transport too"
13819
13941
  );
13820
13942
  }
13821
13943
  const skillPluginDirs = await resolveSkillPluginDirs({
@@ -13829,13 +13951,19 @@ var ClaudeCodeLanguageModel = class {
13829
13951
  const posture = interactivePermissionPosture({
13830
13952
  permissionMode: self.config.permissionMode,
13831
13953
  allow,
13832
- supportsReadOnly: ptyReadOnlySupported
13954
+ supportsReadOnly: ptyReadOnlySupported,
13955
+ skipPermissions: self.config.skipPermissions,
13956
+ controlRequestBehavior: self.config.controlRequestBehavior,
13957
+ controlRequestToolBehaviors: self.config.controlRequestToolBehaviors,
13958
+ supportsBypass: ptyBypassSupported
13833
13959
  });
13834
13960
  if (!posture) throw new Error("the interactive transport cannot hold this permission posture");
13835
13961
  const ap = spawnInteractiveProcess({
13836
13962
  cwd,
13837
13963
  cliPath,
13838
- configDir: self.config.configDir,
13964
+ // On an account switch, the other account's directory: the
13965
+ // TUI writes its transcript there (h #g209).
13966
+ configDir: self.skillBridgeSpawn(failover).configDir,
13839
13967
  model: spawnModelId,
13840
13968
  fastMode,
13841
13969
  mcpConfigPaths: mcp.paths,
@@ -13843,19 +13971,26 @@ var ClaudeCodeLanguageModel = class {
13843
13971
  permissionsAllow: posture.allow,
13844
13972
  permissionMode: posture.permissionMode,
13845
13973
  restricted: posture.restricted,
13846
- disallowedTools: wiring.allDisallowed,
13974
+ bypass: posture.bypass,
13975
+ disallowedTools: [...wiring.allDisallowed, ...posture.disallowed],
13847
13976
  proxyServer: state.proxyServer,
13848
13977
  systemPromptFile,
13849
13978
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13850
13979
  effort: reasoningEffort,
13851
13980
  resumeSessionId,
13981
+ // A fork of a conversation already served (`forkSessions`):
13982
+ // the TUI branches it instead of the history being replayed.
13983
+ ...forkFromClaudeSessionId && !resumeSessionId ? { forkOf: forkFromClaudeSessionId } : {},
13852
13984
  // The headless spawn's env, so hygiene, effort and the
13853
- // agent's prompt cache TTL reach both transports alike.
13854
- env: claudeSpawnEnv({
13855
- ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13856
- effort: reasoningEffort,
13857
- promptCacheTtl
13858
- })
13985
+ // agent's prompt cache TTL reach both transports alike, plus
13986
+ // what keeps a proxied call in the foreground (h #g210).
13987
+ env: interactiveSpawnEnv2(
13988
+ claudeSpawnEnv({
13989
+ ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13990
+ effort: reasoningEffort,
13991
+ promptCacheTtl
13992
+ })
13993
+ )
13859
13994
  });
13860
13995
  ap.proc.once(
13861
13996
  "exit",
@@ -13863,6 +13998,7 @@ var ClaudeCodeLanguageModel = class {
13863
13998
  );
13864
13999
  ap.mcpHash = mcp.bridgedHash;
13865
14000
  ap.mcpServers = mcp.allEnabledServerNames;
14001
+ ap.cliPath = cliPath;
13866
14002
  setActiveProcess(sk, ap);
13867
14003
  state.proc = ap.proc;
13868
14004
  state.lineEmitter = ap.lineEmitter;
@@ -13870,6 +14006,7 @@ var ClaudeCodeLanguageModel = class {
13870
14006
  log.info("spawned interactive claude session", {
13871
14007
  sk,
13872
14008
  resumed: !!resumeSessionId,
14009
+ forked: !!forkFromClaudeSessionId && !resumeSessionId,
13873
14010
  cliPath,
13874
14011
  configDir: self.config.configDir,
13875
14012
  model: effectiveModelId
@@ -14084,7 +14221,8 @@ var ClaudeCodeLanguageModel = class {
14084
14221
  sourceAccount,
14085
14222
  candidates: failoverAccounts,
14086
14223
  resetsAt: state.accountLimitHit?.resetsAt,
14087
- window: state.accountLimitHit?.window
14224
+ window: state.accountLimitHit?.window,
14225
+ resetsText: state.accountLimitHit?.resetsText
14088
14226
  })
14089
14227
  });
14090
14228
  state.endTextBlock();