feat(KB-003): add task steering feature with UI and prompt injection
- Add SteeringComment type to core types for user steering input - Add addSteeringComment method to TaskStore with persistence - Add POST /api/tasks/:id/steer endpoint for adding steering comments - Add frontend API client and SteeringTab UI component - Inject steering comments into execution prompt for agent guidance - Add comprehensive tests for store, API, UI, and executor components - Include changeset for patch release
This commit is contained in:
@@ -994,6 +994,83 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(result).not.toContain("## Project Commands");
|
||||
});
|
||||
|
||||
it("includes Steering Comments section when steeringComments has entries", () => {
|
||||
const task = createMockTaskDetail({
|
||||
steeringComments: [
|
||||
{
|
||||
id: "1",
|
||||
text: "Please handle the edge case",
|
||||
createdAt: new Date().toISOString(),
|
||||
author: "user" as const,
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
expect(result).toContain("## Steering Comments");
|
||||
expect(result).toContain("**user**");
|
||||
expect(result).toContain("> Please handle the edge case");
|
||||
expect(result).toContain("The following steering comments were added by the user");
|
||||
});
|
||||
|
||||
it("formats multiple steering comments correctly", () => {
|
||||
const now = new Date();
|
||||
const task = createMockTaskDetail({
|
||||
steeringComments: [
|
||||
{
|
||||
id: "1",
|
||||
text: "First comment",
|
||||
createdAt: new Date(now.getTime() - 60000).toISOString(), // 1 minute ago
|
||||
author: "user" as const,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
text: "Second comment",
|
||||
createdAt: now.toISOString(),
|
||||
author: "agent" as const,
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
expect(result).toContain("**user**");
|
||||
expect(result).toContain("**agent**");
|
||||
expect(result).toContain("> First comment");
|
||||
expect(result).toContain("> Second comment");
|
||||
});
|
||||
|
||||
it("omits Steering Comments section when steeringComments is empty", () => {
|
||||
const task = createMockTaskDetail({ steeringComments: [] });
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
expect(result).not.toContain("## Steering Comments");
|
||||
});
|
||||
|
||||
it("omits Steering Comments section when steeringComments is undefined", () => {
|
||||
const task = createMockTaskDetail();
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
expect(result).not.toContain("## Steering Comments");
|
||||
});
|
||||
|
||||
it("includes only the 10 most recent steering comments", () => {
|
||||
const steeringComments = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: `${i}`,
|
||||
text: `Comment ${i}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
author: "user" as const,
|
||||
}));
|
||||
|
||||
const task = createMockTaskDetail({ steeringComments });
|
||||
const result = buildExecutionPrompt(task);
|
||||
|
||||
// Should include comments 5-14 (the 10 most recent), not 0-4
|
||||
expect(result).toContain("> Comment 5");
|
||||
expect(result).toContain("> Comment 14");
|
||||
expect(result).not.toContain("> Comment 0");
|
||||
expect(result).not.toContain("> Comment 4");
|
||||
});
|
||||
|
||||
it("passes settings to buildExecutionPrompt in TaskExecutor.execute()", async () => {
|
||||
const store = createMockStore();
|
||||
store.getSettings.mockResolvedValue({
|
||||
|
||||
@@ -959,6 +959,25 @@ export class TaskExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp for display in steering comments.
|
||||
* Returns relative time for recent comments, absolute date for older ones.
|
||||
*/
|
||||
function formatTimestamp(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
|
||||
if (diffMin < 1) return "just now";
|
||||
if (diffMin < 60) return `${diffMin}m ago`;
|
||||
if (diffHr < 24) return `${diffHr}h ago`;
|
||||
if (diffDay < 7) return `${diffDay}d ago`;
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
// Project commands are injected here (for reliability) and also in the PROMPT.md (by triage).
|
||||
// This ensures the executor agent always sees the authoritative commands from settings,
|
||||
// even if the PROMPT.md was written manually or before commands were configured.
|
||||
@@ -1019,6 +1038,26 @@ git log --oneline
|
||||
commandsSection = "\n" + lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
// Build steering comments section (last 10 comments only to avoid context bloat)
|
||||
let steeringSection = "";
|
||||
if (task.steeringComments && task.steeringComments.length > 0) {
|
||||
const recentComments = [...task.steeringComments].slice(-10);
|
||||
const lines = [
|
||||
"",
|
||||
"## Steering Comments",
|
||||
"",
|
||||
"The following steering comments were added by the user during execution. Consider adjusting your approach or replanning remaining steps based on this feedback.",
|
||||
"",
|
||||
];
|
||||
for (const comment of recentComments) {
|
||||
const timestamp = formatTimestamp(comment.createdAt);
|
||||
lines.push(`**${comment.author}** — ${timestamp}`);
|
||||
lines.push(`> ${comment.text}`);
|
||||
lines.push("");
|
||||
}
|
||||
steeringSection = lines.join("\n");
|
||||
}
|
||||
|
||||
return `Execute this task.
|
||||
|
||||
## Task: ${task.id}
|
||||
@@ -1028,7 +1067,7 @@ ${task.dependencies.length > 0 ? `Dependencies: ${task.dependencies.join(", ")}`
|
||||
## PROMPT.md
|
||||
|
||||
${task.prompt}
|
||||
${attachmentsSection}${commandsSection}${progressSection}
|
||||
${attachmentsSection}${commandsSection}${progressSection}${steeringSection}
|
||||
## Review level: ${reviewLevel}
|
||||
|
||||
${reviewLevel === 0 ? "No reviews required. Implement directly." : ""}
|
||||
|
||||
Reference in New Issue
Block a user