From dc75ea0cc0e3501b0689fd16810e9f6f4ac8f259 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 30 Jun 2026 16:56:37 -0500 Subject: [PATCH] feat(opencode): state whether the code mode tool list is complete or partial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preview now reports its own comprehensiveness so the model knows when it has the whole catalog vs. when it must search: - Overall: "This is the COMPLETE list ..." when every tool fits the budget, else "This is a PARTIAL list — X of Y tools are shown ...". - Per namespace: a fully-shown server reads `- github (2 tools)`; a truncated one is annotated `- alpha (70 tools, 31 shown)` or `- zeta (1 tool, none shown)`. When complete, the model isn't pushed toward needless $rune.search calls; when partial, exactly what's missing is unambiguous. --- packages/opencode/src/session/code-mode.ts | 58 +++++++++++++------ .../opencode/test/session/code-mode.test.ts | 12 ++-- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/packages/opencode/src/session/code-mode.ts b/packages/opencode/src/session/code-mode.ts index a96f451230..719d815acc 100644 --- a/packages/opencode/src/session/code-mode.ts +++ b/packages/opencode/src/session/code-mode.ts @@ -200,26 +200,50 @@ export function describe(groups: Map): string { lines.push("", "No MCP servers are currently connected.") return lines.join("\n") } + // Select which signatures fit the budget (cheapest first within each server, + // servers alphabetical) before emitting, so the list can state exactly how + // comprehensive it is — overall and per namespace. + const ordered = [...groups].sort(([a], [b]) => a.localeCompare(b)) + const shown = new Map() + let used = 0 + let budgetLeft = true + let totalTools = 0 + let totalShown = 0 + for (const [server, entries] of ordered) { + totalTools += entries.length + const picked: string[] = [] + if (budgetLeft) { + for (const entry of entries) { + const line = ` - ${previewSignature(entry)}` + if (used + line.length > PREVIEW_BUDGET) { + budgetLeft = false + break + } + picked.push(line) + used += line.length + } + } + shown.set(server, picked) + totalShown += picked.length + } + + const complete = totalShown === totalTools lines.push( "", - "Every connected server is listed below with its tool count. A budgeted sample of tool", - "signatures is shown inline; any tool not shown must be found with `tools.$rune.search` /", - "`tools.$rune.describe` before you call it.", + complete + ? "This is the COMPLETE list of available tools — every connected tool is shown below with its call signature. Use `tools.$rune.describe(path)` for a tool's full types." + : `This is a PARTIAL list — ${totalShown} of ${totalTools} tools are shown below. Any tool not listed must be found with \`tools.$rune.search\` first; use \`tools.$rune.describe(path)\` for full types.`, ) - let used = 0 - let previewing = true - for (const [server, entries] of [...groups].sort(([a], [b]) => a.localeCompare(b))) { - lines.push(`- ${server} (${entries.length} tool${entries.length === 1 ? "" : "s"})`) - if (!previewing) continue - for (const entry of entries) { - const line = ` - ${previewSignature(entry)}` - if (used + line.length > PREVIEW_BUDGET) { - previewing = false - break - } - lines.push(line) - used += line.length - } + for (const [server, entries] of ordered) { + const picked = shown.get(server)! + const total = entries.length + const count = `${total} tool${total === 1 ? "" : "s"}` + // Annotate only when a namespace is not fully shown, so a comprehensive + // namespace reads cleanly and a truncated one is unambiguous. + const label = + picked.length === total ? count : picked.length === 0 ? `${count}, none shown` : `${count}, ${picked.length} shown` + lines.push(`- ${server} (${label})`) + for (const line of picked) lines.push(line) } return lines.join("\n") } diff --git a/packages/opencode/test/session/code-mode.test.ts b/packages/opencode/test/session/code-mode.test.ts index 0c4ad4d815..ac6c212f4b 100644 --- a/packages/opencode/test/session/code-mode.test.ts +++ b/packages/opencode/test/session/code-mode.test.ts @@ -82,7 +82,8 @@ describe("code mode execute", () => { expect(description).toContain("tools.$rune.search(query") expect(description).toContain("tools.$rune.describe(path)") - // Every namespace is always listed, with counts. + // Small catalog: the list is comprehensive and says so, with clean counts. + expect(description).toContain("This is the COMPLETE list") expect(description).toContain("- github (2 tools)") expect(description).toContain("- linear (1 tool)") // Tools are previewed inline as compact, directly-callable input signatures. @@ -104,10 +105,11 @@ describe("code mode execute", () => { const groups = groupByServer(tools, ["alpha", "zeta"]) const description = describeTools(groups) - // Every namespace is always present, with counts. - expect(description).toContain("- alpha (60 tools)") - expect(description).toContain("- zeta (1 tool)") - // The preview is budget-capped, so the later namespace's signatures are not all inlined. + // The list states it is partial, and every namespace is still present with its total. + expect(description).toContain("This is a PARTIAL list") + expect(description).toContain("- alpha (60 tools") + // The later namespace is fully truncated, and says so. + expect(description).toContain("- zeta (1 tool, none shown)") expect(description).not.toContain("tools.zeta.only_tool(") // Some early signatures are still previewed. expect(description).toContain("tools.alpha.op_0(")