Merge pull request #1607 from code-yeongyu/fix/358-skill-description-truncation
fix: use character limit instead of sentence split for skill description (#358)
This commit is contained in:
@@ -8,21 +8,22 @@
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import { formatCustomSkillsBlock, type AvailableAgent, type AvailableSkill } from "../dynamic-agent-prompt-builder"
|
||||
import { DEFAULT_CATEGORIES, CATEGORY_DESCRIPTIONS } from "../../tools/delegate-task/constants"
|
||||
import { truncateDescription } from "../../shared/truncate-description"
|
||||
|
||||
export const getCategoryDescription = (name: string, userCategories?: Record<string, CategoryConfig>) =>
|
||||
userCategories?.[name]?.description ?? CATEGORY_DESCRIPTIONS[name] ?? "General tasks"
|
||||
|
||||
export function buildAgentSelectionSection(agents: AvailableAgent[]): string {
|
||||
if (agents.length === 0) {
|
||||
return `##### Option B: Use AGENT directly (for specialized experts)
|
||||
if (agents.length === 0) {
|
||||
return `##### Option B: Use AGENT directly (for specialized experts)
|
||||
|
||||
No agents available.`
|
||||
}
|
||||
No agents available.`
|
||||
}
|
||||
|
||||
const rows = agents.map((a) => {
|
||||
const shortDesc = a.description.split(".")[0] || a.description
|
||||
return `| \`${a.name}\` | ${shortDesc} |`
|
||||
})
|
||||
const rows = agents.map((a) => {
|
||||
const shortDesc = truncateDescription(a.description)
|
||||
return `| \`${a.name}\` | ${shortDesc} |`
|
||||
})
|
||||
|
||||
return `##### Option B: Use AGENT directly (for specialized experts)
|
||||
|
||||
@@ -59,16 +60,16 @@ export function buildSkillsSection(skills: AvailableSkill[]): string {
|
||||
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const shortDesc = s.description.split(".")[0] || s.description
|
||||
return `| \`${s.name}\` | ${shortDesc} |`
|
||||
})
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const shortDesc = truncateDescription(s.description)
|
||||
return `| \`${s.name}\` | ${shortDesc} |`
|
||||
})
|
||||
|
||||
const customRows = customSkills.map((s) => {
|
||||
const shortDesc = s.description.split(".")[0] || s.description
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${shortDesc} | ${source} |`
|
||||
})
|
||||
const customRows = customSkills.map((s) => {
|
||||
const shortDesc = truncateDescription(s.description)
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${shortDesc} | ${source} |`
|
||||
})
|
||||
|
||||
const customSkillBlock = formatCustomSkillsBlock(customRows, customSkills, "**")
|
||||
|
||||
@@ -121,10 +122,10 @@ export function buildDecisionMatrix(agents: AvailableAgent[], userCategories?: R
|
||||
`| ${getCategoryDescription(name, userCategories)} | \`category="${name}", load_skills=[...]\` |`
|
||||
)
|
||||
|
||||
const agentRows = agents.map((a) => {
|
||||
const shortDesc = a.description.split(".")[0] || a.description
|
||||
return `| ${shortDesc} | \`agent="${a.name}"\` |`
|
||||
})
|
||||
const agentRows = agents.map((a) => {
|
||||
const shortDesc = truncateDescription(a.description)
|
||||
return `| ${shortDesc} | \`agent="${a.name}"\` |`
|
||||
})
|
||||
|
||||
return `##### Decision Matrix
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentPromptMetadata, BuiltinAgentName } from "./types"
|
||||
import { truncateDescription } from "../shared/truncate-description"
|
||||
|
||||
export interface AvailableAgent {
|
||||
name: BuiltinAgentName
|
||||
@@ -205,16 +206,16 @@ export function buildCategorySkillsDelegationGuide(categories: AvailableCategory
|
||||
const builtinSkills = skills.filter((s) => s.location === "plugin")
|
||||
const customSkills = skills.filter((s) => s.location !== "plugin")
|
||||
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const desc = s.description.split(".")[0] || s.description
|
||||
return `| \`${s.name}\` | ${desc} |`
|
||||
})
|
||||
const builtinRows = builtinSkills.map((s) => {
|
||||
const desc = truncateDescription(s.description)
|
||||
return `| \`${s.name}\` | ${desc} |`
|
||||
})
|
||||
|
||||
const customRows = customSkills.map((s) => {
|
||||
const desc = s.description.split(".")[0] || s.description
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${desc} | ${source} |`
|
||||
})
|
||||
const customRows = customSkills.map((s) => {
|
||||
const desc = truncateDescription(s.description)
|
||||
const source = s.location === "project" ? "project" : "user"
|
||||
return `| \`${s.name}\` | ${desc} | ${source} |`
|
||||
})
|
||||
|
||||
const customSkillBlock = formatCustomSkillsBlock(customRows, customSkills)
|
||||
|
||||
|
||||
@@ -42,3 +42,4 @@ export * from "./model-suggestion-retry"
|
||||
export * from "./opencode-server-auth"
|
||||
export * from "./port-utils"
|
||||
export * from "./safe-create-hook"
|
||||
export * from "./truncate-description"
|
||||
|
||||
104
src/shared/truncate-description.test.ts
Normal file
104
src/shared/truncate-description.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { truncateDescription } from "./truncate-description"
|
||||
|
||||
describe("truncateDescription", () => {
|
||||
it("returns description unchanged when under max length", () => {
|
||||
// given
|
||||
const description = "This is a short description"
|
||||
|
||||
// when
|
||||
const result = truncateDescription(description)
|
||||
|
||||
// then
|
||||
expect(result).toBe(description)
|
||||
})
|
||||
|
||||
it("truncates to 120 characters by default and appends ellipsis", () => {
|
||||
// given
|
||||
const description = "This is a very long description that exceeds the default maximum length of 120 characters and should be truncated with an ellipsis at the end"
|
||||
|
||||
// when
|
||||
const result = truncateDescription(description)
|
||||
|
||||
// then
|
||||
expect(result.length).toBe(120) // 117 chars + "..."
|
||||
expect(result).toEndWith("...")
|
||||
expect(result).toBe(description.slice(0, 117) + "...")
|
||||
})
|
||||
|
||||
it("respects custom max length parameter", () => {
|
||||
// given
|
||||
const description = "This is a description that is longer than fifty characters"
|
||||
const maxLength = 50
|
||||
|
||||
// when
|
||||
const result = truncateDescription(description, maxLength)
|
||||
|
||||
// then
|
||||
expect(result.length).toBe(50) // 47 chars + "..."
|
||||
expect(result).toEndWith("...")
|
||||
expect(result).toBe(description.slice(0, 47) + "...")
|
||||
})
|
||||
|
||||
it("handles empty string", () => {
|
||||
// given
|
||||
const description = ""
|
||||
|
||||
// when
|
||||
const result = truncateDescription(description)
|
||||
|
||||
// then
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
it("handles exactly max length without truncation", () => {
|
||||
// given
|
||||
const description = "a".repeat(120)
|
||||
|
||||
// when
|
||||
const result = truncateDescription(description)
|
||||
|
||||
// then
|
||||
expect(result).toBe(description)
|
||||
expect(result).not.toEndWith("...")
|
||||
})
|
||||
|
||||
it("handles description with periods correctly", () => {
|
||||
// given
|
||||
const description = "First sentence. Second sentence. Third sentence that is very long and continues beyond the normal truncation point with even more text to ensure it exceeds 120 characters."
|
||||
|
||||
// when
|
||||
const result = truncateDescription(description)
|
||||
|
||||
// then
|
||||
expect(result.length).toBe(120)
|
||||
expect(result).toContain("First sentence. Second sentence.")
|
||||
expect(result).toEndWith("...")
|
||||
})
|
||||
|
||||
it("handles description with URLs correctly", () => {
|
||||
// given
|
||||
const description = "Check out https://example.com/very/long/path/that/contains/many/segments for more information about this feature and its capabilities"
|
||||
|
||||
// when
|
||||
const result = truncateDescription(description)
|
||||
|
||||
// then
|
||||
expect(result.length).toBe(120)
|
||||
expect(result).toStartWith("Check out https://example.com")
|
||||
expect(result).toEndWith("...")
|
||||
})
|
||||
|
||||
it("handles description with version numbers correctly", () => {
|
||||
// given
|
||||
const description = "Version 1.2.3 of the library includes many improvements and bug fixes that make it more stable and performant with additional enhancements"
|
||||
|
||||
// when
|
||||
const result = truncateDescription(description)
|
||||
|
||||
// then
|
||||
expect(result.length).toBe(120)
|
||||
expect(result).toStartWith("Version 1.2.3")
|
||||
expect(result).toEndWith("...")
|
||||
})
|
||||
})
|
||||
11
src/shared/truncate-description.ts
Normal file
11
src/shared/truncate-description.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function truncateDescription(description: string, maxLength: number = 120): string {
|
||||
if (!description) {
|
||||
return description
|
||||
}
|
||||
|
||||
if (description.length <= maxLength) {
|
||||
return description
|
||||
}
|
||||
|
||||
return description.slice(0, maxLength - 3) + "..."
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { CategoryConfig } from "../../config/schema"
|
||||
import type {
|
||||
AvailableCategory,
|
||||
AvailableSkill,
|
||||
} from "../../agents/dynamic-agent-prompt-builder"
|
||||
AvailableCategory,
|
||||
AvailableSkill,
|
||||
} from "../../agents/dynamic-agent-prompt-builder"
|
||||
import { truncateDescription } from "../../shared/truncate-description"
|
||||
|
||||
export const VISUAL_CATEGORY_PROMPT_APPEND = `<Category_Context>
|
||||
You are working on VISUAL/UI tasks.
|
||||
@@ -492,13 +493,12 @@ function renderPlanAgentCategoryRows(categories: AvailableCategory[]): string[]
|
||||
}
|
||||
|
||||
function renderPlanAgentSkillRows(skills: AvailableSkill[]): string[] {
|
||||
const sorted = [...skills].sort((a, b) => a.name.localeCompare(b.name))
|
||||
return sorted.map((skill) => {
|
||||
const firstSentence = skill.description.split(".")[0] || skill.description
|
||||
const domain = firstSentence.trim() || skill.name
|
||||
return `| \`${skill.name}\` | ${domain} |`
|
||||
})
|
||||
}
|
||||
const sorted = [...skills].sort((a, b) => a.name.localeCompare(b.name))
|
||||
return sorted.map((skill) => {
|
||||
const domain = truncateDescription(skill.description).trim() || skill.name
|
||||
return `| \`${skill.name}\` | ${domain} |`
|
||||
})
|
||||
}
|
||||
|
||||
export function buildPlanAgentSkillsSection(
|
||||
categories: AvailableCategory[] = [],
|
||||
|
||||
Reference in New Issue
Block a user