feat(chat): structured context attachments with metadata round-trip
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.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { create } from "zustand"
|
||||
import type { ContextPartMetadata } from '@/lib/messages/contextParts'
|
||||
import type { AttachedFile } from "@/stores/types/sessionTypes"
|
||||
import { prepareAttachmentFiles } from "./attachment-files"
|
||||
|
||||
@@ -115,6 +116,7 @@ export type SyntheticContextPart = {
|
||||
text: string
|
||||
attachments?: AttachedFile[]
|
||||
synthetic?: boolean
|
||||
metadata?: ContextPartMetadata
|
||||
}
|
||||
|
||||
export type VSCodeActiveEditorFile = {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* SDK-calling actions that need domain data read it from sync-refs.
|
||||
*/
|
||||
|
||||
import type { ContextPartMetadata } from "@/lib/messages/contextParts"
|
||||
import { create } from "zustand"
|
||||
import type { Session, Part, Message, TextPart } from "@opencode-ai/sdk/v2/client"
|
||||
import type { AttachedFile, SessionContextUsage, SessionWorktreeAttachment } from "@/stores/types/sessionTypes"
|
||||
@@ -138,7 +139,7 @@ export function routeMessage(params: {
|
||||
variant?: string
|
||||
inputMode?: "normal" | "shell"
|
||||
files?: Array<{ type: "file"; mime: string; url: string; filename: string }>
|
||||
additionalParts?: Array<{ text: string; synthetic?: boolean; files?: Array<{ type: "file"; mime: string; url: string; filename: string }> }>
|
||||
additionalParts?: Array<{ text: string; synthetic?: boolean; metadata?: ContextPartMetadata; files?: Array<{ type: "file"; mime: string; url: string; filename: string }> }>
|
||||
delivery?: 'steer'
|
||||
}): Promise<void> {
|
||||
const requestDirectory = params.directory ?? undefined
|
||||
@@ -357,7 +358,7 @@ export type SessionUIState = {
|
||||
agent?: string,
|
||||
attachments?: AttachedFile[],
|
||||
agentMentionName?: string,
|
||||
additionalParts?: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }>,
|
||||
additionalParts?: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean; metadata?: ContextPartMetadata }>,
|
||||
variant?: string,
|
||||
inputMode?: "normal" | "shell",
|
||||
options?: SendMessageOptions,
|
||||
@@ -1490,7 +1491,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
agent?: string,
|
||||
attachments?: AttachedFile[],
|
||||
agentMentionName?: string,
|
||||
additionalParts?: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }>,
|
||||
additionalParts?: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean; metadata?: ContextPartMetadata }>,
|
||||
variant?: string,
|
||||
inputMode?: "normal" | "shell",
|
||||
options?: SendMessageOptions,
|
||||
@@ -1578,7 +1579,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
createdDraftSession.directory,
|
||||
createdDraftSession.sessionId,
|
||||
)
|
||||
const draftPrefixParts: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }> =
|
||||
const draftPrefixParts: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean; metadata?: ContextPartMetadata }> =
|
||||
draftKnowledge.text ? [{ text: draftKnowledge.text, synthetic: true }] : []
|
||||
// Left undefined when nothing was added, as before: an empty array is not
|
||||
// the same as no additional parts to everything downstream.
|
||||
@@ -1613,6 +1614,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
additionalParts: mergedAdditionalParts?.map((p) => ({
|
||||
text: p.text,
|
||||
synthetic: p.synthetic,
|
||||
metadata: p.metadata,
|
||||
files: p.attachments?.map((a: AttachedFile) => ({
|
||||
type: "file" as const,
|
||||
mime: a.mimeType,
|
||||
@@ -1694,7 +1696,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
// Prepended so it reads as background before the message it accompanies,
|
||||
// and empty unless the session is actually missing it.
|
||||
const knowledge = await fetchSessionKnowledge(currentSessionDirectory, targetSessionId || "")
|
||||
const prefixParts: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean }> =
|
||||
const prefixParts: Array<{ text: string; attachments?: AttachedFile[]; synthetic?: boolean; metadata?: ContextPartMetadata }> =
|
||||
knowledge.text ? [{ text: knowledge.text, synthetic: true }] : []
|
||||
const partsWithPinnedContext = prefixParts.length > 0
|
||||
? [...prefixParts, ...(additionalParts || [])]
|
||||
@@ -1716,6 +1718,7 @@ export const useSessionUIStore = create<SessionUIState>()((set, get) => ({
|
||||
additionalParts: partsWithPinnedContext?.map((p) => ({
|
||||
text: p.text,
|
||||
synthetic: p.synthetic,
|
||||
metadata: p.metadata,
|
||||
files: p.attachments?.map((a) => ({
|
||||
type: "file" as const,
|
||||
mime: a.mimeType,
|
||||
|
||||
Reference in New Issue
Block a user