fix(session): title user /skill invocations from name and args

Session titles ignored /skill:<name> <args> because the invocation is a
custom skill-prompt, not a user turn. Feed the chip or reconstructed
/skill line into first-title generation and replan context, never the
expanded SKILL.md body.
This commit is contained in:
qiyi71w
2026-08-19 00:59:31 +08:00
parent 8500092296
commit ecd800ef8f
6 changed files with 185 additions and 2 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed session-title generation ignoring user `/skill:<name>` invocations, so titles now see the skill name and args instead of only later assistant text.
## [17.3.7] - 2026-08-17
### Changed
@@ -308,6 +308,7 @@ import {
sanitizeAssistantForReparentedHistory,
USER_INTERRUPT_LABEL,
} from "./messages";
import { skillPromptTitleInput } from "./skill-title-input";
import { ModelControls, type ModelControlsHost } from "./model-controls";
import { isPrewalkPlanNudge, PrewalkCoordinator, type PrewalkCoordinatorHost } from "./prewalk";
import {
@@ -5466,11 +5467,20 @@ export class AgentSession {
let keywordNotices: CustomMessage[] = [];
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE && message.attribution === "user") {
const details = message.details;
let skillName: string | undefined;
let skillArgs = "";
if (details && typeof details === "object" && "args" in details && typeof details.args === "string") {
skillArgs = details.args;
if (details && typeof details === "object") {
if ("name" in details && typeof details.name === "string") skillName = details.name;
if ("args" in details && typeof details.args === "string") skillArgs = details.args;
}
keywordNotices = this.#createMagicKeywordNotices(skillArgs);
this.maybeStartTitleGeneration(
skillPromptTitleInput({
name: skillName,
args: skillArgs,
queueChipText: options?.queueChipText,
}),
);
}
if (options?.queueOnly) {
@@ -38,6 +38,7 @@ export {
import type { OutputMeta } from "../tools/output-meta";
import { formatOutputNotice } from "../tools/output-meta";
import { titleTextFromSkillPrompt } from "./skill-title-input";
export const SKILL_PROMPT_MESSAGE_TYPE = "skill-prompt";
export const LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE = "lsp-late-diagnostic";
@@ -163,6 +164,11 @@ function thinkingFromContent(content: unknown): string {
}
function titleConversationTurnFromMessage(message: AgentMessage): TitleConversationTurn | undefined {
if (message.role === "custom") {
const text = titleTextFromSkillPrompt(message);
if (!text) return undefined;
return { role: "user", text };
}
if (message.role !== "user" && message.role !== "assistant") return undefined;
const text = textFromContent(message.content);
const thinking = message.role === "assistant" ? thinkingFromContent(message.content) : undefined;
@@ -0,0 +1,36 @@
/** Compact title-model input for a user-invoked `/skill:<name>` prompt. */
export function skillPromptTitleInput(input: {
name?: string;
args?: string;
queueChipText?: string;
}): string {
const chip = input.queueChipText?.trim();
if (chip) return chip;
const name = input.name?.trim();
const args = input.args?.trim();
if (name && args) return `/skill:${name} ${args}`;
if (name) return `/skill:${name}`;
return args ?? "";
}
/** Title text for a persisted skill-prompt custom message. Never the expanded SKILL.md body. */
export function titleTextFromSkillPrompt(message: {
role: string;
customType?: string;
attribution?: string;
details?: unknown;
}): string | undefined {
if (message.role !== "custom" || message.customType !== "skill-prompt" || message.attribution !== "user") {
return undefined;
}
let name: string | undefined;
let args: string | undefined;
let queueChipText: string | undefined;
if (message.details && typeof message.details === "object") {
const details = message.details as Record<string, unknown>;
if (typeof details.name === "string") name = details.name;
if (typeof details.args === "string") args = details.args;
if (typeof details.__queueChipText === "string") queueChipText = details.__queueChipText;
}
return skillPromptTitleInput({ name, args, queueChipText }) || undefined;
}
@@ -3,6 +3,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import {
type CustomMessage,
buildReplanTitleContext,
convertToLlm,
INTERRUPTED_THINKING_MESSAGE_TYPE,
replaceLlmImagesWithText,
@@ -281,3 +282,58 @@ describe("replaceLlmImagesWithText", () => {
expect(replaceLlmImagesWithText(converted, "[image omitted]")).toBe(converted);
});
});
describe("buildReplanTitleContext", () => {
it("titles a user skill invocation from skill args, not the expanded skill body", () => {
const skill: CustomMessage<SkillPromptDetails> = {
role: "custom",
customType: SKILL_PROMPT_MESSAGE_TYPE,
content:
"[IMPORTANT: User invoked the \"implement\" skill]\n\nImplement the work described by the user.\n\nUse this skill.",
display: true,
details: {
name: "implement",
path: "/tmp/implement/SKILL.md",
args: "issues/07-manual-llm.md 创建临时工作树实现",
lineCount: 20,
},
attribution: "user",
timestamp: 1,
};
const context = buildReplanTitleContext([skill, settledAssistant("先读 implement 技能、ticket 07")]);
expect(context).toContain("07-manual-llm.md");
expect(context).toContain("ticket 07");
expect(context).not.toContain("Use this skill.");
expect(context).not.toContain("IMPORTANT");
});
it("does not feed an autoloaded skill prompt into title context", () => {
const skill = customMessage(SKILL_PROMPT_MESSAGE_TYPE, "agent");
skill.details = { ...skill.details, args: "issues/07-manual-llm.md" };
expect(buildReplanTitleContext([skill])).toBe("");
});
it("prefers the operator /skill chip over persisted name and args", () => {
const skill: CustomMessage<SkillPromptDetails> = {
role: "custom",
customType: SKILL_PROMPT_MESSAGE_TYPE,
content: "Use this skill.",
display: true,
details: {
name: "implement",
path: "/tmp/implement/SKILL.md",
args: "issues/07-manual-llm.md",
lineCount: 1,
__queueChipText: "/skill:implement issues/08-app-settings.md",
},
attribution: "user",
timestamp: 1,
};
const context = buildReplanTitleContext([skill]);
expect(context).toContain("08-app-settings.md");
expect(context).not.toContain("07-manual-llm.md");
});
});
@@ -0,0 +1,71 @@
import { describe, expect, it } from "bun:test";
import { formatTitleConversationContext } from "../../src/tiny/message-preproc";
import { skillPromptTitleInput, titleTextFromSkillPrompt } from "../../src/session/skill-title-input";
describe("skillPromptTitleInput", () => {
it("uses the operator /skill chip when present", () => {
expect(
skillPromptTitleInput({
name: "implement",
args: "issues/07-manual-llm.md",
queueChipText: "/skill:implement issues/08-app-settings.md",
}),
).toBe("/skill:implement issues/08-app-settings.md");
});
it("reconstructs /skill:name args when the chip was stripped", () => {
expect(skillPromptTitleInput({ name: "implement", args: "issues/07-manual-llm.md 创建临时工作树实现" })).toBe(
"/skill:implement issues/07-manual-llm.md 创建临时工作树实现",
);
});
});
describe("titleTextFromSkillPrompt", () => {
it("reads args from a user skill-prompt and ignores expanded body fields", () => {
const text = titleTextFromSkillPrompt({
role: "custom",
customType: "skill-prompt",
attribution: "user",
details: {
name: "implement",
path: "/tmp/implement/SKILL.md",
args: "issues/07-manual-llm.md",
lineCount: 20,
},
});
expect(text).toBe("/skill:implement issues/07-manual-llm.md");
});
it("ignores autoloaded skill prompts", () => {
expect(
titleTextFromSkillPrompt({
role: "custom",
customType: "skill-prompt",
attribution: "agent",
details: { name: "implement", args: "issues/07-manual-llm.md" },
}),
).toBeUndefined();
});
});
describe("replan title envelope", () => {
it("puts skill args in the user turn and keeps assistant text", () => {
const text = titleTextFromSkillPrompt({
role: "custom",
customType: "skill-prompt",
attribution: "user",
details: {
name: "implement",
args: "issues/07-manual-llm.md 创建临时工作树实现",
},
});
const context = formatTitleConversationContext([
{ role: "user", text },
{ role: "assistant", text: "先读 implement 技能、ticket 07" },
]);
expect(context).toContain("07-manual-llm.md");
expect(context).toContain("ticket 07");
expect(context).not.toContain("SKILL.md");
});
});