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; onOpenFile?: (path: string) => void; pendingCommentsByFile: Record; 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, ): 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 = { 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); }