a2d086efb8
* feat(web): @-mention tasks in the chat composer Lets users type @<taskname> in the chat input to reference other tasks. The picked task is inserted as a chip and its metadata (id, workflow_id, step, state) is appended to the prompt as a <kandev-system> block so the agent can act on it via the kandev MCP tools. Extends the existing TipTap mention extension with a new "task" kind; task data comes from the kanban store (current workflow + visited kanbanMulti snapshots), so no backend changes are needed. * fix(web): harden task @-mentions from review feedback (#963) - Sanitize interpolated task strings (title, id, workflow_id, step, state) before they land in the <kandev-system> block so a hostile task title (e.g. from Jira/Linear sync) can't terminate the block early via embedded newlines or angle brackets. - Expand the task-mentions context block on the onSend code path too (used by preview-session-tabs), which was silently dropping the agent-facing task context when chips were present in the editor. - Deduplicate task mentions by taskId in getTaskMentions so mentioning the same task twice no longer produces redundant lines. - Skip stale state.kanban.tasks entries whose workflowStepId isn't in the current workflow's steps to prevent tagging them with the wrong workflow_id. * fix(web): skip task mentions missing workflowId or workflowStepId (#963) CodeRabbit flagged that getTaskMentions was filling missing workflowId / workflowStepId with empty strings, propagating malformed metadata into the prompt block. Skip such nodes during extraction. --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
79 lines
2.5 KiB
TypeScript
79 lines
2.5 KiB
TypeScript
import type { MentionItem } from "@/hooks/use-inline-mention";
|
|
import type { AppState } from "@/lib/state/store";
|
|
|
|
type TaskLike = AppState["kanban"]["tasks"][number];
|
|
|
|
function buildWorkflowNameMap(state: AppState): Map<string, string> {
|
|
const m = new Map<string, string>();
|
|
for (const w of state.workflows.items) m.set(w.id, w.name);
|
|
for (const [wfId, snap] of Object.entries(state.kanbanMulti.snapshots)) {
|
|
if (!m.has(wfId) && snap.workflowName) m.set(wfId, snap.workflowName);
|
|
}
|
|
return m;
|
|
}
|
|
|
|
function buildStepTitleMap(state: AppState): Map<string, string> {
|
|
const m = new Map<string, string>();
|
|
for (const s of state.kanban.steps) m.set(s.id, s.title);
|
|
for (const snap of Object.values(state.kanbanMulti.snapshots)) {
|
|
for (const s of snap.steps ?? []) m.set(s.id, s.title);
|
|
}
|
|
return m;
|
|
}
|
|
|
|
function toMentionItem(
|
|
task: TaskLike,
|
|
workflowId: string,
|
|
workflowName: string,
|
|
stepTitle: string,
|
|
): MentionItem {
|
|
return {
|
|
id: `task:${task.id}`,
|
|
kind: "task",
|
|
label: task.title,
|
|
description: `${workflowName} · ${stepTitle}`,
|
|
task: {
|
|
taskId: task.id,
|
|
title: task.title,
|
|
workflowId,
|
|
workflowStepId: task.workflowStepId,
|
|
state: task.state ?? null,
|
|
},
|
|
onSelect: () => {},
|
|
};
|
|
}
|
|
|
|
export function buildTaskMentionItems(
|
|
state: AppState,
|
|
currentTaskId: string | null,
|
|
): MentionItem[] {
|
|
const items: MentionItem[] = [];
|
|
const seen = new Set<string>();
|
|
const workflowNameById = buildWorkflowNameMap(state);
|
|
const stepTitleById = buildStepTitleMap(state);
|
|
|
|
const addTask = (task: TaskLike, workflowId: string) => {
|
|
if (task.id === currentTaskId || seen.has(task.id)) return;
|
|
seen.add(task.id);
|
|
const workflowName = workflowNameById.get(workflowId) ?? "Workflow";
|
|
const stepTitle = stepTitleById.get(task.workflowStepId) ?? "Step";
|
|
items.push(toMentionItem(task, workflowId, workflowName, stepTitle));
|
|
};
|
|
|
|
if (state.kanban.workflowId) {
|
|
// Guard against stale tasks left over from a previous workflow: only add
|
|
// entries whose workflowStepId belongs to the current workflow's steps,
|
|
// since we tag them with state.kanban.workflowId here.
|
|
const activeStepIds = new Set(state.kanban.steps.map((s) => s.id));
|
|
for (const t of state.kanban.tasks) {
|
|
if (activeStepIds.size > 0 && !activeStepIds.has(t.workflowStepId)) continue;
|
|
addTask(t, state.kanban.workflowId);
|
|
}
|
|
}
|
|
for (const [wfId, snap] of Object.entries(state.kanbanMulti.snapshots)) {
|
|
for (const t of snap.tasks) addTask(t, wfId);
|
|
}
|
|
|
|
return items;
|
|
}
|