summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-06 13:30:31 -0600
committerAiden Cline <[email protected]>2026-01-06 13:30:39 -0600
commit5db78f20e9ae83c9708b9b0a490f10659e3ce229 (patch)
treeadaff901d12717045ef7b332e1e989c12e272e9e
parent5fc4472921f216318c5fc28d9391f93a7c936955 (diff)
downloadopencode-5db78f20e9ae83c9708b9b0a490f10659e3ce229.tar.gz
opencode-5db78f20e9ae83c9708b9b0a490f10659e3ce229.zip
core: fix title generation for subtask-only messages to extract actual user prompts instead of generic tool execution descriptions
-rw-r--r--packages/opencode/src/session/prompt.ts43
1 files changed, 22 insertions, 21 deletions
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 1f5fc9884..b669e9567 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -306,7 +306,6 @@ export namespace SessionPrompt {
session,
modelID: lastUser.model.modelID,
providerID: lastUser.model.providerID,
- message: msgs.find((m) => m.info.role === "user")!,
history: msgs,
})
@@ -1565,22 +1564,39 @@ export namespace SessionPrompt {
async function ensureTitle(input: {
session: Session.Info
- message: MessageV2.WithParts
history: MessageV2.WithParts[]
providerID: string
modelID: string
}) {
if (input.session.parentID) return
if (!Session.isDefaultTitle(input.session.title)) return
+
+ // Find first non-synthetic user message
+ const firstRealUserIdx = input.history.findIndex(
+ (m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic),
+ )
+ if (firstRealUserIdx === -1) return
+
const isFirst =
input.history.filter((m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic))
.length === 1
if (!isFirst) return
+
+ // Gather all messages up to and including the first real user message for context
+ // This includes any shell/subtask executions that preceded the user's first prompt
+ const contextMessages = input.history.slice(0, firstRealUserIdx + 1)
+ const firstRealUser = contextMessages[firstRealUserIdx]
+
+ // For subtask-only messages (from command invocations), extract the prompt directly
+ // since toModelMessage converts subtask parts to generic "The following tool was executed by the user"
+ const subtaskParts = firstRealUser.parts.filter((p) => p.type === "subtask") as MessageV2.SubtaskPart[]
+ const hasOnlySubtaskParts = subtaskParts.length > 0 && firstRealUser.parts.every((p) => p.type === "subtask")
+
const agent = await Agent.get("title")
if (!agent) return
const result = await LLM.stream({
agent,
- user: input.message.info as MessageV2.User,
+ user: firstRealUser.info as MessageV2.User,
system: [],
small: true,
tools: {},
@@ -1598,24 +1614,9 @@ export namespace SessionPrompt {
role: "user",
content: "Generate a title for this conversation:\n",
},
- ...MessageV2.toModelMessage([
- {
- info: {
- id: Identifier.ascending("message"),
- role: "user",
- sessionID: input.session.id,
- time: {
- created: Date.now(),
- },
- agent: input.message.info.role === "user" ? input.message.info.agent : await Agent.defaultAgent(),
- model: {
- providerID: input.providerID,
- modelID: input.modelID,
- },
- },
- parts: input.message.parts,
- },
- ]),
+ ...(hasOnlySubtaskParts
+ ? [{ role: "user" as const, content: subtaskParts.map((p) => p.prompt).join("\n") }]
+ : MessageV2.toModelMessage(contextMessages)),
],
})
const text = await result.text.catch((err) => log.error("failed to generate title", { error: err }))