diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 5c882e7f0a9..f4845fe9f7d 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1,3 +1,4 @@ +import * as path from "path" import * as vscode from "vscode" import { z } from "zod" import { @@ -7,6 +8,8 @@ import { type KiloConnectionService, type KilocodeNotification, } from "./services/cli-backend" +import type { EditorContext } from "./services/cli-backend/types" +import { FileIgnoreController } from "./services/autocomplete/shims/FileIgnoreController" import { handleChatCompletionRequest } from "./services/autocomplete/chat-autocomplete/handleChatCompletionRequest" import { handleChatCompletionAccepted } from "./services/autocomplete/chat-autocomplete/handleChatCompletionAccepted" import { buildWebviewHtml } from "./utils" @@ -48,6 +51,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private unsubscribeNotificationDismiss: (() => void) | null = null private webviewMessageDisposable: vscode.Disposable | null = null + /** Lazily initialized ignore controller for .kilocodeignore filtering */ + private ignoreController: FileIgnoreController | null = null + private ignoreControllerDir: string | null = null + /** Optional interceptor called before the standard message handler. * Return null to consume the message, or return a (possibly transformed) message. */ private onBeforeMessage: ((msg: Record) => Promise | null>) | null = null @@ -1055,16 +1062,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Build parts array with file context and user text const parts: Array<{ type: "text"; text: string } | { type: "file"; mime: string; url: string }> = [] - // Inject active editor file as context - const editor = vscode.window.activeTextEditor - if (editor && editor.document.uri.scheme === "file") { - const url = editor.document.uri.toString() - const already = files?.some((f) => f.url === url) - if (!already) { - parts.push({ type: "file", mime: "text/plain", url }) - } - } - // Add any explicitly attached files from the webview if (files) { for (const f of files) { @@ -1074,11 +1071,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper parts.push({ type: "text", text }) + const editorContext = await this.gatherEditorContext() + await this.httpClient.sendMessage(targetSessionID, parts, workspaceDir, { providerID, modelID, agent, variant, + editorContext, }) } catch (error) { console.error("[Kilo New] KiloProvider: Failed to send message:", error) @@ -1506,6 +1506,86 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper }) } + /** + * Gather VS Code editor context to send alongside messages to the CLI backend. + */ + /** + * Get or create a FileIgnoreController for the current workspace directory. + * Reinitializes if the workspace directory has changed. + */ + private async getIgnoreController(workspaceDir: string): Promise { + if (this.ignoreController && this.ignoreControllerDir === workspaceDir) { + return this.ignoreController + } + const controller = new FileIgnoreController(workspaceDir) + await controller.initialize() + this.ignoreController = controller + this.ignoreControllerDir = workspaceDir + return controller + } + + private async gatherEditorContext(): Promise { + const workspaceDir = this.getWorkspaceDirectory() + const controller = await this.getIgnoreController(workspaceDir) + + const toRelative = (fsPath: string): string | undefined => { + if (!workspaceDir) { + return undefined + } + const relative = path.relative(workspaceDir, fsPath) + if (relative.startsWith("..")) { + return undefined + } + return relative + } + + // Visible files (capped to avoid bloating context, filtered through .kilocodeignore) + const visibleFiles = vscode.window.visibleTextEditors + .map((e) => e.document.uri) + .filter((uri) => uri.scheme === "file") + .map((uri) => toRelative(uri.fsPath)) + .filter((p): p is string => p !== undefined && controller.validateAccess(path.resolve(workspaceDir, p))) + .slice(0, 200) + + // Open tabs — use instanceof TabInputText to exclude notebooks, diffs, custom editors + const openTabSet = new Set() + for (const group of vscode.window.tabGroups.all) { + for (const tab of group.tabs) { + if (tab.input instanceof vscode.TabInputText) { + const uri = tab.input.uri + if (uri.scheme === "file") { + const rel = toRelative(uri.fsPath) + if (rel && controller.validateAccess(uri.fsPath)) { + openTabSet.add(rel) + } + } + } + } + } + const openTabs = [...openTabSet].slice(0, 20) + + // Active file (also filtered through .kilocodeignore) + const activeEditor = vscode.window.activeTextEditor + const activeRel = + activeEditor?.document.uri.scheme === "file" ? toRelative(activeEditor.document.uri.fsPath) : undefined + const activeFile = + activeRel && controller.validateAccess(activeEditor!.document.uri.fsPath) ? activeRel : undefined + + // Shell + const shell = vscode.env.shell || undefined + + // Timezone + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || undefined + + return { + ...(visibleFiles.length > 0 ? { visibleFiles } : {}), + ...(openTabs.length > 0 ? { openTabs } : {}), + ...(activeFile ? { activeFile } : {}), + ...(shell ? { shell } : {}), + ...(timezone ? { timezone } : {}), + } + } + /** * Get the workspace directory for a session. * Checks session directory overrides first (e.g., worktree paths), then falls back to workspace root. @@ -1544,5 +1624,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.webviewMessageDisposable?.dispose() this.trackedSessionIds.clear() this.sessionDirectories.clear() + this.ignoreController?.dispose() } } diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts index 8892525c4bc..724a3d808e6 100644 --- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -12,6 +12,7 @@ import type { McpConfig, Config, KilocodeNotification, + EditorContext, } from "./types" import { extractHttpErrorMessage, parseSSEDataLine } from "./http-utils" @@ -214,7 +215,7 @@ export class HttpClient { sessionId: string, parts: Array<{ type: "text"; text: string } | { type: "file"; mime: string; url: string }>, directory: string, - options?: { providerID?: string; modelID?: string; agent?: string; variant?: string }, + options?: { providerID?: string; modelID?: string; agent?: string; variant?: string; editorContext?: EditorContext }, ): Promise { const body: Record = { parts } if (options?.providerID && options?.modelID) { @@ -227,6 +228,9 @@ export class HttpClient { if (options?.variant) { body.variant = options.variant } + if (options?.editorContext) { + body.editorContext = options.editorContext + } await this.request("POST", `/session/${sessionId}/message`, body, { directory, allowEmpty: true }) } diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts index bf143c7144b..33f8d87b403 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -336,3 +336,17 @@ export interface Config { layout?: "auto" | "stretch" experimental?: ExperimentalConfig } + +/** VS Code editor context sent alongside messages to the CLI backend */ +export interface EditorContext { + /** Workspace-relative paths of currently visible editors */ + visibleFiles?: string[] + /** Workspace-relative paths of open tabs */ + openTabs?: string[] + /** Workspace-relative path of the active editor file */ + activeFile?: string + /** User's default shell (from vscode.env.shell) */ + shell?: string + /** User's timezone (e.g. "Europe/Amsterdam") */ + timezone?: string +} diff --git a/packages/opencode/src/kilocode/editor-context.ts b/packages/opencode/src/kilocode/editor-context.ts new file mode 100644 index 00000000000..5431c7720eb --- /dev/null +++ b/packages/opencode/src/kilocode/editor-context.ts @@ -0,0 +1,41 @@ +export interface EditorContext { + visibleFiles?: string[] + openTabs?: string[] + activeFile?: string + shell?: string + timezone?: string +} + +function formatTime(timezone?: string): string[] { + const now = new Date() + const lines = [` Current time: ${now.toISOString()}`] + if (timezone) { + const offset = -now.getTimezoneOffset() + const sign = offset >= 0 ? "+" : "-" + const hours = Math.floor(Math.abs(offset) / 60) + const mins = Math.abs(offset) % 60 + lines.push(` User timezone: ${timezone}, UTC${sign}${hours}:${mins.toString().padStart(2, "0")}`) + } + return lines +} + +/** + * Build additional lines from VS Code editor context. + * Returns an array of pre-formatted ` key: value` strings. + */ +export function editorContextEnvLines(ctx?: EditorContext): string[] { + const lines = formatTime(ctx?.timezone) + if (ctx?.shell) { + lines.push(` Default shell: ${ctx.shell}`) + } + if (ctx?.activeFile) { + lines.push(` Active file: ${ctx.activeFile}`) + } + if (ctx?.visibleFiles?.length) { + lines.push(` Visible files: ${ctx.visibleFiles.join(", ")}`) + } + if (ctx?.openTabs?.length) { + lines.push(` Open tabs: ${ctx.openTabs.join(", ")}`) + } + return lines +} diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 70763548c6a..31ee2bce370 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -359,6 +359,17 @@ export namespace MessageV2 { system: z.string().optional(), tools: z.record(z.string(), z.boolean()).optional(), variant: z.string().optional(), + // kilocode_change start + editorContext: z + .object({ + visibleFiles: z.array(z.string()).optional(), + openTabs: z.array(z.string()).optional(), + activeFile: z.string().optional(), + shell: z.string().optional(), + timezone: z.string().optional(), + }) + .optional(), + // kilocode_change end }).meta({ ref: "UserMessage", }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ae6248a0efd..f3c0e5e39d6 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -109,6 +109,17 @@ export namespace SessionPrompt { format: MessageV2.Format.optional(), system: z.string().optional(), variant: z.string().optional(), + // kilocode_change start + editorContext: z + .object({ + visibleFiles: z.array(z.string()).optional(), + openTabs: z.array(z.string()).optional(), + activeFile: z.string().optional(), + shell: z.string().optional(), + timezone: z.string().optional(), + }) + .optional(), + // kilocode_change end parts: z.array( z.discriminatedUnion("type", [ MessageV2.TextPart.omit({ @@ -667,7 +678,7 @@ export namespace SessionPrompt { await Plugin.trigger("experimental.chat.messages.transform", {}, { messages: sessionMessages }) // Build system prompt, adding structured output instruction if needed - const system = [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())] + const system = [...(await SystemPrompt.environment(model, lastUser.editorContext)), ...(await InstructionPrompt.system())] // kilocode_change const format = lastUser.format ?? { type: "text" } if (format.type === "json_schema") { system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) @@ -994,6 +1005,7 @@ export namespace SessionPrompt { system: input.system, format: input.format, variant, + editorContext: input.editorContext, // kilocode_change } using _ = defer(() => InstructionPrompt.clear(info.id)) diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index d1c836dd5d1..cd7ddf84e08 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -13,6 +13,7 @@ import type { Provider } from "@/provider/provider" // kilocode_change start import SOUL from "../kilocode/soul.txt" +import { editorContextEnvLines, type EditorContext } from "../kilocode/editor-context" // kilocode_change end export namespace SystemPrompt { @@ -36,7 +37,9 @@ export namespace SystemPrompt { return [PROMPT_ANTHROPIC_WITHOUT_TODO] } - export async function environment(model: Provider.Model) { + // kilocode_change start + export async function environment(model: Provider.Model, editorContext?: EditorContext) { + // kilocode_change end const project = Instance.project return [ [ @@ -46,7 +49,7 @@ export namespace SystemPrompt { ` Working directory: ${Instance.directory}`, ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`, ` Platform: ${process.platform}`, - ` Today's date: ${new Date().toDateString()}`, + ...editorContextEnvLines(editorContext), // kilocode_change ``, ``, ` ${