Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs-web/content/docs/user-dashboard-chat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ For integrators, the stored metadata uses `metadata.promptSuggestions` with `lab

Supported generic icon identifiers are `sparkles`, `search`, `edit`, `code`, `terminal`, `bug`, `check`, `play`, `refresh`, `settings`, `file`, `folder`, `git-branch`, `git-pull-request`, `database`, `shield`, `book-open`, `message-circle`, `list-checks`, `rocket`, `zap`, `lightbulb`, `clipboard`, `download`, `upload`, `eye`, `package`, `server`, `clock`, and `help-circle`.

Messages can also render external work references as rich cards without changing the stored transcript. The dashboard recognizes Jira issues, GitHub issues and pull requests, and GitLab issues and merge requests from explicit message metadata first, then from JSON-looking message bodies with the same fields. Recognized cards show the provider, key or number, title, status, safe external link, repository or project path, labels, assignee or author, and a short preview. Malformed JSON or unsupported providers remain ordinary markdown instead of being dropped.
Normal thread messages can also render external work references as rich cards without changing the stored transcript. The dashboard recognizes Jira issues, GitHub issues and pull requests, and GitLab issues and merge requests from explicit message metadata first, then from JSON-looking message bodies with the same fields. Recognized cards show the provider, key or number, title, status, safe external link, repository or project path, labels, assignee or author, and a short preview. Malformed JSON or unsupported providers remain ordinary markdown instead of being dropped.

## Compacting a thread

Expand All @@ -89,12 +89,12 @@ The **Invocations** tab is a structured log of server-created execution invocati

Use this for debugging provider runs and MCP client integrations, for example to inspect agent transcripts or see exactly what arguments your LLM is passing to tools like `manage_memory` or `manage_settings`.

Invocation transcripts use the same live sprint status card as thread messages when planning metadata links them to a sprint. This means a planning invocation and its related chat message should show consistent task progress without a separate refresh control. Parsed provider conversation turns stream into running invocation transcripts for provider-backed planning, QA review, dashboard/chat replies, CI repair, merge-conflict repair, memory remediation, setup, and task coding; text-only provider output is appended when the run completes.

Planning and QA self-reflection messages appear as structured cards instead of raw system text. Each card shows whether the reflection passed, needed improvement, or hit an error; the final decision; the attempt number; and each criterion's 5-star rating, numeric score, threshold, rationale, and improvement instructions when available.
Invocation transcripts use the same live sprint status card as thread messages when planning metadata links them to a sprint. Completed sprint-planning invocations append a final assistant summary with `metadata.widget_metadata.type = "planning_request"`, `status = "completed"`, and `metadata.executionPlan` for that exact sprint, including the sprint id, created task ids, and planned task titles. This means a planning invocation and its related chat message should show consistent task progress without a separate refresh control. Parsed provider conversation turns stream into running invocation transcripts for provider-backed planning, QA review, dashboard/chat replies, CI repair, merge-conflict repair, memory remediation, setup, and task coding; text-only provider output is appended when the run completes.

Invocation transcripts use the same external-reference cards as thread messages for recognized Jira, GitHub, and GitLab payloads, including JSON payloads that would otherwise appear as raw punctuation-heavy output. This keeps linked work readable while preserving the original backend metadata and message content.

Planning and QA self-reflection messages appear as structured cards instead of raw system text. Each card shows whether the reflection passed, needed improvement, or hit an error; the final decision; the attempt number; and each criterion's 5-star rating, numeric score, threshold, rationale, and improvement instructions when available.

## Posting messages

The composer at the bottom supports:
Expand Down
2 changes: 1 addition & 1 deletion docs-web/user/dashboard/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ The **Invocations** tab is a structured log of server-created execution invocati

Use this for debugging provider runs and MCP client integrations, for example to inspect agent transcripts or see exactly what arguments your LLM is passing to tools like `manage_memory` or `manage_settings`.

Invocation transcripts use the same live sprint status card as thread messages when planning metadata links them to a sprint. This means a planning invocation and its related chat message should show consistent task progress without a separate refresh control. Parsed provider conversation turns stream into running invocation transcripts for provider-backed planning, QA review, dashboard/chat replies, CI repair, merge-conflict repair, memory remediation, setup, and task coding; text-only provider output is appended when the run completes.
Invocation transcripts use the same live sprint status card as thread messages when planning metadata links them to a sprint. Completed sprint-planning invocations append a final assistant summary with `metadata.widget_metadata.type = "planning_request"`, `status = "completed"`, and `metadata.executionPlan` for that exact sprint, including the sprint id, created task ids, and planned task titles. This means a planning invocation and its related chat message should show consistent task progress without a separate refresh control. Parsed provider conversation turns stream into running invocation transcripts for provider-backed planning, QA review, dashboard/chat replies, CI repair, merge-conflict repair, memory remediation, setup, and task coding; text-only provider output is appended when the run completes.

Invocation transcripts use the same external-reference cards as thread messages for recognized Jira, GitHub, and GitLab payloads, including JSON payloads that would otherwise appear as raw punctuation-heavy output. This keeps linked work readable while preserving the original backend metadata and message content.

Expand Down
2 changes: 1 addition & 1 deletion docs/dashboard/design-system-chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The chat and invocation design system for the Code UX dashboard defines the layo
- **Tool Calls / Reasoning**: Presented as full-width, compact cards rather than standard bubbles to clearly differentiate them as structural operations or internal thoughts rather than user-facing dialogue.
- **Prompt suggestion tags**: Agent replies can append optional next-step prompt tags below the normal markdown body when message metadata includes `metadata.promptSuggestions`. These tags are adjuncts to the bubble, not replacements for the transcript; the markdown reply remains visible and readable even when suggestions are present. Selecting a tag populates and focuses the composer with that prompt so the user can review or edit it before sending. Tags must never auto-send a prompt from standard thread bubbles, and invocation transcripts remain read-only even if prompt-suggestion metadata is present.
- **Widgets**: specialized components (Routing, Planning, Container) embedded within the stream to provide rich status and execution context without cluttering the text transcript. They use a unified visual language (`ChatWidgetFrame`).
- **Planning status**: planning widgets prefer live project state when available. The view-model resolves a sprint from message metadata (`sprintId`, `sprintRunId`, or planning widget metadata) or from the active execution run, then combines project task records with the execution snapshot's dispatches and runtime events. The card shows sprint key/name, request/task/run materialization, a real `progressbar`, queued/completed counts, and per-task status labels. Both project task records and the execution snapshot must report loaded for the active project before the live card is rendered; otherwise the widget falls back to the generic planning card instead of rendering partial or invented progress.
- **Planning status**: planning widgets prefer live project state when available. The view-model resolves a sprint from message metadata (`sprintId`, `sprintRunId`, or planning widget metadata) or from the active execution run, then combines project task records with the execution snapshot's dispatches and runtime events. Completed sprint-planning invocations also append an assistant transcript message with `metadata.widget_metadata.type = "planning_request"`, `metadata.widget_metadata.status = "completed"`, and `metadata.executionPlan` containing the exact invocation's `projectId`, `sprintId`, sprint label, goal, created task IDs, and planned task summaries. The card shows sprint key/name, request/task/run materialization, a real `progressbar`, queued/completed counts, and per-task status labels. Both project task records and the execution snapshot must report loaded for the active project before the live card is rendered; otherwise the widget falls back to the generic planning card instead of rendering partial or invented progress.
- **Reasoning turns**: internal thinking output renders as a dedicated `ReasoningWidget`, not as a generic assistant bubble and not as a tool-call widget. It keeps the text plain and whitespace-preserving, adds provider/model/timing/token context in the header, and collapses long content behind an expand/collapse button with `aria-expanded` and `aria-controls`.
- **Self-reflection turns**: planning and QA reflection metadata renders as a dedicated `SelfReflectionWidget`, not as the raw system text. The widget summarizes the reflection purpose, attempt, pass/fail/error state, and final decision, then lists each criterion with a 5-star visual rating derived from the 1-10 score, the numeric score, threshold, textual pass/fail state, rationale, and improvement instructions when present. Star ratings expose semantic labels for screen readers, and pass/fail is always shown as text plus icon so it is not color-only.
- **External references**: Normal thread bubbles and invocation transcript bubbles render Jira issues, GitHub issues/pull requests, and GitLab issues/merge requests as `ExternalReferenceWidget` cards when the frontend view-model recognizes explicit metadata (`widget_metadata`, `externalReference`, `linkedIssue`, or top-level provider/kind/url fields) or a JSON-looking message body with the same fields. The widget shows provider, issue key or number, title, state/status, safe `http`/`https` outbound link, repository/project path, labels, assignee/author, and a short preview. Malformed JSON or unsupported providers stay on the normal markdown path so chat storage contracts and transcripts are not mutated.
Expand Down
19 changes: 19 additions & 0 deletions src/services/planning-agent-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { parsePlannedSprintReply, PlanningParseError } from "./planning-json-ext
import { extractJsonFromText } from "../domain/llm/json-extraction.js";
import type { PlannedSprintPayload, PlannedTaskDraft } from "../contracts/project-management-types.js";
import { persistPlannedTasks } from "./planning-task-persistence.js";
import { buildPlanningExecutionPlanMessage } from "./planning-execution-plan-message.js";
import { ProviderExecutionService, resolveEffectiveModel } from "./provider-execution-service.js";
import { StructuredAgentRequestService, type StructuredAgentRequestResult } from "./structured-agent-request-service.js";
import { ProviderInvocationCancelledError, StructuredProviderResponseService } from "./structured-provider-response-service.js";
Expand Down Expand Up @@ -457,6 +458,8 @@ export class PlanningAgentService {
if (Object.keys(sprintUpdate).length > 0) {
this.deps.projectManagementRepository.updateSprint(sprint.id, sprintUpdate);
}
const finalSprintName = sprintUpdate.name || sprint.name;
const finalSprintGoal = sprintUpdate.goal || sprint.goal;

const { createdTaskIds } = persistPlannedTasks(
projectId,
Expand All @@ -466,6 +469,22 @@ export class PlanningAgentService {
{ defaultAgentPresetId: manualCodingAgent?.id || null },
);

if (invocation && isExecutionInvocationActiveForFinalize(this.deps.executionRepository, invocation.id)) {
this.deps.executionRepository?.appendExecutionInvocationMessage(
invocation.id,
buildPlanningExecutionPlanMessage({
invocationId: invocation.id,
projectId,
sprintId,
sprintNumber: sprint.number,
sprintName: finalSprintName,
goal: finalSprintGoal,
tasks: payload.tasks,
createdTaskIds,
}),
);
}

const titles: string[] = [];
for (const t of payload.tasks) {
titles.push(t.title);
Expand Down
96 changes: 96 additions & 0 deletions src/services/planning-execution-plan-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { AppendExecutionInvocationMessageInput } from "../contracts/execution-types.js";
import type { PlannedTaskDraft, TaskExecutorType, TaskPriority } from "../contracts/project-management-types.js";

export interface PlanningExecutionPlanMessageInput {
invocationId: string;
projectId: string;
sprintId: string;
sprintNumber: number | null;
sprintName: string;
goal: string;
tasks: readonly PlannedTaskDraft[];
createdTaskIds: readonly string[];
}

interface PlanningExecutionPlanTaskSummary {
key: string;
title: string;
description: string;
priority: TaskPriority;
executorType: TaskExecutorType;
dependsOn: string[];
}

interface PlanningExecutionPlanMetadata {
invocationId: string;
projectId: string;
sprintId: string;
sprintNumber: number | null;
sprintName: string;
goal: string;
taskCount: number;
createdTaskIds: string[];
tasks: PlanningExecutionPlanTaskSummary[];
}

export function buildPlanningExecutionPlanMessage(
input: PlanningExecutionPlanMessageInput,
): AppendExecutionInvocationMessageInput {
const tasks = input.tasks.map((task) => ({
key: task.key,
title: task.title,
description: task.description,
priority: task.priority || "medium",
executorType: task.executorType || "auto",
dependsOn: [...(task.dependsOn || [])],
}));
const executionPlan: PlanningExecutionPlanMetadata = {
invocationId: input.invocationId,
projectId: input.projectId,
sprintId: input.sprintId,
sprintNumber: input.sprintNumber,
sprintName: input.sprintName,
goal: input.goal,
taskCount: tasks.length,
createdTaskIds: [...input.createdTaskIds],
tasks,
};

return {
role: "assistant",
contentMarkdown: buildPlanningExecutionPlanMarkdown(executionPlan),
metadata: {
widget_metadata: {
type: "planning_request",
status: "completed",
projectId: input.projectId,
sprintId: input.sprintId,
sprintNumber: input.sprintNumber,
sprintName: input.sprintName,
},
executionPlan,
},
};
}

function buildPlanningExecutionPlanMarkdown(plan: PlanningExecutionPlanMetadata): string {
const sprintLabel = plan.sprintNumber === null
? plan.sprintName
: `Sprint ${plan.sprintNumber} - ${plan.sprintName}`;
const lines = [
`## Execution Plan: ${sprintLabel}`,
"",
`Goal: ${plan.goal}`,
"",
`Planned ${plan.taskCount} ${plan.taskCount === 1 ? "task" : "tasks"}:`,
];

for (const task of plan.tasks) {
const dependencySummary = task.dependsOn.length > 0
? ` (depends on ${task.dependsOn.map((key) => `\`${key}\``).join(", ")})`
: "";
lines.push(`- \`${task.key}\` - ${task.title}${dependencySummary}`);
}

return lines.join("\n");
}
94 changes: 94 additions & 0 deletions tests/backend/services/planning-agent-service.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ describe("PlanningAgentService Integration", () => {
};
}

function findCompletedExecutionPlanMessage(
executionRepository: ExecutionRepository,
invocationId: string,
) {
return executionRepository.listExecutionInvocationMessages(invocationId).find((message) => {
const widgetMetadata = message.metadata?.widget_metadata as Record<string, unknown> | undefined;
return widgetMetadata?.type === "planning_request" && widgetMetadata.status === "completed";
});
}

function reflectionResult(score: number): string {
return JSON.stringify({
criteria: [
Expand Down Expand Up @@ -301,6 +311,90 @@ describe("PlanningAgentService Integration", () => {
expect(messages[0].contentMarkdown).toContain("Turn sprint goals into concrete executable tasks.");
});

it("persists sprint-specific execution plan metadata for separate planning invocations", async () => {
const {
projectRepository,
connectionRepository,
executionRepository,
settingsRepository,
syncService,
executionControlService,
project,
sprint: firstSprint,
} = await setupTestHarness({
name: "First Planning Sprint",
goal: "Plan the first sprint.",
});
const secondSprint = projectRepository.createSprint(project.id, {
name: "Second Planning Sprint",
goal: "Plan the second sprint.",
});

const service = new PlanningAgentService({
projectManagementRepository: projectRepository,
connectionChatRepository: connectionRepository,
executionRepository,
settingsRepository,
agentPresetSyncService: syncService,
executionControlService: executionControlService as any,
providerRunner: createPlanningTextProviderRunner([
JSON.stringify(planningProviderPayload("First sprint task")),
JSON.stringify(planningProviderPayload("Second sprint task")),
]),
});

const firstResult = await service.planSprint(project.id, firstSprint.id, {});
const secondResult = await service.planSprint(project.id, secondSprint.id, {});

const firstInvocation = executionRepository
.listExecutionInvocations({ projectId: project.id, sprintId: firstSprint.id })
.find((record) => record.sprintId === firstSprint.id);
const secondInvocation = executionRepository
.listExecutionInvocations({ projectId: project.id, sprintId: secondSprint.id })
.find((record) => record.sprintId === secondSprint.id);
expect(firstInvocation).toBeDefined();
expect(secondInvocation).toBeDefined();

const firstMessage = findCompletedExecutionPlanMessage(executionRepository, firstInvocation!.id);
const secondMessage = findCompletedExecutionPlanMessage(executionRepository, secondInvocation!.id);
const firstPlan = firstMessage?.metadata?.executionPlan as {
projectId: string;
sprintId: string;
sprintName: string;
taskCount: number;
createdTaskIds: string[];
tasks: Array<{ title: string }>;
} | undefined;
const secondPlan = secondMessage?.metadata?.executionPlan as {
projectId: string;
sprintId: string;
sprintName: string;
taskCount: number;
createdTaskIds: string[];
tasks: Array<{ title: string }>;
} | undefined;

expect(firstPlan).toMatchObject({
projectId: project.id,
sprintId: firstSprint.id,
sprintName: "First Planning Sprint",
taskCount: 1,
createdTaskIds: firstResult.createdTaskIds,
tasks: [{ title: "First sprint task" }],
});
expect(secondPlan).toMatchObject({
projectId: project.id,
sprintId: secondSprint.id,
sprintName: "Second Planning Sprint",
taskCount: 1,
createdTaskIds: secondResult.createdTaskIds,
tasks: [{ title: "Second sprint task" }],
});
expect(firstPlan?.sprintId).not.toBe(secondPlan?.sprintId);
expect(firstMessage?.contentMarkdown).toContain("- `T01` - First sprint task");
expect(secondMessage?.contentMarkdown).toContain("- `T01` - Second sprint task");
});

it("auto-starts after planning when self-reflection is disabled", async () => {
const {
projectRepository,
Expand Down
Loading