308aada5f9
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
275 lines
8.8 KiB
TypeScript
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);
|
|
}
|