@khalilgharbaoui/opencode-claude-code-plugin 0.42.2 → 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",
@@ -5862,6 +5890,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5862
5890
  "stale scratch directory sweep failed",
5863
5891
  "task_batch resolving with child results missing",
5864
5892
  "the interactive transport has not been measured on this Claude Code version; it reads the TUI's screen and transcript, which a new release can change. If a turn hangs or a dialog is not handled, run /claude-code-doctor bundle and report it",
5893
+ "the interactive turn did not take a forwarded user message",
5865
5894
  "this opencode process is running an older plugin build than the one on disk",
5866
5895
  "timed out pending proxy call",
5867
5896
  "turn abort listener threw",
@@ -7541,6 +7570,20 @@ function parseModelId(modelId) {
7541
7570
  if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
7542
7571
  return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
7543
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
+ }
7544
7587
 
7545
7588
  // src/agent-models.ts
7546
7589
  var AGENT_DIR_NAMES = ["agents", "agent"];
@@ -8860,6 +8903,7 @@ function isApiCallRecord(rec) {
8860
8903
  }
8861
8904
  var TurnUsageAccumulator = class {
8862
8905
  calls = /* @__PURE__ */ new Map();
8906
+ models = /* @__PURE__ */ new Map();
8863
8907
  lastKey = null;
8864
8908
  /** Feed every transcript record of the turn, in file order. */
8865
8909
  add(rec) {
@@ -8867,8 +8911,24 @@ var TurnUsageAccumulator = class {
8867
8911
  const id = rec.message.id;
8868
8912
  const key = typeof id === "string" && id.length > 0 ? id : `uuid:${rec.uuid ?? this.calls.size}`;
8869
8913
  this.calls.set(key, rec.message.usage);
8914
+ this.models.set(key, rec.message.model);
8870
8915
  this.lastKey = key;
8871
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
+ }
8872
8932
  /** Distinct API calls seen, for diagnostics. */
8873
8933
  get callCount() {
8874
8934
  return this.calls.size;
@@ -8999,6 +9059,9 @@ var ClaudeSession = class {
8999
9059
  * (the TUI repaints it while it takes the key, measured on 2.1.288) can
9000
9060
  * never stand in for the next call's dialog. */
9001
9061
  planDialog = null;
9062
+ /** `queue-operation` enqueue records read, which is how `queueInput` knows
9063
+ * the TUI took a message into the running turn. */
9064
+ enqueued = 0;
9002
9065
  /** This turn's `ExitPlanMode` calls already answered. */
9003
9066
  answeredPlans = /* @__PURE__ */ new Set();
9004
9067
  abandonTurn = false;
@@ -9057,7 +9120,9 @@ var ClaudeSession = class {
9057
9120
  // The decision arrives after opencode's own question round trip, by
9058
9121
  // which time the dialog has long been drawn; this only bounds a TUI
9059
9122
  // that never draws it.
9060
- planDialogWaitMs: opts.planDialogWaitMs ?? 3e4
9123
+ planDialogWaitMs: opts.planDialogWaitMs ?? 3e4,
9124
+ // 100 times the longest silence measured while Claude works (h #g207).
9125
+ idleEndMs: opts.idleEndMs ?? 2e4
9061
9126
  };
9062
9127
  }
9063
9128
  /** The CLI arguments this session spawns with. */
@@ -9335,7 +9400,8 @@ var ClaudeSession = class {
9335
9400
  end: null,
9336
9401
  sawUserRecord: false,
9337
9402
  lastBeat: Date.now(),
9338
- awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null
9403
+ awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null,
9404
+ sawReply: false
9339
9405
  };
9340
9406
  this.forkPending = false;
9341
9407
  this.scan = scan;
@@ -9358,6 +9424,9 @@ var ClaudeSession = class {
9358
9424
  if (scan.stopReason && Date.now() - scan.stopSeenAt >= this.o.stopSettleMs) {
9359
9425
  return { stopReason: scan.stopReason, end: "stop", denied: this.turnDenied };
9360
9426
  }
9427
+ if (scan.sawReply && !this.planApprovalToolUseId && Date.now() - this.lastDataAt >= this.o.idleEndMs && Date.now() - scan.stopSeenAt >= this.o.idleEndMs) {
9428
+ return { stopReason: null, end: "ended", denied: this.turnDenied };
9429
+ }
9361
9430
  if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
9362
9431
  if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
9363
9432
  const now = Date.now();
@@ -9407,9 +9476,11 @@ var ClaudeSession = class {
9407
9476
  scan.awaitingOwnPrompt = null;
9408
9477
  }
9409
9478
  if (rec) this.notePlanApproval(rec);
9479
+ if (rec?.type === "queue-operation" && rec.operation === "enqueue") this.enqueued++;
9410
9480
  scan.onRecord(s, rec);
9411
9481
  if (rec) {
9412
9482
  if (rec.type === "assistant" && rec.message) {
9483
+ scan.sawReply = true;
9413
9484
  const reason = rec.message.stop_reason;
9414
9485
  if (typeof reason === "string") {
9415
9486
  scan.stopReason = TERMINAL_STOP.has(reason) ? reason : null;
@@ -9498,6 +9569,29 @@ var ClaudeSession = class {
9498
9569
  this.write("\r");
9499
9570
  return true;
9500
9571
  }
9572
+ /**
9573
+ * Type a message into the running turn's own input queue, not as a turn of
9574
+ * its own (h #g206). Measured on 2.1.288: text pasted and submitted while a
9575
+ * tool call runs is recorded as `queue-operation` enqueue and remove plus a
9576
+ * `queued_command` attachment, answered in the same turn, which still ends
9577
+ * once. Resolves true once the enqueue is in the transcript; false with no
9578
+ * turn running, or with a plan approval dialog up, which would take the
9579
+ * keys as its answer.
9580
+ */
9581
+ async queueInput(text, timeoutMs = 3e3) {
9582
+ if (!this.turn || !this.proc || this.exited || this.pendingPlanApproval) return false;
9583
+ const before = this.enqueued;
9584
+ this.write(this.o.bracketedPaste ? "\x1B[200~" + text + "\x1B[201~" : text);
9585
+ await delay(this.o.submitMinMs);
9586
+ this.write("\r");
9587
+ const deadline = Date.now() + timeoutMs;
9588
+ while (this.enqueued === before) {
9589
+ if (!this.turn || this.exited || Date.now() >= deadline) return false;
9590
+ await delay(this.o.pollMs);
9591
+ this.scanTranscript();
9592
+ }
9593
+ return true;
9594
+ }
9501
9595
  /** Wait until the PTY has been quiet for `permissionQuietMs`, at most 3 s. */
9502
9596
  async waitForQuiet() {
9503
9597
  const until = Date.now() + 3e3;
@@ -9585,11 +9679,29 @@ var ClaudeSession = class {
9585
9679
  async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
9586
9680
  const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
9587
9681
  const usage = new TurnUsageAccumulator();
9682
+ const startedAt = Date.now();
9683
+ let reportedDurationMs = null;
9684
+ let apiError = null;
9588
9685
  const { stopReason, end, denied } = await this.runTurnLoop(
9589
9686
  prompt,
9590
9687
  (raw, rec) => {
9591
9688
  onLine(raw);
9592
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
+ }
9593
9705
  },
9594
9706
  timeout,
9595
9707
  onHeartbeat
@@ -9607,6 +9719,9 @@ var ClaudeSession = class {
9607
9719
  usage: usage.turnTotal,
9608
9720
  lastCallUsage: usage.lastCall,
9609
9721
  callCount: usage.callCount,
9722
+ costUsd: usage.costUsd,
9723
+ durationMs: reportedDurationMs ?? Date.now() - startedAt,
9724
+ apiError,
9610
9725
  denied
9611
9726
  };
9612
9727
  }
@@ -9754,13 +9869,19 @@ function interactiveExtraArgs(opts) {
9754
9869
  if (opts.fastMode) {
9755
9870
  flagSettings.fastMode = true;
9756
9871
  }
9872
+ if (opts.bypass) {
9873
+ flagSettings.skipDangerousModePermissionPrompt = true;
9874
+ extraArgs.push("--dangerously-skip-permissions");
9875
+ }
9757
9876
  if (Object.keys(flagSettings).length > 0) {
9758
9877
  extraArgs.push("--settings", JSON.stringify(flagSettings));
9759
9878
  }
9760
9879
  if (opts.permissionMode === "bypassPermissions") {
9761
- log.warn(
9762
- "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
9763
- );
9880
+ if (!opts.bypass) {
9881
+ log.warn(
9882
+ "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI"
9883
+ );
9884
+ }
9764
9885
  } else if (opts.permissionMode) {
9765
9886
  extraArgs.push("--permission-mode", opts.permissionMode);
9766
9887
  }
@@ -9812,6 +9933,7 @@ function heartbeatFrame(sessionId, openCall, now = Date.now()) {
9812
9933
  }
9813
9934
  function interactiveResultFrame(opts) {
9814
9935
  const completed = opts.end === "stop" && !!opts.stopReason;
9936
+ const apiError = completed && opts.apiError ? opts.apiError : null;
9815
9937
  let result;
9816
9938
  let terminalReason = "completed";
9817
9939
  if (!completed) {
@@ -9829,20 +9951,35 @@ function interactiveResultFrame(opts) {
9829
9951
  return JSON.stringify({
9830
9952
  type: "result",
9831
9953
  subtype: completed ? "success" : "error_during_execution",
9832
- is_error: !completed,
9954
+ is_error: !completed || apiError !== null,
9833
9955
  stop_reason: completed ? opts.stopReason : null,
9834
9956
  terminal_reason: terminalReason,
9835
- result,
9957
+ result: apiError ? apiError.text : result,
9958
+ ...apiError?.status !== void 0 ? { api_error_status: apiError.status } : {},
9836
9959
  session_id: opts.sessionId,
9837
9960
  usage: opts.usage ?? {},
9838
9961
  // The TUI has no `can_use_tool` channel, so a permission dialog is the
9839
9962
  // only way it asks; each one the session denied is reported in the
9840
9963
  // headless field's place, names and ids only (h #g109).
9841
9964
  permission_denials: opts.denials ?? [],
9842
- total_cost_usd: null,
9843
- duration_ms: 0
9965
+ total_cost_usd: opts.costUsd ?? null,
9966
+ duration_ms: opts.durationMs ?? 0
9844
9967
  });
9845
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
+ }
9846
9983
  function carriesReplyText(line) {
9847
9984
  try {
9848
9985
  const rec = JSON.parse(line);
@@ -9880,6 +10017,7 @@ function spawnInteractiveProcess(opts) {
9880
10017
  ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
9881
10018
  effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
9882
10019
  resumeSessionId: opts.resumeSessionId,
10020
+ ...opts.forkOf ? { forkOf: opts.forkOf } : {},
9883
10021
  env: opts.env,
9884
10022
  spawnPty: opts.spawnPty,
9885
10023
  onScreen: (event) => {
@@ -9911,6 +10049,7 @@ function spawnInteractiveProcess(opts) {
9911
10049
  effort: opts.effort,
9912
10050
  sessionId: session.sessionId,
9913
10051
  resumed: !!opts.resumeSessionId,
10052
+ forkedFrom: opts.forkOf ?? null,
9914
10053
  jsonlPath: session.jsonlPath
9915
10054
  });
9916
10055
  let startPromise = null;
@@ -9940,14 +10079,14 @@ function spawnInteractiveProcess(opts) {
9940
10079
  }
9941
10080
  openCalls.clear();
9942
10081
  denials = [];
9943
- const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
10082
+ const { stopReason, end, usage, lastCallUsage, callCount, costUsd, durationMs, apiError, denied } = await session.tailTurn(
9944
10083
  userMsg,
9945
10084
  (raw) => {
9946
10085
  try {
9947
10086
  openCalls.add(JSON.parse(raw));
9948
10087
  } catch {
9949
10088
  }
9950
- emit2(raw);
10089
+ emit2(streamFrameFromRecord(raw));
9951
10090
  },
9952
10091
  void 0,
9953
10092
  // Only to a listening turn: it feeds that turn's watchdog, and the
@@ -9978,7 +10117,10 @@ function spawnInteractiveProcess(opts) {
9978
10117
  end,
9979
10118
  stopReason,
9980
10119
  usage,
9981
- denials
10120
+ denials,
10121
+ costUsd,
10122
+ durationMs,
10123
+ apiError
9982
10124
  })
9983
10125
  );
9984
10126
  } catch (err) {
@@ -10022,7 +10164,9 @@ function spawnInteractiveProcess(opts) {
10022
10164
  configDir: opts.configDir,
10023
10165
  model: opts.model,
10024
10166
  settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
10025
- 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 }),
10026
10170
  ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
10027
10171
  effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
10028
10172
  env: opts.env,
@@ -10123,6 +10267,13 @@ function spawnInteractiveProcess(opts) {
10123
10267
  },
10124
10268
  flushTranscript: () => session.flushTranscript(),
10125
10269
  planApprovalPending: () => session.pendingPlanApproval !== null,
10270
+ // Text only: an image in a mid-call message is dropped with the
10271
+ // decoder's own warning rather than staged for a turn that is not ours.
10272
+ queueInput: async (content) => {
10273
+ const text = decodeUserEnvelope(JSON.stringify({ type: "user", message: { role: "user", content } })).trim();
10274
+ if (!text) return false;
10275
+ return session.queueInput(text);
10276
+ },
10126
10277
  askAside
10127
10278
  }
10128
10279
  };
@@ -11120,6 +11271,7 @@ function createTurnState(init) {
11120
11271
  lastCallUsage: void 0,
11121
11272
  resultFailure: void 0,
11122
11273
  accountLimitHit: null,
11274
+ apiErrorTextShown: null,
11123
11275
  usageLimitNote: false,
11124
11276
  accountBlock: null,
11125
11277
  modelRefusal: null,
@@ -12211,11 +12363,21 @@ ${plan}
12211
12363
  state.hasReceivedContent = true;
12212
12364
  }
12213
12365
  const apiErrorReply = msg.is_api_error_message === true;
12214
- const limitErrorReply = apiErrorReply && ctx.usageLimitNoteActive && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
12366
+ const limitFromReply = apiErrorReply && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
12215
12367
  resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
12216
12368
  }));
12217
- if (limitErrorReply) {
12369
+ const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
12370
+ if (limitFromReply) {
12218
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
+ };
12219
12381
  }
12220
12382
  if (hasText && !hasToolUse) {
12221
12383
  startResultFallback(state);
@@ -12226,6 +12388,7 @@ ${plan}
12226
12388
  for (const block2 of msg.message.content) {
12227
12389
  if (block2.type === "text" && block2.text) {
12228
12390
  if (limitErrorReply) continue;
12391
+ if (apiErrorReply) state.apiErrorTextShown = block2.text;
12229
12392
  resetLastVisibleTextBlock(state);
12230
12393
  const blockId = state.startTextBlock();
12231
12394
  state.controller.enqueue({
@@ -12450,7 +12613,8 @@ ${plan}
12450
12613
  state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
12451
12614
  // and says what to run; two notes about one failure is one too many.
12452
12615
  !state.accountBlock && msg.is_error === true;
12453
- 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) {
12454
12618
  const errId = state.startTextBlock();
12455
12619
  state.controller.enqueue({
12456
12620
  type: "text-delta",
@@ -13025,12 +13189,12 @@ var ClaudeCodeLanguageModel = class {
13025
13189
  if (turnAbort.aborted) return abortedBeforeWork("selecting the transport");
13026
13190
  const useInteractive = selectedTransport === "interactive";
13027
13191
  const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
13028
- const modelFallbackArmed = attempt?.armed === true && !compactionMode && !useInteractive;
13192
+ const modelFallbackArmed = attempt?.armed === true && !compactionMode;
13029
13193
  const sourceAccount = normalizeAccountName(
13030
13194
  this.config.account ?? DEFAULT_ACCOUNT
13031
13195
  );
13032
13196
  const baseCliPath = this.config.baseCliPath ?? this.config.cliPath;
13033
- 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({
13034
13198
  account: sourceAccount,
13035
13199
  baseCliPath,
13036
13200
  cliPath: this.config.cliPath,
@@ -13183,7 +13347,7 @@ var ClaudeCodeLanguageModel = class {
13183
13347
  deleteActiveProcess(sk);
13184
13348
  clearCompression(sk);
13185
13349
  }
13186
- const failoverAnswer = compactionMode || useInteractive || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
13350
+ const failoverAnswer = compactionMode || mode === "generate" ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
13187
13351
  sourceAccount,
13188
13352
  candidates: failoverCandidates(this.config.failoverAccounts, sourceAccount)
13189
13353
  });
@@ -13246,7 +13410,7 @@ var ClaudeCodeLanguageModel = class {
13246
13410
  });
13247
13411
  }
13248
13412
  const processForAccount = getActiveProcess(sk);
13249
- if (!compactionMode && !useInteractive && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
13413
+ if (!compactionMode && processForAccount?.cliPath && processForAccount.cliPath !== cliPath) {
13250
13414
  log.notice("claude process belongs to another account; starting fresh", {
13251
13415
  sessionKey: sk,
13252
13416
  was: processForAccount.cliPath,
@@ -13263,7 +13427,7 @@ var ClaudeCodeLanguageModel = class {
13263
13427
  includeHistoryContext = true;
13264
13428
  }
13265
13429
  let forkFromClaudeSessionId;
13266
- if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && !useInteractive && failoverAnswer?.kind !== "switch") {
13430
+ if (this.config.forkSessions === true && includeHistoryContext && !compactionMode && failoverAnswer?.kind !== "switch") {
13267
13431
  const parent = findForkParent({
13268
13432
  sessionKey: sk,
13269
13433
  prompt: options.prompt,
@@ -13319,7 +13483,7 @@ var ClaudeCodeLanguageModel = class {
13319
13483
  });
13320
13484
  }
13321
13485
  }
13322
- if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
13486
+ if (this.config.forkSessions === true && !compactionMode) {
13323
13487
  recordForkFingerprint(sk, options.prompt, cliPath);
13324
13488
  }
13325
13489
  const { model: spawnModelId, fast: fastMode } = parseModelId(failover.modelId);
@@ -13375,6 +13539,7 @@ var ClaudeCodeLanguageModel = class {
13375
13539
  if (useInteractive) reportUnmeasuredInteractiveCli(cliVersion);
13376
13540
  const ptyReadOnly = useInteractive && !compactionMode && !doctor && isReadOnlyPermissionMode(this.config.permissionMode);
13377
13541
  const ptyReadOnlySupported = cliSupportsRestricted(cliVersion) && cliSupportsDontAsk(cliVersion);
13542
+ const ptyBypassSupported = cliSupportsInteractiveBypass(cliVersion);
13378
13543
  if (ptyReadOnly && !ptyReadOnlySupported) {
13379
13544
  throw new Error(
13380
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."
@@ -13384,14 +13549,13 @@ var ClaudeCodeLanguageModel = class {
13384
13549
  this.config.failoverAccounts,
13385
13550
  sourceAccount
13386
13551
  );
13387
- 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
13388
13553
  // rate-limit error, never the form. See `TurnMode`.
13389
13554
  mode !== "generate" && isAccountFailoverQuestionActive({
13390
13555
  configured: this.config.accountFailover,
13391
13556
  candidates: failoverAccounts,
13392
13557
  opencodeHasQuestion: (await loadLiveToolInfo()).hasQuestion,
13393
13558
  compactionMode,
13394
- interactive: !!useInteractive,
13395
13559
  // A subagent follows its parent's account for free, because the
13396
13560
  // override is account-scoped. Asking it would put a form in a session
13397
13561
  // the operator is usually not even looking at.
@@ -13773,7 +13937,7 @@ var ClaudeCodeLanguageModel = class {
13773
13937
  }
13774
13938
  if (interactiveBypassRequested) {
13775
13939
  log.warn(
13776
- "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
13940
+ "interactiveBypass is ignored: skipPermissions governs the interactive transport too"
13777
13941
  );
13778
13942
  }
13779
13943
  const skillPluginDirs = await resolveSkillPluginDirs({
@@ -13787,13 +13951,19 @@ var ClaudeCodeLanguageModel = class {
13787
13951
  const posture = interactivePermissionPosture({
13788
13952
  permissionMode: self.config.permissionMode,
13789
13953
  allow,
13790
- supportsReadOnly: ptyReadOnlySupported
13954
+ supportsReadOnly: ptyReadOnlySupported,
13955
+ skipPermissions: self.config.skipPermissions,
13956
+ controlRequestBehavior: self.config.controlRequestBehavior,
13957
+ controlRequestToolBehaviors: self.config.controlRequestToolBehaviors,
13958
+ supportsBypass: ptyBypassSupported
13791
13959
  });
13792
13960
  if (!posture) throw new Error("the interactive transport cannot hold this permission posture");
13793
13961
  const ap = spawnInteractiveProcess({
13794
13962
  cwd,
13795
13963
  cliPath,
13796
- 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,
13797
13967
  model: spawnModelId,
13798
13968
  fastMode,
13799
13969
  mcpConfigPaths: mcp.paths,
@@ -13801,19 +13971,26 @@ var ClaudeCodeLanguageModel = class {
13801
13971
  permissionsAllow: posture.allow,
13802
13972
  permissionMode: posture.permissionMode,
13803
13973
  restricted: posture.restricted,
13804
- disallowedTools: wiring.allDisallowed,
13974
+ bypass: posture.bypass,
13975
+ disallowedTools: [...wiring.allDisallowed, ...posture.disallowed],
13805
13976
  proxyServer: state.proxyServer,
13806
13977
  systemPromptFile,
13807
13978
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13808
13979
  effort: reasoningEffort,
13809
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 } : {},
13810
13984
  // The headless spawn's env, so hygiene, effort and the
13811
- // agent's prompt cache TTL reach both transports alike.
13812
- env: claudeSpawnEnv({
13813
- ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13814
- effort: reasoningEffort,
13815
- promptCacheTtl
13816
- })
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
+ )
13817
13994
  });
13818
13995
  ap.proc.once(
13819
13996
  "exit",
@@ -13821,6 +13998,7 @@ var ClaudeCodeLanguageModel = class {
13821
13998
  );
13822
13999
  ap.mcpHash = mcp.bridgedHash;
13823
14000
  ap.mcpServers = mcp.allEnabledServerNames;
14001
+ ap.cliPath = cliPath;
13824
14002
  setActiveProcess(sk, ap);
13825
14003
  state.proc = ap.proc;
13826
14004
  state.lineEmitter = ap.lineEmitter;
@@ -13828,6 +14006,7 @@ var ClaudeCodeLanguageModel = class {
13828
14006
  log.info("spawned interactive claude session", {
13829
14007
  sk,
13830
14008
  resumed: !!resumeSessionId,
14009
+ forked: !!forkFromClaudeSessionId && !resumeSessionId,
13831
14010
  cliPath,
13832
14011
  configDir: self.config.configDir,
13833
14012
  model: effectiveModelId
@@ -14042,7 +14221,8 @@ var ClaudeCodeLanguageModel = class {
14042
14221
  sourceAccount,
14043
14222
  candidates: failoverAccounts,
14044
14223
  resetsAt: state.accountLimitHit?.resetsAt,
14045
- window: state.accountLimitHit?.window
14224
+ window: state.accountLimitHit?.window,
14225
+ resetsText: state.accountLimitHit?.resetsText
14046
14226
  })
14047
14227
  });
14048
14228
  state.endTextBlock();
@@ -14332,9 +14512,10 @@ var ClaudeCodeLanguageModel = class {
14332
14512
  state.proc.on("error", procErrorHandler);
14333
14513
  if (hasMatchedPendingResults) {
14334
14514
  state.cliAskedForWork = true;
14335
- const forwardTrailingUserMessages = () => {
14515
+ const forwardTrailingUserMessages = async () => {
14336
14516
  if (compactionMode || !state.activeProcess) return 0;
14337
- if (state.activeProcess.interactiveControl) return 0;
14517
+ const control = state.activeProcess.interactiveControl;
14518
+ if (control && (!control.queueInput || !control.turnRunning())) return 0;
14338
14519
  const trailing = getTrailingUserMessages(effectivePrompt, {
14339
14520
  stripContextReminders: self.stripContextRemindersEnabled()
14340
14521
  });
@@ -14345,6 +14526,21 @@ var ClaudeCodeLanguageModel = class {
14345
14526
  assistantIndex: trailing.assistantIndex,
14346
14527
  count: trailing.messages.length
14347
14528
  };
14529
+ if (control) {
14530
+ let queued = 0;
14531
+ for (const content of fresh) if (await control.queueInput(content)) queued++;
14532
+ if (queued < fresh.length) {
14533
+ log.warn("the interactive turn did not take a forwarded user message", {
14534
+ sessionKey: sk,
14535
+ messages: fresh.length - queued
14536
+ });
14537
+ }
14538
+ log.info("forwarded user messages into the interactive turn's input queue", {
14539
+ sessionKey: sk,
14540
+ messages: queued
14541
+ });
14542
+ return queued;
14543
+ }
14348
14544
  for (const content of fresh) {
14349
14545
  state.proc.stdin?.write(
14350
14546
  JSON.stringify({ type: "user", message: { role: "user", content } }) + "\n"
@@ -14356,7 +14552,7 @@ var ClaudeCodeLanguageModel = class {
14356
14552
  });
14357
14553
  return fresh.length;
14358
14554
  };
14359
- if (!state.unattendedTurnEnded && forwardTrailingUserMessages() > 0) {
14555
+ if (!state.unattendedTurnEnded && await forwardTrailingUserMessages() > 0) {
14360
14556
  await new Promise((resolve6) => setTimeout(resolve6, FORWARD_SETTLE_MS));
14361
14557
  if (state.controllerClosed) return;
14362
14558
  }
@@ -14391,7 +14587,7 @@ var ClaudeCodeLanguageModel = class {
14391
14587
  }
14392
14588
  if (state.unattendedTurnEnded) {
14393
14589
  deliverPendingCompletions(state);
14394
- forwardTrailingUserMessages();
14590
+ await forwardTrailingUserMessages();
14395
14591
  }
14396
14592
  const unemitted = getPendingProxyCalls(sk).filter(
14397
14593
  (call) => !call.emitted