@khalilgharbaoui/opencode-claude-code-plugin 0.15.2 → 0.15.4

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/README.md CHANGED
@@ -524,7 +524,14 @@ The plugin registers the command without replacing an existing user-defined `btw
524
524
 
525
525
  Where the answer appears, in the conversation either way:
526
526
 
527
- - **A receipt, straight away**, when you asked while a turn was running: a `▌ **btw:** *sent to Claude on the side*` line in the reply you are watching, so a `/btw` typed mid-turn is visibly taken rather than looking swallowed until the answer arrives. If opencode is between two streams at that moment (it was running a tool), the receipt lands when the next one opens.
527
+ - **A receipt, straight away**, when you asked while a turn was running, in the reply you are watching, so a `/btw` typed mid-turn is visibly taken rather than looking swallowed until the answer arrives:
528
+
529
+ ```text
530
+ ▌ **btw:** <your question, in full>
531
+ ▌ *sent to Claude on the side*
532
+ ```
533
+
534
+ It quotes the question back untruncated because the prompt box clears on submit and no `/btw` message is ever created, so this is the only place you can read back what you sent. If opencode is between two streams at that moment (it was running a tool), the receipt lands when the next one opens.
528
535
  - **Inside the running turn's own reply**, as soon as the answer arrives, when you asked while Claude was working. It is written into the reply you are already watching as its own block, headed `▌ **btw:** <your question>`, so it stays there and is easy to pick out. Every line of the aside, answer included, carries that `▌` bar, so it reads as one block down its whole height. Nothing is queued and the `/btw` message itself is dropped, because the answer is already in the transcript. The turn goes on to deliver its own reply as usual.
529
536
  - **As its own `/btw` message and answer** when the conversation is idle, or when the turn had no stream open to write into at that moment (opencode was running a tool between two of them). In the second case the pair lands when the turn ends; nothing is announced in the meantime, because the answer itself is what arrives.
530
537
  - Follow-ups work: earlier asides in the conversation are sent along as the aside's history.
package/dist/index.js CHANGED
@@ -2100,8 +2100,11 @@ var LEGACY_INLINE_ASIDE_MARKERS = ["> **btw:**"];
2100
2100
  function barEveryLine(text) {
2101
2101
  return text.split("\n").map((line) => line.trim() === "" ? "\u258C" : `\u258C ${line}`).join("\n");
2102
2102
  }
2103
+ function oneLine(question) {
2104
+ return question.replace(/\s+/g, " ").trim();
2105
+ }
2103
2106
  function asideHeader(question) {
2104
- return `${INLINE_ASIDE_MARKER} ${question.replace(/\s+/g, " ").trim()}`;
2107
+ return `${INLINE_ASIDE_MARKER} ${oneLine(question)}`;
2105
2108
  }
2106
2109
  function formatInlineAside(question, answer) {
2107
2110
  return `
@@ -2110,10 +2113,11 @@ ${asideHeader(question)}
2110
2113
  ${barEveryLine(answer.trim())}
2111
2114
  `;
2112
2115
  }
2113
- var INLINE_ASIDE_SENT = `${INLINE_ASIDE_MARKER} *sent to Claude on the side*`;
2114
- function formatInlineAsideAsk() {
2116
+ var INLINE_ASIDE_SENT_NOTE = "*sent to Claude on the side*";
2117
+ function formatInlineAsideAsk(question) {
2115
2118
  return `
2116
- ${INLINE_ASIDE_SENT}
2119
+ ${asideHeader(question)}
2120
+ \u258C ${INLINE_ASIDE_SENT_NOTE}
2117
2121
  `;
2118
2122
  }
2119
2123
  var asideSinks = /* @__PURE__ */ new Map();
@@ -2282,7 +2286,7 @@ async function handleBtwCommand(client, input, options = {}) {
2282
2286
  questionLength: question.length,
2283
2287
  history: history.length
2284
2288
  });
2285
- const asked = busy ? deliverAsideInline(client, input.sessionID, formatInlineAsideAsk(), options).catch(() => false) : Promise.resolve(false);
2289
+ const asked = busy ? deliverAsideInline(client, input.sessionID, formatInlineAsideAsk(question), options).catch(() => false) : Promise.resolve(false);
2286
2290
  answer.then(
2287
2291
  async (result) => {
2288
2292
  log.info("btw: early answer arrived", { sessionID: input.sessionID, busy, responseLength: result.response.length });
@@ -2507,7 +2511,7 @@ function compactConversationHistory(prompt, opts = {}) {
2507
2511
  return buildCompactionHistory(prompt);
2508
2512
  }
2509
2513
  const conversationMessages = prompt.filter(
2510
- (m) => m.role === "user" || m.role === "assistant"
2514
+ (m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
2511
2515
  );
2512
2516
  if (conversationMessages.length <= 1) {
2513
2517
  return null;
@@ -2515,28 +2519,8 @@ function compactConversationHistory(prompt, opts = {}) {
2515
2519
  const historyParts = [];
2516
2520
  for (let i = 0; i < conversationMessages.length - 1; i++) {
2517
2521
  const msg = conversationMessages[i];
2518
- const role = msg.role === "user" ? "User" : "Assistant";
2519
- let text = "";
2520
- if (typeof msg.content === "string") {
2521
- text = msg.content;
2522
- } else if (Array.isArray(msg.content)) {
2523
- const textParts = msg.content.filter((p) => p.type === "text" && p.text).map((p) => p.text);
2524
- text = textParts.join("\n");
2525
- const toolCalls = msg.content.filter(
2526
- (p) => p.type === "tool-call"
2527
- );
2528
- const toolResults = msg.content.filter(
2529
- (p) => p.type === "tool-result"
2530
- );
2531
- if (toolCalls.length > 0) {
2532
- text += `
2533
- [Called ${toolCalls.length} tool(s): ${toolCalls.map((t) => t.toolName).join(", ")}]`;
2534
- }
2535
- if (toolResults.length > 0) {
2536
- text += `
2537
- [Received ${toolResults.length} tool result(s)]`;
2538
- }
2539
- }
2522
+ const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
2523
+ const { text } = renderMessageContentForCompaction(msg);
2540
2524
  if (text.trim()) {
2541
2525
  const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
2542
2526
  historyParts.push(`${role}: ${truncated}`);
@@ -2579,7 +2563,27 @@ function buildCompactionHistory(prompt) {
2579
2563
  }
2580
2564
  function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
2581
2565
  const compactionMode = opts.compactionMode === true;
2566
+ const cliToolCallIds = opts.cliToolCallIds;
2582
2567
  const content = [];
2568
+ const pushToolResult = (part) => {
2569
+ const id = part.toolCallId;
2570
+ const text = getToolResultText(part);
2571
+ if (!cliToolCallIds || cliToolCallIds.has(id)) {
2572
+ content.push({ type: "tool_result", tool_use_id: id, content: text });
2573
+ return;
2574
+ }
2575
+ log.info("rendering opencode-side tool result as text", {
2576
+ toolCallId: id,
2577
+ toolName: part.toolName,
2578
+ chars: text.length
2579
+ });
2580
+ content.push({
2581
+ type: "text",
2582
+ text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
2583
+ ${text}
2584
+ </opencode_tool_result>`
2585
+ });
2586
+ };
2583
2587
  if (compactionMode) {
2584
2588
  const transcript = compactConversationHistory(prompt, {
2585
2589
  mode: "compaction"
@@ -2649,12 +2653,7 @@ Now continuing with the current message:
2649
2653
  });
2650
2654
  }
2651
2655
  } else if (part.type === "tool-result") {
2652
- const p = part;
2653
- content.push({
2654
- type: "tool_result",
2655
- tool_use_id: p.toolCallId,
2656
- content: getToolResultText(p)
2657
- });
2656
+ pushToolResult(part);
2658
2657
  }
2659
2658
  }
2660
2659
  }
@@ -2662,12 +2661,7 @@ Now continuing with the current message:
2662
2661
  if (Array.isArray(msg.content)) {
2663
2662
  for (const part of msg.content) {
2664
2663
  if (part?.type === "tool-result") {
2665
- const p = part;
2666
- content.push({
2667
- type: "tool_result",
2668
- tool_use_id: p.toolCallId,
2669
- content: getToolResultText(p)
2670
- });
2664
+ pushToolResult(part);
2671
2665
  }
2672
2666
  }
2673
2667
  }
@@ -5054,7 +5048,12 @@ var ClaudeCodeLanguageModel = class {
5054
5048
  }
5055
5049
  const hasExistingSession = !!getClaudeSessionId(sk);
5056
5050
  const includeHistoryContext = !hasExistingSession && hasPriorConversation;
5057
- const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ?? getClaudeUserMessage(options.prompt, includeHistoryContext);
5051
+ const userMsg = consumeExitPlanModeQuestionResult(sk, options.prompt) ?? // doGenerate has no proxy wiring, so this process issued no tool calls
5052
+ // at all: every tool result reaching it belongs to opencode and must be
5053
+ // rendered as text rather than an orphaned `tool_result` (issue #29).
5054
+ getClaudeUserMessage(options.prompt, includeHistoryContext, {
5055
+ cliToolCallIds: /* @__PURE__ */ new Set()
5056
+ });
5058
5057
  const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
5059
5058
  getRuntimeMcpStatus(),
5060
5059
  detectCliVersion(this.config.cliPath),
@@ -5510,8 +5509,10 @@ ${plan}
5510
5509
  if (exitPlanModeQuestionResult) {
5511
5510
  log.info("sending plan approval decision to claude", { sk });
5512
5511
  }
5512
+ const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
5513
5513
  const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(options.prompt, includeHistoryContext, {
5514
- compactionMode
5514
+ compactionMode,
5515
+ cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId))
5515
5516
  });
5516
5517
  const resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
5517
5518
  const loadLiveToolInfo = this.createLiveToolInfoLoader();
@@ -5520,7 +5521,6 @@ ${plan}
5520
5521
  loadLiveToolInfo
5521
5522
  );
5522
5523
  const self = this;
5523
- const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
5524
5524
  const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
5525
5525
  call,
5526
5526
  result: this.extractPendingProxyResult(options.prompt, call.toolCallId)