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(")