Every user-attached context item (diff/file/plan comments, terminal selections, browser annotations, PR comments and failed checks, linked issues/PRs, and new chat-quote comments from the selection menu) is now sent as its own synthetic text part carrying an openchamberContext metadata payload. The model-facing text keeps the previous wording; the timeline reads the metadata back and renders each item as a context card instead of raw prompt text. Legacy messages still render via the old text sniffing. The selection menu gains a Comment option with an inline multiline input, the quoted fragment stays highlighted while commenting, and on mobile the input overlays the composer pill by rendering inside the composer form. Add to chat is renamed Add to input; the menu is restyled and the mobile Copy tile removed. Terminal drafts move their terminal id out of the language field (persisted-draft migration v3), and the dead preview-console source is deleted.
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import type { Part } from '@opencode-ai/sdk/v2';
|
|
import { readContextPart } from '@/lib/messages/contextParts';
|
|
|
|
type PartWithText = Part & { text?: string; content?: string; value?: string };
|
|
|
|
const isValidPart = (part: unknown): part is Part => {
|
|
return Boolean(part && typeof part === 'object' && typeof (part as { type?: unknown }).type === 'string');
|
|
};
|
|
|
|
export const normalizeParts = (parts: Part[]): Part[] => {
|
|
return parts.filter(isValidPart);
|
|
};
|
|
|
|
export const extractTextContent = (part: Part): string => {
|
|
const partWithText = part as PartWithText;
|
|
const rawText = partWithText.text;
|
|
if (typeof rawText === 'string') {
|
|
return rawText;
|
|
}
|
|
return partWithText.content || partWithText.value || '';
|
|
};
|
|
|
|
export const isEmptyTextPart = (part: Part): boolean => {
|
|
if (part.type !== 'text') {
|
|
return false;
|
|
}
|
|
const text = extractTextContent(part);
|
|
return !text || text.trim().length === 0;
|
|
};
|
|
|
|
type PartWithSynthetic = Part & { synthetic?: boolean };
|
|
|
|
interface VisibleFilterOptions {
|
|
includeReasoning?: boolean;
|
|
}
|
|
|
|
export const filterVisibleParts = (parts: Part[], options: VisibleFilterOptions = {}): Part[] => {
|
|
const { includeReasoning = true } = options;
|
|
const validParts = normalizeParts(parts);
|
|
|
|
// Check if there are any non-synthetic parts
|
|
const hasNonSynthetic = validParts.some((part) => {
|
|
const partWithSynthetic = part as PartWithSynthetic;
|
|
return !partWithSynthetic.synthetic;
|
|
});
|
|
|
|
return validParts.filter((part) => {
|
|
const partWithSynthetic = part as PartWithSynthetic;
|
|
const isSynthetic = Boolean(partWithSynthetic.synthetic);
|
|
|
|
if (isSynthetic && part.type === 'text') {
|
|
const text = extractTextContent(part);
|
|
if (text.includes('<system-reminder>')) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// User-attached context (inline comments, terminal selections, and
|
|
// such) is synthetic transport-wise but is user content: it renders
|
|
// as a context block and must survive alongside regular text.
|
|
if (isSynthetic && readContextPart(part)) {
|
|
return true;
|
|
}
|
|
|
|
// Only filter out synthetic parts if there are non-synthetic parts present
|
|
// Otherwise, show synthetic parts so the message is displayed
|
|
if (isSynthetic && hasNonSynthetic) {
|
|
return false;
|
|
}
|
|
if (!includeReasoning && part.type === 'reasoning') {
|
|
return false;
|
|
}
|
|
const isPatchPart = part.type === 'patch';
|
|
|
|
return !isPatchPart;
|
|
});
|
|
};
|