From fc62801a15dd7ae6d37f108bfdbc382fa3f71512 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 15 Jan 2026 01:52:45 -0500 Subject: [PATCH 1/4] feat: add set_current_session_title tool Add a new tool that allows the model to set the title of the current session. This enables custom slash commands to automatically title sessions based on their context. - Add tool definition with 1-255 character validation - Register tool in registry - Include comprehensive unit tests - Tool requests permission (configurable by user) - Updates session title immediately via reactive UI --- packages/opencode/src/tool/registry.ts | 2 + .../src/tool/set-current-session-title.ts | 36 ++++ .../src/tool/set-current-session-title.txt | 8 + .../tool/set-current-session-title.test.ts | 161 ++++++++++++++++++ 4 files changed, 207 insertions(+) create mode 100644 packages/opencode/src/tool/set-current-session-title.ts create mode 100644 packages/opencode/src/tool/set-current-session-title.txt create mode 100644 packages/opencode/test/tool/set-current-session-title.test.ts diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 35e378f080b..00d0dde34b7 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -26,6 +26,7 @@ import { Log } from "@/util/log" import { LspTool } from "./lsp" import { Truncate } from "./truncation" import { PlanExitTool, PlanEnterTool } from "./plan" +import { SetCurrentSessionTitleTool } from "./set-current-session-title" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -105,6 +106,7 @@ export namespace ToolRegistry { WebFetchTool, TodoWriteTool, TodoReadTool, + SetCurrentSessionTitleTool, WebSearchTool, CodeSearchTool, SkillTool, diff --git a/packages/opencode/src/tool/set-current-session-title.ts b/packages/opencode/src/tool/set-current-session-title.ts new file mode 100644 index 00000000000..f555853c2e6 --- /dev/null +++ b/packages/opencode/src/tool/set-current-session-title.ts @@ -0,0 +1,36 @@ +import z from "zod" +import { Tool } from "./tool" +import { Session } from "../session" +import DESCRIPTION from "./set-current-session-title.txt" + +export const SetCurrentSessionTitleTool = Tool.define("set_current_session_title", { + description: DESCRIPTION, + parameters: z.object({ + title: z + .string() + .min(1, "Title must be at least 1 character") + .max(255, "Title must be at most 255 characters") + .describe("The new title for the current session"), + }), + async execute(params, ctx) { + await ctx.ask({ + permission: "set_current_session_title", + patterns: ["*"], + always: ["*"], + metadata: {}, + }) + + const session = await Session.update(ctx.sessionID, (draft) => { + draft.title = params.title + }) + + return { + title: params.title, + output: `Session title updated to: ${session.title}`, + metadata: { + sessionID: ctx.sessionID, + title: session.title, + }, + } + }, +}) diff --git a/packages/opencode/src/tool/set-current-session-title.txt b/packages/opencode/src/tool/set-current-session-title.txt new file mode 100644 index 00000000000..5f2aa4f1a02 --- /dev/null +++ b/packages/opencode/src/tool/set-current-session-title.txt @@ -0,0 +1,8 @@ +Sets the title of the current session. + +Use this tool to give the session a descriptive title that reflects its purpose or content. The title will be immediately visible in the UI. + +Usage notes: +- The title must be between 1 and 255 characters +- Choose a concise, descriptive title that helps identify the session's purpose +- This is useful in custom slash commands to automatically title sessions based on their context diff --git a/packages/opencode/test/tool/set-current-session-title.test.ts b/packages/opencode/test/tool/set-current-session-title.test.ts new file mode 100644 index 00000000000..9ef550454e1 --- /dev/null +++ b/packages/opencode/test/tool/set-current-session-title.test.ts @@ -0,0 +1,161 @@ +import { describe, expect, test } from "bun:test" +import { SetCurrentSessionTitleTool } from "../../src/tool/set-current-session-title" +import { Instance } from "../../src/project/instance" +import { Session } from "../../src/session" +import { tmpdir } from "../fixture/fixture" + +describe("tool.set_current_session_title", () => { + test("updates session title", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create() + const tool = await SetCurrentSessionTitleTool.init() + const ctx = { + sessionID: session.id, + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + metadata: () => {}, + ask: async () => {}, + } + + const result = await tool.execute({ title: "My Test Session" }, ctx) + + expect(result.title).toBe("My Test Session") + expect(result.output).toContain("My Test Session") + + const updated = await Session.get(session.id) + expect(updated.title).toBe("My Test Session") + }, + }) + }) + + test("rejects empty title", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create() + const tool = await SetCurrentSessionTitleTool.init() + const ctx = { + sessionID: session.id, + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + metadata: () => {}, + ask: async () => {}, + } + + await expect(tool.execute({ title: "" }, ctx)).rejects.toThrow() + }, + }) + }) + + test("rejects title exceeding 255 characters", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create() + const tool = await SetCurrentSessionTitleTool.init() + const ctx = { + sessionID: session.id, + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + metadata: () => {}, + ask: async () => {}, + } + + const longTitle = "a".repeat(256) + await expect(tool.execute({ title: longTitle }, ctx)).rejects.toThrow() + }, + }) + }) + + test("accepts title with exactly 255 characters", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create() + const tool = await SetCurrentSessionTitleTool.init() + const ctx = { + sessionID: session.id, + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + metadata: () => {}, + ask: async () => {}, + } + + const maxTitle = "a".repeat(255) + const result = await tool.execute({ title: maxTitle }, ctx) + + expect(result.title).toBe(maxTitle) + const updated = await Session.get(session.id) + expect(updated.title).toBe(maxTitle) + }, + }) + }) + + test("accepts single character title", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create() + const tool = await SetCurrentSessionTitleTool.init() + const ctx = { + sessionID: session.id, + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + metadata: () => {}, + ask: async () => {}, + } + + const result = await tool.execute({ title: "X" }, ctx) + + expect(result.title).toBe("X") + const updated = await Session.get(session.id) + expect(updated.title).toBe("X") + }, + }) + }) + + test("asks for permission", async () => { + await using tmp = await tmpdir({ git: true }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const session = await Session.create() + const tool = await SetCurrentSessionTitleTool.init() + const requests: Array<{ permission: string }> = [] + const ctx = { + sessionID: session.id, + messageID: "", + callID: "", + agent: "build", + abort: AbortSignal.any([]), + metadata: () => {}, + ask: async (req: { permission: string }) => { + requests.push(req) + }, + } + + await tool.execute({ title: "Test Title" }, ctx) + + expect(requests.length).toBe(1) + expect(requests[0].permission).toBe("set_current_session_title") + }, + }) + }) +}) From 9e0091c148eaa4dbaa79250b12e8bf113db157c4 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 15 Jan 2026 01:55:00 -0500 Subject: [PATCH 2/4] fix: add required parameter to Session.create() in tests --- .../test/tool/set-current-session-title.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/opencode/test/tool/set-current-session-title.test.ts b/packages/opencode/test/tool/set-current-session-title.test.ts index 9ef550454e1..ecd38051f26 100644 --- a/packages/opencode/test/tool/set-current-session-title.test.ts +++ b/packages/opencode/test/tool/set-current-session-title.test.ts @@ -10,7 +10,7 @@ describe("tool.set_current_session_title", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create() + const session = await Session.create({}) const tool = await SetCurrentSessionTitleTool.init() const ctx = { sessionID: session.id, @@ -38,7 +38,7 @@ describe("tool.set_current_session_title", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create() + const session = await Session.create({}) const tool = await SetCurrentSessionTitleTool.init() const ctx = { sessionID: session.id, @@ -60,7 +60,7 @@ describe("tool.set_current_session_title", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create() + const session = await Session.create({}) const tool = await SetCurrentSessionTitleTool.init() const ctx = { sessionID: session.id, @@ -83,7 +83,7 @@ describe("tool.set_current_session_title", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create() + const session = await Session.create({}) const tool = await SetCurrentSessionTitleTool.init() const ctx = { sessionID: session.id, @@ -110,7 +110,7 @@ describe("tool.set_current_session_title", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create() + const session = await Session.create({}) const tool = await SetCurrentSessionTitleTool.init() const ctx = { sessionID: session.id, @@ -136,7 +136,7 @@ describe("tool.set_current_session_title", () => { await Instance.provide({ directory: tmp.path, fn: async () => { - const session = await Session.create() + const session = await Session.create({}) const tool = await SetCurrentSessionTitleTool.init() const requests: Array<{ permission: string }> = [] const ctx = { From 3704314a8216dc4b00359b1471442982f1e5be22 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 15 Jan 2026 04:54:02 -0500 Subject: [PATCH 3/4] fix: prevent race condition in session title generation Check if title is still default before overwriting in ensureTitle(). This prevents auto-generated titles from overwriting titles set by the set_current_session_title tool during the LLM call. --- packages/opencode/src/session/prompt.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 345b1c49e65..e138d699ee7 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1774,7 +1774,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the if (!cleaned) return const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned - draft.title = title + // Only set if title is still default (wasn't changed by tool during LLM call) + if (Session.isDefaultTitle(draft.title)) draft.title = title }) } } From dc49a2a89ef0c2d3c7e8c2c03218c5554ab6aec1 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 15 Jan 2026 05:05:01 -0500 Subject: [PATCH 4/4] tweak: minor revision to tool description for set_current_session_title tool --- packages/opencode/src/tool/set-current-session-title.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/set-current-session-title.txt b/packages/opencode/src/tool/set-current-session-title.txt index 5f2aa4f1a02..687ee0fc7d3 100644 --- a/packages/opencode/src/tool/set-current-session-title.txt +++ b/packages/opencode/src/tool/set-current-session-title.txt @@ -1,6 +1,6 @@ Sets the title of the current session. -Use this tool to give the session a descriptive title that reflects its purpose or content. The title will be immediately visible in the UI. +Use this tool to give the session a descriptive title that reflects its purpose or content, or when the user asks you to set the session's title. The title will be immediately visible in the UI. Usage notes: - The title must be between 1 and 255 characters