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.
74 lines
2.5 KiB
TypeScript
74 lines
2.5 KiB
TypeScript
import type { Part } from "@opencode-ai/sdk/v2";
|
|
|
|
import { readContextPart } from "./contextParts";
|
|
|
|
const GITHUB_ISSUE_CONTEXT_PREFIX = 'GitHub issue context (JSON)';
|
|
const GITHUB_PR_CONTEXT_PREFIX = 'GitHub pull request context (JSON)';
|
|
|
|
export const isSyntheticPart = (part: Part | undefined): boolean => {
|
|
if (!part || typeof part !== "object") {
|
|
return false;
|
|
}
|
|
return Boolean((part as { synthetic?: boolean }).synthetic);
|
|
};
|
|
|
|
/**
|
|
* Checks if a message consists entirely of synthetic parts.
|
|
* Used for status/completion logic (not display filtering).
|
|
*/
|
|
export const isFullySyntheticMessage = (parts: Part[] | undefined): boolean => {
|
|
if (!Array.isArray(parts) || parts.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return parts.every((part) => isSyntheticPart(part));
|
|
};
|
|
|
|
/**
|
|
* Filters out synthetic parts from a message, but only if there are
|
|
* non-synthetic parts present. If all parts are synthetic, returns
|
|
* them as-is so the message can still be displayed.
|
|
*/
|
|
export const filterSyntheticParts = (parts: Part[] | undefined): Part[] => {
|
|
if (!Array.isArray(parts) || parts.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const hasNonSynthetic = parts.some((part) => !isSyntheticPart(part));
|
|
|
|
const shouldKeepSyntheticPart = (part: Part): boolean => {
|
|
if (!isSyntheticPart(part) || part.type !== 'text') {
|
|
return false;
|
|
}
|
|
|
|
// User-attached context (inline comments, terminal selections, and
|
|
// such) is synthetic transport-wise but is user content that renders
|
|
// as its own context block.
|
|
if (readContextPart(part)) {
|
|
return true;
|
|
}
|
|
|
|
const text = (part as { text?: unknown }).text;
|
|
if (typeof text !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
const trimmed = text.trimStart();
|
|
return trimmed.startsWith(GITHUB_ISSUE_CONTEXT_PREFIX) || trimmed.startsWith(GITHUB_PR_CONTEXT_PREFIX);
|
|
};
|
|
|
|
// If there are non-synthetic parts, filter out synthetic ones
|
|
if (hasNonSynthetic) {
|
|
// Optimization: Check if there are actually any synthetic parts to filter.
|
|
// If not, return the original array to preserve referential equality.
|
|
const hasSynthetic = parts.some((part) => isSyntheticPart(part));
|
|
if (!hasSynthetic) {
|
|
return parts;
|
|
}
|
|
return parts.filter((part) => !isSyntheticPart(part) || shouldKeepSyntheticPart(part));
|
|
}
|
|
|
|
// If all parts are synthetic, return them all (so message is displayed)
|
|
return parts;
|
|
};
|