diff --git a/packages/opencode/src/session/code-mode.ts b/packages/opencode/src/session/code-mode.ts index 14c242f95c..a96f451230 100644 --- a/packages/opencode/src/session/code-mode.ts +++ b/packages/opencode/src/session/code-mode.ts @@ -156,19 +156,26 @@ const returnType = (outputSchema: JSONSchema7 | undefined) => const signatureFor = (entry: CatalogEntry) => `tools${access(entry.server)}${access(entry.local)}(input: ${inputType(entry.tool)}): ${returnType(entry.outputSchema)}` +/** The compact, directly-callable signature for the inline preview: the call path + * plus its input type, but without the (uniform) `Promise<{ result, attachments? }>` + * return — that full typed form is reserved for `tools.$rune.describe`. */ +const previewSignature = (entry: CatalogEntry) => + `tools${access(entry.server)}${access(entry.local)}(input: ${inputType(entry.tool)})` + /** - * Character budget for the inline tool preview in the tool description. All - * namespaces are always listed; individual tools are previewed (cheapest first, - * server by server) until this many characters of preview lines are used, after - * which the remaining namespaces show counts only. This front-loads a useful slice - * of the catalog — cutting discovery round-trips — without dumping every tool. + * Character budget for the inline signature preview in the tool description. All + * namespaces are always listed; per-tool call signatures are previewed (cheapest + * first, server by server) until this many characters are used, after which the + * remaining namespaces show counts only. This front-loads a directly-callable slice + * of the catalog — cutting discovery round-trips — without dumping every signature. */ const PREVIEW_BUDGET = 2000 /** * The execute tool description: the calling convention, the discovery API, and the - * list of namespaces. A budgeted preview of individual tools is inlined; the full - * per-tool signatures are still fetched on demand with `tools.$rune.describe`. + * list of namespaces. A budgeted preview of per-tool call signatures is inlined; the + * full typed signature + schemas are fetched on demand with `tools.$rune.describe`, + * and any tool not previewed must be found via `tools.$rune.search` first. */ export function describe(groups: Map): string { const lines = [ @@ -193,14 +200,19 @@ export function describe(groups: Map): string { lines.push("", "No MCP servers are currently connected.") return lines.join("\n") } - lines.push("", "Available namespaces (use tools.$rune.search / tools.$rune.describe to explore tools not shown):") + 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.", + ) 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 = ` - ${entry.path}${entry.description ? ` — ${brief(entry.description, 80)}` : ""}` + const line = ` - ${previewSignature(entry)}` if (used + line.length > PREVIEW_BUDGET) { previewing = false break diff --git a/packages/opencode/test/session/code-mode.test.ts b/packages/opencode/test/session/code-mode.test.ts index 38c7ba1687..0c4ad4d815 100644 --- a/packages/opencode/test/session/code-mode.test.ts +++ b/packages/opencode/test/session/code-mode.test.ts @@ -65,10 +65,14 @@ describe("code mode execute", () => { await expect(Effect.runPromise(decode({}))).rejects.toThrow() }) - test("lists all namespaces, previews tools within budget, and documents discovery", () => { + test("lists all namespaces, previews tool signatures within budget, and documents discovery", () => { const groups = groupByServer( { - github_create_issue: mcpTool("create_issue", () => ""), + github_create_issue: mcpTool("create_issue", () => "", { + type: "object", + properties: { title: { type: "string" }, body: { type: "string" } }, + required: ["title"], + }), github_list_issues: mcpTool("list_issues", () => ""), linear_search: mcpTool("search", () => ""), }, @@ -78,21 +82,23 @@ 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. expect(description).toContain("- github (2 tools)") expect(description).toContain("- linear (1 tool)") - // Small catalog: individual tools are previewed inline as `.`. - expect(description).toContain("github.create_issue") - expect(description).toContain("linear.search") - // ...but never full signatures (those come from tools.$rune.describe). + // Tools are previewed inline as compact, directly-callable input signatures. + expect(description).toContain("tools.github.create_issue(input: { title: string; body?: string })") + expect(description).toContain("tools.linear.search(input: object)") + // ...but not the full Promise return — that comes from tools.$rune.describe. expect(description).not.toContain("): Promise<") }) test("falls back to namespaces-only when the catalog exceeds the preview budget", () => { const tools: Record = {} - const longDesc = "performs a meaningful operation against the service with several options" for (let i = 0; i < 60; i++) { - tools[`alpha_op_${i}`] = mcpTool(`op_${i}`, () => "", { type: "object", properties: {} }) - ;(tools[`alpha_op_${i}`] as any).description = longDesc + tools[`alpha_op_${i}`] = mcpTool(`op_${i}`, () => "", { + type: "object", + properties: { value: { type: "string" }, count: { type: "number" } }, + }) } tools["zeta_only_tool"] = mcpTool("only_tool", () => "") const groups = groupByServer(tools, ["alpha", "zeta"]) @@ -101,10 +107,10 @@ describe("code mode execute", () => { // 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 tools are not all inlined. - expect(description).not.toContain("zeta.only_tool") - // Some early tools are still previewed. - expect(description).toContain("alpha.op_0") + // The preview is budget-capped, so the later namespace's signatures are not all inlined. + expect(description).not.toContain("tools.zeta.only_tool(") + // Some early signatures are still previewed. + expect(description).toContain("tools.alpha.op_0(") }) test("tools.$rune.search and tools.$rune.describe expose the catalog on demand", async () => {