Files
2026-09-22 13:20:58 +01:00

275 lines
8.8 KiB
TypeScript

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