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:
gsxdsm
2026-03-29 17:40:42 -07:00
parent 5214cc0657
commit 80abb826ee
15 changed files with 960 additions and 5 deletions

View File

@@ -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({

View File

@@ -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." : ""}