import type { ContextFile } from "@/lib/state/context-files-store"; import { getFileName } from "@/lib/utils/file-path"; import { t } from "@/lib/i18n"; import type { ContextItem } from "@/lib/types/context"; import type { ReviewComment } from "@/lib/state/slices/comments"; import type { PlanComment, PRFeedbackComment, WalkthroughComment, AgentMessageComment, } from "@/lib/state/slices/comments"; import type { TaskPreviewFeedback } from "@/lib/types/http"; 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, repositoryName?: string) => void; planComments: PlanComment[]; handleClearPlanComments: () => void; previewFeedback?: TaskPreviewFeedback[]; pendingPRFeedback: PRFeedbackComment[]; handleRemovePRFeedback: (commentId: string) => void; handleClearPRFeedback: () => void; walkthroughComments: WalkthroughComment[]; handleRemoveWalkthroughComment: (commentId: string) => void; handleClearWalkthroughComments: () => void; messageComments: AgentMessageComment[]; handleClearMessageComments: () => void; taskId: string | null; onOpenPreviewFeedback?: () => void; }; 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: t("task:panelPlan"), 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, }; } function buildFileContextItem( f: ContextFile, helpers: FileItemHelpers, onOpenFile: ((path: string) => void) | undefined, ): ContextItem { return { kind: "file", id: f.path, label: f.name, path: f.path, isDirectory: f.isDirectory === true, pinned: f.pinned, onRemove: makeRemoveHandler(helpers.sid, f.path, helpers.removeContextFile), onUnpin: makeUnpinHandler(f.pinned, helpers.sid, f.path, helpers.unpinFile), onOpen: f.isDirectory ? undefined : 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 [groupKey, comments] of Object.entries(pendingCommentsByFile)) { if (comments.length === 0) continue; const first = comments[0]; const filePath = first.filePath; const repositoryName = first.repositoryName; const fileName = getFileName(filePath); items.push({ kind: "comment", id: `comment:${groupKey}`, label: `${fileName} (${comments.length})`, filePath, repositoryName, comments, onRemove: () => handleRemoveCommentFile(groupKey), onRemoveComment: (cid) => handleRemoveComment(cid), onOpen: onOpenFileAtLine ? () => onOpenFileAtLine(filePath, repositoryName) : 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} ${pendingPRFeedback.every((comment) => comment.provider === "gitlab") ? "MR" : "PR"} feedback`, comments: pendingPRFeedback, onRemove: handleClearPRFeedback, onRemoveComment: (id: string) => handleRemovePRFeedback(id), }, ]; } function buildWalkthroughCommentItems(params: BuildContextItemsParams): ContextItem[] { const { walkthroughComments, handleRemoveWalkthroughComment, handleClearWalkthroughComments } = params; if (!walkthroughComments || walkthroughComments.length === 0) return []; return [ { kind: "walkthrough-comment" as const, id: "walkthrough-comments", label: t("task:walkthroughNoteCount", { count: walkthroughComments.length }), comments: walkthroughComments, onRemove: handleClearWalkthroughComments, onRemoveComment: (id: string) => handleRemoveWalkthroughComment(id), }, ]; } function buildAgentMessageCommentItems(params: BuildContextItemsParams): ContextItem[] { const { messageComments, handleClearMessageComments } = params; if (messageComments.length === 0) return []; return [ { kind: "agent-message-comment" as const, id: "agent-message-comments", label: t("task:messageCommentCount", { count: messageComments.length }), comments: messageComments, onRemove: handleClearMessageComments, }, ]; } /** 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, "preview-feedback": 5, "walkthrough-comment": 6, "agent-message-comment": 7, image: 8, "pr-feedback": 9, }; 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(...buildWalkthroughCommentItems(params)); items.push(...buildAgentMessageCommentItems(params)); items.push(...buildPRFeedbackItems(params)); if (params.planComments.length > 0) { items.push({ kind: "plan-comment", id: "plan-comments", label: t("task:planCommentCount", { count: params.planComments.length }), comments: params.planComments, onOpen: params.addPlan, }); } const previewFeedback = params.previewFeedback ?? []; if (previewFeedback.length > 0) { items.push({ kind: "preview-feedback", id: "preview-feedback", label: t("task:previewFeedbackCount", { count: previewFeedback.length }), items: previewFeedback, onOpen: params.onOpenPreviewFeedback, }); } return items.sort(contextItemSortFn); }