Feat/add ide opened file to chat context (#1106)
* feat(chat): add active editor file context and related functionality * feat(chat): improve active editor file context handling and update translations * feat(i18n): standardize quotation marks in file attachment messages * update Korean translation for image removal action in file attachment * feat(chat): refine active editor file handling and optimize broadcast logic * fix(chat): stabilize VS Code editor context chips --------- Signed-off-by: David Saz <david.saz.g@gmail.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
7fc22bc69b
commit
5475ef2db3
@@ -6,17 +6,65 @@
|
||||
import { create } from "zustand"
|
||||
import type { AttachedFile } from "@/stores/types/sessionTypes"
|
||||
|
||||
const FILE_URI_PREFIX = "file://"
|
||||
const pendingVSCodeSelectionKeys = new Set<string>()
|
||||
|
||||
const encodeFilePath = (filepath: string): string => {
|
||||
let normalized = filepath.replace(/\\/g, "/")
|
||||
if (/^[A-Za-z]:/.test(normalized)) {
|
||||
normalized = `/${normalized}`
|
||||
}
|
||||
return normalized
|
||||
.split("/")
|
||||
.map((segment, index) => {
|
||||
if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment
|
||||
return encodeURIComponent(segment)
|
||||
})
|
||||
.join("/")
|
||||
}
|
||||
|
||||
const toFileUrl = (filepath: string): string => {
|
||||
const normalized = filepath.replace(/\\/g, "/").trim()
|
||||
if (normalized.toLowerCase().startsWith(FILE_URI_PREFIX)) {
|
||||
return normalized
|
||||
}
|
||||
return `${FILE_URI_PREFIX}${encodeFilePath(normalized)}`
|
||||
}
|
||||
|
||||
const getVSCodeSelectionKey = (path: string, filename: string): string => `${path}\u0000${filename}`
|
||||
|
||||
const isSameVSCodeActiveEditorFile = (a: VSCodeActiveEditorFile | null, b: VSCodeActiveEditorFile | null): boolean => {
|
||||
if (a === b) return true
|
||||
if (!a || !b) return false
|
||||
return a.filePath === b.filePath
|
||||
&& a.fileName === b.fileName
|
||||
&& a.relativePath === b.relativePath
|
||||
&& a.fileSize === b.fileSize
|
||||
&& a.selection?.startLine === b.selection?.startLine
|
||||
&& a.selection?.endLine === b.selection?.endLine
|
||||
&& a.selection?.text === b.selection?.text
|
||||
}
|
||||
|
||||
export type SyntheticContextPart = {
|
||||
text: string
|
||||
attachments?: AttachedFile[]
|
||||
synthetic?: boolean
|
||||
}
|
||||
|
||||
export type VSCodeActiveEditorFile = {
|
||||
filePath: string
|
||||
fileName: string
|
||||
relativePath: string
|
||||
fileSize: number | null
|
||||
selection: { startLine: number; endLine: number; text: string } | null
|
||||
}
|
||||
|
||||
export type InputState = {
|
||||
pendingInputText: string | null
|
||||
pendingInputMode: "replace" | "append" | "append-inline"
|
||||
pendingSyntheticParts: SyntheticContextPart[] | null
|
||||
attachedFiles: AttachedFile[]
|
||||
activeEditorFile: VSCodeActiveEditorFile | null
|
||||
|
||||
setPendingInputText: (text: string | null, mode?: "replace" | "append" | "append-inline") => void
|
||||
consumePendingInputText: () => { text: string; mode: "replace" | "append" | "append-inline" } | null
|
||||
@@ -25,6 +73,9 @@ export type InputState = {
|
||||
addAttachedFile: (file: File) => Promise<void>
|
||||
removeAttachedFile: (id: string) => void
|
||||
clearAttachedFiles: () => void
|
||||
addVSCodeFileAttachment: (path: string, name: string, fileSize: number | null) => void
|
||||
addVSCodeSelectionAttachment: (path: string, file: File) => Promise<void>
|
||||
setActiveEditorFile: (file: VSCodeActiveEditorFile | null) => void
|
||||
}
|
||||
|
||||
export const useInputStore = create<InputState>()((set, get) => ({
|
||||
@@ -32,6 +83,7 @@ export const useInputStore = create<InputState>()((set, get) => ({
|
||||
pendingInputMode: "replace",
|
||||
pendingSyntheticParts: null,
|
||||
attachedFiles: [],
|
||||
activeEditorFile: null,
|
||||
|
||||
setPendingInputText: (text, mode = "replace") =>
|
||||
set({ pendingInputText: text, pendingInputMode: mode }),
|
||||
@@ -76,4 +128,65 @@ export const useInputStore = create<InputState>()((set, get) => ({
|
||||
set((s) => ({ attachedFiles: s.attachedFiles.filter((f) => f.id !== id) })),
|
||||
|
||||
clearAttachedFiles: () => set({ attachedFiles: [] }),
|
||||
|
||||
addVSCodeFileAttachment: (path: string, name: string, fileSize: number | null) => {
|
||||
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
const isDuplicate = get().attachedFiles.some(
|
||||
(f) => f.source === 'vscode' && f.vscodeSource === 'file' && (f.vscodePath || '') === path
|
||||
)
|
||||
if (isDuplicate) return
|
||||
const dataUrl = toFileUrl(path)
|
||||
// `file://` URLs are the same contract used by server-source attachments.
|
||||
// The submission path passes `dataUrl` as `url` directly to the OpenCode
|
||||
// server, which resolves `file://` paths natively. No base64 encoding needed.
|
||||
const attached: AttachedFile = {
|
||||
id,
|
||||
file: new File([], name, { type: 'text/plain' }),
|
||||
dataUrl,
|
||||
mimeType: 'text/plain',
|
||||
filename: name,
|
||||
size: fileSize || 0,
|
||||
source: 'vscode',
|
||||
vscodePath: path,
|
||||
vscodeSource: 'file',
|
||||
}
|
||||
set((s) => ({ attachedFiles: [...s.attachedFiles, attached] }))
|
||||
},
|
||||
|
||||
addVSCodeSelectionAttachment: async (path: string, file: File) => {
|
||||
const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
const selectionKey = getVSCodeSelectionKey(path, file.name)
|
||||
const isDuplicate = get().attachedFiles.some(
|
||||
(f) => f.source === 'vscode' && f.vscodeSource === 'selection' && f.filename === file.name && f.vscodePath === path
|
||||
)
|
||||
if (isDuplicate || pendingVSCodeSelectionKeys.has(selectionKey)) return
|
||||
pendingVSCodeSelectionKeys.add(selectionKey)
|
||||
let dataUrl: string
|
||||
try {
|
||||
dataUrl = await new Promise<string>((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
} finally {
|
||||
pendingVSCodeSelectionKeys.delete(selectionKey)
|
||||
}
|
||||
const attached: AttachedFile = {
|
||||
id,
|
||||
file,
|
||||
dataUrl,
|
||||
mimeType: file.type,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
source: 'vscode',
|
||||
vscodePath: path,
|
||||
vscodeSource: 'selection',
|
||||
}
|
||||
set((s) => ({ attachedFiles: [...s.attachedFiles, attached] }))
|
||||
},
|
||||
|
||||
setActiveEditorFile: (file) => {
|
||||
if (isSameVSCodeActiveEditorFile(get().activeEditorFile, file)) return
|
||||
set({ activeEditorFile: file })
|
||||
},
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user