Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fc62801
feat: add set_current_session_title tool
ariane-emory Jan 15, 2026
9e0091c
fix: add required parameter to Session.create() in tests
ariane-emory Jan 15, 2026
3704314
fix: prevent race condition in session title generation
ariane-emory Jan 15, 2026
dc49a2a
tweak: minor revision to tool description for set_current_session_tit…
ariane-emory Jan 15, 2026
fa56690
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 16, 2026
f55ad9d
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 17, 2026
fd0cd7f
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 17, 2026
e3efb5a
Merge dev into feat/set-session-title
ariane-emory Jan 19, 2026
163a57b
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 19, 2026
aeb3603
Merge dev into feat/set-session-title: resolve conflict in title sett…
ariane-emory Jan 22, 2026
851bb78
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 23, 2026
10f10db
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 25, 2026
2348389
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 25, 2026
c143107
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 26, 2026
f7edb81
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 27, 2026
74f458b
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 29, 2026
dbaeab3
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 29, 2026
d50ff79
Merge branch 'dev' into feat/set-session-title
ariane-emory Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1815,10 +1815,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
.find((line) => line.length > 0)
if (!cleaned) return

const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
draft.title = title
},
{ touch: false },
)
const title = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
// Only set if title is still default (wasn't changed by tool during LLM call)
if (Session.isDefaultTitle(draft.title)) draft.title = title
}, { touch: false })
}
}
2 changes: 2 additions & 0 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
import { ApplyPatchTool } from "./apply_patch"

export namespace ToolRegistry {
Expand Down Expand Up @@ -111,6 +112,7 @@ export namespace ToolRegistry {
WebFetchTool,
TodoWriteTool,
TodoReadTool,
SetCurrentSessionTitleTool,
WebSearchTool,
CodeSearchTool,
SkillTool,
Expand Down
36 changes: 36 additions & 0 deletions packages/opencode/src/tool/set-current-session-title.ts
Original file line number Diff line number Diff line change
@@ -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,
},
}
},
})
8 changes: 8 additions & 0 deletions packages/opencode/src/tool/set-current-session-title.txt
Original file line number Diff line number Diff line change
@@ -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, 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
- 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
161 changes: 161 additions & 0 deletions packages/opencode/test/tool/set-current-session-title.test.ts
Original file line number Diff line number Diff line change
@@ -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")
},
})
})
})