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

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
@@ -5862,6 +5862,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5862
5862
  "stale scratch directory sweep failed",
5863
5863
  "task_batch resolving with child results missing",
5864
5864
  "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",
5865
+ "the interactive turn did not take a forwarded user message",
5865
5866
  "this opencode process is running an older plugin build than the one on disk",
5866
5867
  "timed out pending proxy call",
5867
5868
  "turn abort listener threw",
@@ -8999,6 +9000,9 @@ var ClaudeSession = class {
8999
9000
  * (the TUI repaints it while it takes the key, measured on 2.1.288) can
9000
9001
  * never stand in for the next call's dialog. */
9001
9002
  planDialog = null;
9003
+ /** `queue-operation` enqueue records read, which is how `queueInput` knows
9004
+ * the TUI took a message into the running turn. */
9005
+ enqueued = 0;
9002
9006
  /** This turn's `ExitPlanMode` calls already answered. */
9003
9007
  answeredPlans = /* @__PURE__ */ new Set();
9004
9008
  abandonTurn = false;
@@ -9057,7 +9061,9 @@ var ClaudeSession = class {
9057
9061
  // The decision arrives after opencode's own question round trip, by
9058
9062
  // which time the dialog has long been drawn; this only bounds a TUI
9059
9063
  // that never draws it.
9060
- planDialogWaitMs: opts.planDialogWaitMs ?? 3e4
9064
+ planDialogWaitMs: opts.planDialogWaitMs ?? 3e4,
9065
+ // 100 times the longest silence measured while Claude works (h #g207).
9066
+ idleEndMs: opts.idleEndMs ?? 2e4
9061
9067
  };
9062
9068
  }
9063
9069
  /** The CLI arguments this session spawns with. */
@@ -9335,7 +9341,8 @@ var ClaudeSession = class {
9335
9341
  end: null,
9336
9342
  sawUserRecord: false,
9337
9343
  lastBeat: Date.now(),
9338
- awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null
9344
+ awaitingOwnPrompt: this.forkPending ? normalizedPrompt(prompt) : null,
9345
+ sawReply: false
9339
9346
  };
9340
9347
  this.forkPending = false;
9341
9348
  this.scan = scan;
@@ -9358,6 +9365,9 @@ var ClaudeSession = class {
9358
9365
  if (scan.stopReason && Date.now() - scan.stopSeenAt >= this.o.stopSettleMs) {
9359
9366
  return { stopReason: scan.stopReason, end: "stop", denied: this.turnDenied };
9360
9367
  }
9368
+ if (scan.sawReply && !this.planApprovalToolUseId && Date.now() - this.lastDataAt >= this.o.idleEndMs && Date.now() - scan.stopSeenAt >= this.o.idleEndMs) {
9369
+ return { stopReason: null, end: "ended", denied: this.turnDenied };
9370
+ }
9361
9371
  if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
9362
9372
  if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
9363
9373
  const now = Date.now();
@@ -9407,9 +9417,11 @@ var ClaudeSession = class {
9407
9417
  scan.awaitingOwnPrompt = null;
9408
9418
  }
9409
9419
  if (rec) this.notePlanApproval(rec);
9420
+ if (rec?.type === "queue-operation" && rec.operation === "enqueue") this.enqueued++;
9410
9421
  scan.onRecord(s, rec);
9411
9422
  if (rec) {
9412
9423
  if (rec.type === "assistant" && rec.message) {
9424
+ scan.sawReply = true;
9413
9425
  const reason = rec.message.stop_reason;
9414
9426
  if (typeof reason === "string") {
9415
9427
  scan.stopReason = TERMINAL_STOP.has(reason) ? reason : null;
@@ -9498,6 +9510,29 @@ var ClaudeSession = class {
9498
9510
  this.write("\r");
9499
9511
  return true;
9500
9512
  }
9513
+ /**
9514
+ * Type a message into the running turn's own input queue, not as a turn of
9515
+ * its own (h #g206). Measured on 2.1.288: text pasted and submitted while a
9516
+ * tool call runs is recorded as `queue-operation` enqueue and remove plus a
9517
+ * `queued_command` attachment, answered in the same turn, which still ends
9518
+ * once. Resolves true once the enqueue is in the transcript; false with no
9519
+ * turn running, or with a plan approval dialog up, which would take the
9520
+ * keys as its answer.
9521
+ */
9522
+ async queueInput(text, timeoutMs = 3e3) {
9523
+ if (!this.turn || !this.proc || this.exited || this.pendingPlanApproval) return false;
9524
+ const before = this.enqueued;
9525
+ this.write(this.o.bracketedPaste ? "\x1B[200~" + text + "\x1B[201~" : text);
9526
+ await delay(this.o.submitMinMs);
9527
+ this.write("\r");
9528
+ const deadline = Date.now() + timeoutMs;
9529
+ while (this.enqueued === before) {
9530
+ if (!this.turn || this.exited || Date.now() >= deadline) return false;
9531
+ await delay(this.o.pollMs);
9532
+ this.scanTranscript();
9533
+ }
9534
+ return true;
9535
+ }
9501
9536
  /** Wait until the PTY has been quiet for `permissionQuietMs`, at most 3 s. */
9502
9537
  async waitForQuiet() {
9503
9538
  const until = Date.now() + 3e3;
@@ -10123,6 +10158,13 @@ function spawnInteractiveProcess(opts) {
10123
10158
  },
10124
10159
  flushTranscript: () => session.flushTranscript(),
10125
10160
  planApprovalPending: () => session.pendingPlanApproval !== null,
10161
+ // Text only: an image in a mid-call message is dropped with the
10162
+ // decoder's own warning rather than staged for a turn that is not ours.
10163
+ queueInput: async (content) => {
10164
+ const text = decodeUserEnvelope(JSON.stringify({ type: "user", message: { role: "user", content } })).trim();
10165
+ if (!text) return false;
10166
+ return session.queueInput(text);
10167
+ },
10126
10168
  askAside
10127
10169
  }
10128
10170
  };
@@ -14332,9 +14374,10 @@ var ClaudeCodeLanguageModel = class {
14332
14374
  state.proc.on("error", procErrorHandler);
14333
14375
  if (hasMatchedPendingResults) {
14334
14376
  state.cliAskedForWork = true;
14335
- const forwardTrailingUserMessages = () => {
14377
+ const forwardTrailingUserMessages = async () => {
14336
14378
  if (compactionMode || !state.activeProcess) return 0;
14337
- if (state.activeProcess.interactiveControl) return 0;
14379
+ const control = state.activeProcess.interactiveControl;
14380
+ if (control && (!control.queueInput || !control.turnRunning())) return 0;
14338
14381
  const trailing = getTrailingUserMessages(effectivePrompt, {
14339
14382
  stripContextReminders: self.stripContextRemindersEnabled()
14340
14383
  });
@@ -14345,6 +14388,21 @@ var ClaudeCodeLanguageModel = class {
14345
14388
  assistantIndex: trailing.assistantIndex,
14346
14389
  count: trailing.messages.length
14347
14390
  };
14391
+ if (control) {
14392
+ let queued = 0;
14393
+ for (const content of fresh) if (await control.queueInput(content)) queued++;
14394
+ if (queued < fresh.length) {
14395
+ log.warn("the interactive turn did not take a forwarded user message", {
14396
+ sessionKey: sk,
14397
+ messages: fresh.length - queued
14398
+ });
14399
+ }
14400
+ log.info("forwarded user messages into the interactive turn's input queue", {
14401
+ sessionKey: sk,
14402
+ messages: queued
14403
+ });
14404
+ return queued;
14405
+ }
14348
14406
  for (const content of fresh) {
14349
14407
  state.proc.stdin?.write(
14350
14408
  JSON.stringify({ type: "user", message: { role: "user", content } }) + "\n"
@@ -14356,7 +14414,7 @@ var ClaudeCodeLanguageModel = class {
14356
14414
  });
14357
14415
  return fresh.length;
14358
14416
  };
14359
- if (!state.unattendedTurnEnded && forwardTrailingUserMessages() > 0) {
14417
+ if (!state.unattendedTurnEnded && await forwardTrailingUserMessages() > 0) {
14360
14418
  await new Promise((resolve6) => setTimeout(resolve6, FORWARD_SETTLE_MS));
14361
14419
  if (state.controllerClosed) return;
14362
14420
  }
@@ -14391,7 +14449,7 @@ var ClaudeCodeLanguageModel = class {
14391
14449
  }
14392
14450
  if (state.unattendedTurnEnded) {
14393
14451
  deliverPendingCompletions(state);
14394
- forwardTrailingUserMessages();
14452
+ await forwardTrailingUserMessages();
14395
14453
  }
14396
14454
  const unemitted = getPendingProxyCalls(sk).filter(
14397
14455
  (call) => !call.emitted