Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,15 @@
"type": "boolean",
"default": true,
"description": "Show the task timeline graph in the chat header"
},
"kilo-code.new.attachments.imageMode": {
"type": "string",
"default": "data",
"enum": [
"data",
"path"
],
"description": "How to send image attachments: 'data' sends base64 data URLs, 'path' sends file:// paths for MCP servers that expect file paths."
}
}
}
Expand Down
44 changes: 44 additions & 0 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.slimEditMetadata = options?.slimEditMetadata ?? true

TelemetryProxy.getInstance().setProvider(this)

// Listen for imageMode setting changes and push to webview
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("kilo-code.new.attachments")) {
this.sendImageMode()
}
})
}

setRemoteService(service: RemoteStatusService): void {
Expand Down Expand Up @@ -687,6 +694,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "previewImage":
this.handlePreviewImage(message.dataUrl, message.filename)
break
case "saveImageToTemp":
await this.handleSaveImageToTemp(message.id, message.mime, message.base64, message.filename)
break
case "openFile":
if (message.filePath) {
this.handleOpenFile(message.filePath, message.line, message.column)
Expand Down Expand Up @@ -848,6 +858,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "requestClaudeCompatSetting":
this.sendClaudeCompatSetting()
break
case "requestImageMode":
this.sendImageMode()
break
case "requestNotificationSettings":
this.sendNotificationSettings()
break
Expand Down Expand Up @@ -2720,6 +2733,28 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
.then(open, (err) => console.error("[Kilo New] KiloProvider: Failed to preview image:", err))
}

private async handleSaveImageToTemp(id: string, mime: string, base64: string, filename: string): Promise<void> {
const tempDir = vscode.env.tempFilesStoragePath
if (!tempDir) {
console.error("[Kilo New] KiloProvider: tempFilesStoragePath not available")
return
}

try {
const uri = vscode.Uri.joinPath(vscode.Uri.file(tempDir), `image-${Date.now()}-${filename}`)
const data = Buffer.from(base64, "base64")
await vscode.workspace.fs.writeFile(uri, data)
this.postMessage({
type: "imageSaved",
id,
filePath: uri.toString(),
mime,
})
} catch (err) {
console.error("[Kilo New] KiloProvider: Failed to save image to temp:", err)
}
}

/**
* Handle openFile request from the webview — open a file in the VS Code editor.
* Resolves relative paths against the current session's directory (which may be
Expand Down Expand Up @@ -2830,6 +2865,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
})
}

private sendImageMode(): void {
const config = vscode.workspace.getConfiguration("kilo-code.new.attachments")
const mode = config.get<"data" | "path">("imageMode", "data")
this.postMessage({
type: "imageModeLoaded",
mode,
})
}

/** Re-fetch all server-side state after an auth change. */
private async reloadAfterAuthChange(): Promise<void> {
await Promise.all([
Expand Down
13 changes: 8 additions & 5 deletions packages/kilo-vscode/webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ProviderProvider, useProvider } from "./context/provider"
import { ConfigProvider } from "./context/config"
import { SessionProvider, useSession } from "./context/session"
import { LanguageProvider } from "./context/language"
import { ImageModeProvider } from "./context/ImageModeContext"
import { ChatView } from "./components/chat"
import { MarketplaceView } from "./components/marketplace"
import { registerExpandedTaskTool } from "./components/chat/TaskToolExpanded"
Expand Down Expand Up @@ -285,11 +286,13 @@ const App: Component = () => {
<ProviderProvider>
<ConfigProvider>
<NotificationsProvider>
<SessionProvider>
<DataBridge>
<AppContent />
</DataBridge>
</SessionProvider>
<ImageModeProvider>
<SessionProvider>
<DataBridge>
<AppContent />
</DataBridge>
</SessionProvider>
</ImageModeProvider>
</NotificationsProvider>
</ConfigProvider>
</ProviderProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useServer } from "../../context/server"
import { useLanguage } from "../../context/language"
import { useVSCode } from "../../context/vscode"
import { useWorktreeMode } from "../../context/worktree-mode"
import { useImageMode } from "../../context/ImageModeContext"
import { ModelSelector } from "../shared/ModelSelector"
import { ModeSwitcher } from "../shared/ModeSwitcher"
import { ThinkingSelector } from "../shared/ThinkingSelector"
Expand Down Expand Up @@ -56,11 +57,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const language = useLanguage()
const vscode = useVSCode()
const worktree = useWorktreeMode()
const imageMode = useImageMode()
const dialog = useDialog()
const mention = useFileMention(vscode)
const excluded = worktree ? new Set(["sessions"]) : undefined
const slash = useSlashCommand(vscode, excluded)
const imageAttach = useImageAttachments()
const imageAttach = useImageAttachments({ imageMode: imageMode })

createEffect(() => {
const mode = imageMode()
imageAttach.replace([])
})

onCleanup(
vscode.onMessage((msg: any) => {
if (msg.type === "imageSaved") {
imageAttach.handleImageSaved(msg.id, msg.filePath, msg.mime)
}
}),
)
imageAttach.setFilePathDropHandler((paths) => {
const cwd = server.workspaceDirectory()
const resolved = paths.map((p) => convertToMentionPath(p, cwd))
Expand Down
50 changes: 50 additions & 0 deletions packages/kilo-vscode/webview-ui/src/context/ImageModeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* ImageModeContext
* Provides the current image attachment mode setting ("data" or "path").
* "data" sends base64 data URLs, "path" sends file:// paths for MCP servers.
*/

import { createContext, useContext, createSignal, onCleanup, ParentComponent } from "solid-js"
import type { Accessor } from "solid-js"
import { useVSCode } from "./vscode"
import type { ExtensionMessage } from "../types/messages"

export type ImageMode = "data" | "path"

interface ImageModeContextValue {
imageMode: Accessor<ImageMode>
}

export const ImageModeContext = createContext<ImageModeContextValue>()

export const ImageModeProvider: ParentComponent = (props) => {
const vscode = useVSCode()
const [imageMode, setImageMode] = createSignal<ImageMode>("data")

const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
if (message.type === "imageModeLoaded") {
setImageMode(message.mode)
}
})

onCleanup(unsubscribe)

// Request initial value and listen for changes
vscode.postMessage({ type: "requestImageMode" })

const value: ImageModeContextValue = { imageMode }

return <ImageModeContext.Provider value={value}>{props.children}</ImageModeContext.Provider>
}

export function useImageModeContext(): ImageModeContextValue {
const context = useContext(ImageModeContext)
if (!context) {
throw new Error("useImageModeContext must be used within an ImageModeProvider")
}
return context
}

export function useImageMode(): Accessor<ImageMode> {
return useImageModeContext().imageMode
}
52 changes: 42 additions & 10 deletions packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSignal } from "solid-js"
import { createSignal, type Accessor } from "solid-js"
import { ACCEPTED_IMAGE_TYPES, isAcceptedImageType, isDragLeavingComponent } from "./image-attachments-utils"
import { extractDropPaths } from "../utils/path-mentions"

Expand All @@ -12,18 +12,38 @@ export interface ImageAttachment {
/** Callback for handling text/URI file path drops. */
export type FilePathDropHandler = (paths: string[]) => void

export function useImageAttachments() {
export interface UseImageAttachmentsOptions {
imageMode?: Accessor<"data" | "path">
}

export function useImageAttachments(options: UseImageAttachmentsOptions = {}) {
const [images, setImages] = createSignal<ImageAttachment[]>([])
const [dragging, setDragging] = createSignal(false)
let onFilePaths: FilePathDropHandler | undefined

/** Register a handler for file path drops (text/URI-list). */
const setFilePathDropHandler = (handler: FilePathDropHandler) => {
onFilePaths = handler
}
const getMode = () => options.imageMode?.() ?? "data"

const add = (file: File) => {
if (!isAcceptedImageType(file.type)) return
const mode = getMode()
if (mode === "path") {
const reader = new FileReader()
reader.onload = () => {
const dataUrl = reader.result as string
const base64 = dataUrl.split(",")[1]
if (!base64) return
const id = crypto.randomUUID()
window.vscode?.postMessage({
type: "saveImageToTemp",
id,
mime: file.type || "image/png",
base64,
filename: file.name || "image.png",
})
}
reader.readAsDataURL(file)
return
}
const reader = new FileReader()
reader.onload = () => {
const attachment: ImageAttachment = {
Expand All @@ -37,6 +57,11 @@ export function useImageAttachments() {
reader.readAsDataURL(file)
}

/** Register a handler for file path drops (text/URI-list). */
const setFilePathDropHandler = (handler: FilePathDropHandler) => {
onFilePaths = handler
}

const remove = (id: string) => {
setImages((prev) => prev.filter((img) => img.id !== id))
}
Expand All @@ -59,8 +84,6 @@ export function useImageAttachments() {
const handleDragOver = (event: DragEvent) => {
const types = event.dataTransfer?.types
if (!types) return
// Accept file drops and VS Code URI-list drops (explorer, editor tabs).
// Do NOT accept bare text/plain here — that would intercept normal text drags.
const acceptable = types.includes("Files") || types.includes("application/vnd.code.uri-list")
if (!acceptable) return
event.preventDefault()
Expand All @@ -79,19 +102,27 @@ export function useImageAttachments() {
const dt = event.dataTransfer
if (!dt) return

// First: check for text/URI file path drops (VS Code explorer, editor tabs)
const paths = extractDropPaths(dt)
if (paths && paths.length > 0 && onFilePaths) {
onFilePaths(paths)
return
}

// Second: fall through to image file drops
const files = dt.files
if (!files) return
for (const file of Array.from(files)) add(file)
}

const handleImageSaved = (id: string, filePath: string, mime: string) => {
const attachment: ImageAttachment = {
id,
filename: filePath.split("/").pop() || "image",
mime: mime || "image/png",
dataUrl: filePath,
}
setImages((prev) => [...prev, attachment])
}

return {
images,
dragging,
Expand All @@ -104,5 +135,6 @@ export function useImageAttachments() {
handleDragLeave,
handleDrop,
setFilePathDropHandler,
handleImageSaved,
}
}
28 changes: 28 additions & 0 deletions packages/kilo-vscode/webview-ui/src/types/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,8 @@ export type ExtensionMessage =
| QuestionErrorMessage
| BrowserSettingsLoadedMessage
| ClaudeCompatSettingLoadedMessage
| ImageModeLoadedMessage
| ImageSavedMessage
| ConfigLoadedMessage
| ConfigUpdatedMessage
| GlobalConfigLoadedMessage
Expand Down Expand Up @@ -1808,6 +1810,22 @@ export interface ClaudeCompatSettingLoadedMessage {
enabled: boolean
}

export interface RequestImageModeMessage {
type: "requestImageMode"
}

export interface ImageModeLoadedMessage {
type: "imageModeLoaded"
mode: "data" | "path"
}

export interface ImageSavedMessage {
type: "imageSaved"
id: string
filePath: string
mime: string
}

export interface RequestConfigMessage {
type: "requestConfig"
}
Expand Down Expand Up @@ -1934,6 +1952,14 @@ export interface RenameWorktreeRequest {
label: string
}

export interface SaveImageToTempRequest {
type: "saveImageToTemp"
id: string
mime: string
base64: string
filename: string
}

export interface RequestRepoInfoMessage {
type: "agentManager.requestRepoInfo"
}
Expand Down Expand Up @@ -2374,6 +2400,7 @@ export type WebviewMessage =
| RequestTimelineSettingMessage
| RequestBrowserSettingsMessage
| RequestClaudeCompatSettingMessage
| RequestImageModeMessage
| RequestConfigMessage
| RequestGlobalConfigMessage
| UpdateConfigMessage
Expand Down Expand Up @@ -2468,6 +2495,7 @@ export type WebviewMessage =
| ToggleSectionCollapsedRequest
| MoveToSectionRequest
| MoveSectionRequest
| SaveImageToTempRequest

// ============================================
// VS Code API type
Expand Down