Files
kandev/apps/web/components/task/chat-context-items.ts
T
2026-02-23 00:16:37 +00:00

214 lines
6.6 KiB
TypeScript

import type { ContextFile } from "@/lib/state/context-files-store";
import { getFileName } from "@/lib/utils/file-path";
import type { ContextItem } from "@/lib/types/context";
import type { DiffComment } from "@/lib/diff/types";
import type { PlanComment, PRFeedbackComment } from "@/lib/state/slices/comments";
const PLAN_CONTEXT_PATH = "plan:context";
export type BuildContextItemsParams = {
planContextEnabled: boolean;
contextFiles: ContextFile[];
resolvedSessionId: string | null;
removeContextFile: (sid: string, path: string) => void;
unpinFile: (sid: string, path: string) => void;
addPlan: () => void;
promptsMap: Map<string, { content: string }>;
onOpenFile?: (path: string) => void;
pendingCommentsByFile: Record<string, DiffComment[]>;
handleRemoveCommentFile: (filePath: string) => void;
handleRemoveComment: (commentId: string) => void;
onOpenFileAtLine?: (filePath: string) => void;
planComments: PlanComment[];
handleClearPlanComments: () => void;
pendingPRFeedback: PRFeedbackComment[];
handleRemovePRFeedback: (commentId: string) => void;
handleClearPRFeedback: () => void;
taskId: string | null;
};
type FileItemHelpers = {
sid: string | null;
removeContextFile: (sid: string, path: string) => void;
unpinFile: (sid: string, path: string) => void;
};
function makeRemoveHandler(
sid: string | null,
path: string,
removeContextFile: (sid: string, path: string) => void,
) {
return sid ? () => removeContextFile(sid, path) : undefined;
}
function makeUnpinHandler(
pinned: boolean | undefined,
sid: string | null,
path: string,
unpinFile: (sid: string, path: string) => void,
) {
return pinned && sid ? () => unpinFile(sid, path) : undefined;
}
function buildPlanContextItem(params: BuildContextItemsParams): ContextItem | null {
if (!params.planContextEnabled) return null;
const {
contextFiles,
resolvedSessionId: sid,
removeContextFile,
unpinFile,
addPlan,
taskId,
} = params;
const planFile = contextFiles.find((f) => f.path === PLAN_CONTEXT_PATH);
return {
kind: "plan",
id: PLAN_CONTEXT_PATH,
label: "Plan",
taskId: taskId ?? undefined,
pinned: planFile?.pinned,
onRemove: sid ? () => removeContextFile(sid, PLAN_CONTEXT_PATH) : undefined,
onUnpin: planFile?.pinned && sid ? () => unpinFile(sid, PLAN_CONTEXT_PATH) : undefined,
onOpen: addPlan,
};
}
function buildPromptContextItem(
f: ContextFile,
helpers: FileItemHelpers,
promptsMap: Map<string, { content: string }>,
): ContextItem {
const prompt = promptsMap.get(f.path.replace("prompt:", ""));
return {
kind: "prompt",
id: f.path,
label: f.name,
pinned: f.pinned,
onRemove: makeRemoveHandler(helpers.sid, f.path, helpers.removeContextFile),
onUnpin: makeUnpinHandler(f.pinned, helpers.sid, f.path, helpers.unpinFile),
promptContent: prompt?.content,
onClick: () => {
/* navigate to settings/prompts if desired */
},
};
}
function buildFileContextItem(
f: ContextFile,
helpers: FileItemHelpers,
onOpenFile: ((path: string) => void) | undefined,
): ContextItem {
return {
kind: "file",
id: f.path,
label: f.name,
path: f.path,
pinned: f.pinned,
onRemove: makeRemoveHandler(helpers.sid, f.path, helpers.removeContextFile),
onUnpin: makeUnpinHandler(f.pinned, helpers.sid, f.path, helpers.unpinFile),
onOpen: onOpenFile ?? (() => {}),
};
}
function buildFileAndPromptItems(params: BuildContextItemsParams): ContextItem[] {
const {
contextFiles,
resolvedSessionId: sid,
removeContextFile,
unpinFile,
promptsMap,
onOpenFile,
} = params;
const helpers: FileItemHelpers = { sid, removeContextFile, unpinFile };
const items: ContextItem[] = [];
for (const f of contextFiles) {
if (f.path === PLAN_CONTEXT_PATH) continue;
items.push(
f.path.startsWith("prompt:")
? buildPromptContextItem(f, helpers, promptsMap)
: buildFileContextItem(f, helpers, onOpenFile),
);
}
return items;
}
function buildCommentItems(params: BuildContextItemsParams): ContextItem[] {
const { pendingCommentsByFile, handleRemoveCommentFile, handleRemoveComment, onOpenFileAtLine } =
params;
const items: ContextItem[] = [];
if (!pendingCommentsByFile) return items;
for (const [filePath, comments] of Object.entries(pendingCommentsByFile)) {
if (comments.length === 0) continue;
const fileName = getFileName(filePath);
items.push({
kind: "comment",
id: `comment:${filePath}`,
label: `${fileName} (${comments.length})`,
filePath,
comments,
onRemove: () => handleRemoveCommentFile(filePath),
onRemoveComment: (cid) => handleRemoveComment(cid),
onOpen: onOpenFileAtLine ? () => onOpenFileAtLine(filePath) : undefined,
});
}
return items;
}
function buildPRFeedbackItems(params: BuildContextItemsParams): ContextItem[] {
const { pendingPRFeedback, handleRemovePRFeedback, handleClearPRFeedback } = params;
if (!pendingPRFeedback || pendingPRFeedback.length === 0) return [];
return [
{
kind: "pr-feedback" as const,
id: "pr-feedback",
label: `${pendingPRFeedback.length} PR feedback`,
comments: pendingPRFeedback,
onRemove: handleClearPRFeedback,
onRemoveComment: (id: string) => handleRemovePRFeedback(id),
},
];
}
/** Sort: pinned first, then by kind order, then by label */
const KIND_ORDER: Record<string, number> = {
plan: 0,
file: 1,
prompt: 2,
comment: 3,
"plan-comment": 4,
image: 5,
"pr-feedback": 6,
};
export function contextItemSortFn(a: ContextItem, b: ContextItem): number {
const aPinned = a.pinned ? 0 : 1;
const bPinned = b.pinned ? 0 : 1;
if (aPinned !== bPinned) return aPinned - bPinned;
const aKind = KIND_ORDER[a.kind] ?? 99;
const bKind = KIND_ORDER[b.kind] ?? 99;
if (aKind !== bKind) return aKind - bKind;
return a.label.localeCompare(b.label);
}
export function buildContextItems(params: BuildContextItemsParams): ContextItem[] {
const items: ContextItem[] = [];
const planItem = buildPlanContextItem(params);
if (planItem) items.push(planItem);
items.push(...buildFileAndPromptItems(params));
items.push(...buildCommentItems(params));
items.push(...buildPRFeedbackItems(params));
if (params.planComments.length > 0) {
items.push({
kind: "plan-comment",
id: "plan-comments",
label: `${params.planComments.length} plan comment${params.planComments.length !== 1 ? "s" : ""}`,
comments: params.planComments,
onRemove: params.handleClearPlanComments,
onOpen: params.addPlan,
});
}
return items.sort(contextItemSortFn);
}