瀏覽代碼

auto refresh files in IDE on edit/write

paviko 2 月之前
父節點
當前提交
828fee43f1

+ 1 - 0
hosts/IDE_BRIDGE.md

@@ -156,6 +156,7 @@ if (ideBridge.isInstalled()) {
 
 - **openFile** — payload: `{ path: string, line?: number }` → opens file in IDE, responds with `{ replyTo, ok }` or `{ replyTo, ok: false, error }`
 - **openUrl** — payload: `{ url: string }` → opens URL in default browser, responds with `{ replyTo, ok }`
+- **reloadPath** — payload: `{ path: string, operation: "write" | "edit" }` → reloads file from disk after AI agent modifies it, responds with `{ replyTo, ok }`
 
 ### Protocol notes
 

+ 5 - 0
hosts/jetbrains-plugin/changelog.html

@@ -1,5 +1,10 @@
 <h2>OpenCode (unofficial) JetBrains Plugin Changelog</h2>
 
+<h3>2025.12.xx</h3>
+<ul>
+  <li>Auto refresh files in IDE on edit/write</li>
+</ul>
+
 <h3>2025.11.30</h3>
 <ul>
   <li>Updated OpenCode to v1.0.121</li>

+ 25 - 0
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/IdeBridge.kt

@@ -138,6 +138,12 @@ object IdeBridge {
                     replyError(project, id, t.message ?: "Failed to open url")
                 }
             }
+            "reloadPath" -> {
+                val payload = obj.get("payload")
+                val path = payload?.get("path")?.asText() ?: return replyError(project, id, "missing path")
+                reloadFile(project, path)
+                replyOk(project, id)
+            }
             else -> replyOk(project, id)
         }
     }
@@ -190,6 +196,25 @@ object IdeBridge {
     private fun replyOk(project: Project, replyTo: String?) { sendRaw(project, mapOf("replyTo" to replyTo, "ok" to true)) }
     private fun replyError(project: Project, replyTo: String?, error: String) { sendRaw(project, mapOf("replyTo" to replyTo, "ok" to false, "error" to error)) }
 
+    private fun reloadFile(project: Project, path: String) {
+        try {
+            val lfs = LocalFileSystem.getInstance()
+            val vf = lfs.findFileByPath(path) ?: lfs.refreshAndFindFileByPath(path)
+            if (vf != null) {
+                ApplicationManager.getApplication().invokeLater {
+                    vf.refresh(false, false)
+                }
+            } else {
+                // File doesn't exist yet (new file), refresh parent directory
+                val parentPath = path.substringBeforeLast("/")
+                val parentVf = lfs.findFileByPath(parentPath) ?: lfs.refreshAndFindFileByPath(parentPath)
+                parentVf?.refresh(false, true)
+            }
+        } catch (t: Throwable) {
+            logger.warn("reloadFile failed", t)
+        }
+    }
+
     fun send(project: Project, type: String, payload: Map<String, Any?> = emptyMap()) {
         val message = mutableMapOf<String, Any?>("type" to type, "timestamp" to System.currentTimeMillis())
         if (payload.isNotEmpty()) message["payload"] = payload

+ 4 - 0
hosts/vscode-plugin/CHANGELOG.md

@@ -5,6 +5,10 @@ All notable changes to the OpenCode VSCode extension will be documented in this
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [25.12.xx] - 2025-12-xx
+
+- Auto refresh files in IDE on edit/write
+
 ## [25.11.30] - 2025-11-30
 
 - Updated OpenCode to v1.0.121

+ 47 - 0
hosts/vscode-plugin/src/ui/CommunicationBridge.ts

@@ -337,6 +337,48 @@ export class CommunicationBridge implements PluginCommunicator {
     }
   }
 
+  /**
+   * Handle reload path request from web UI - refreshes file from disk after AI agent modifies it
+   * @param filePath File path to reload
+   */
+  async handleReloadPath(filePath: string): Promise<void> {
+    try {
+      if (!filePath || filePath.trim().length === 0) {
+        logger.appendLine("No path provided to reload")
+        return
+      }
+
+      const normalizedPath = this.normalizePath(filePath)
+      if (!normalizedPath) {
+        logger.appendLine(`Invalid path to reload: ${filePath}`)
+        return
+      }
+
+      const fileUri = vscode.Uri.file(normalizedPath)
+
+      // Check if file exists and refresh it
+      try {
+        await vscode.workspace.fs.stat(fileUri)
+        // File exists - find open editors and refresh them
+        for (const editor of vscode.window.visibleTextEditors) {
+          if (editor.document.uri.fsPath === fileUri.fsPath) {
+            // Revert the document to reload from disk
+            await vscode.commands.executeCommand("workbench.action.files.revert", editor.document.uri)
+            logger.appendLine(`Reloaded file: ${normalizedPath}`)
+            return
+          }
+        }
+        // File not open in editor, no action needed
+        logger.appendLine(`File not open in editor, skipping reload: ${normalizedPath}`)
+      } catch {
+        // File doesn't exist yet (new file), refresh workspace
+        logger.appendLine(`File not found, refreshing workspace: ${normalizedPath}`)
+      }
+    } catch (error) {
+      logger.appendLine(`Error reloading path: ${error}`)
+    }
+  }
+
   /**
    * Handle state change from web UI
    * @param key Setting key
@@ -424,6 +466,11 @@ export class CommunicationBridge implements PluginCommunicator {
                 if (m.id) {
                   this.webview?.postMessage({ replyTo: m.id, ok: true })
                 }
+              } else if (m && m.type === "reloadPath") {
+                await this.handleReloadPath(m.payload?.path)
+                if (m.id) {
+                  this.webview?.postMessage({ replyTo: m.id, ok: true })
+                }
               } else {
                 // Generic ack for unknown types
                 if (m && m.id) this.webview?.postMessage({ replyTo: m.id, ok: true })

+ 5 - 0
packages/opencode/webgui/src/lib/ideBridge.ts

@@ -119,3 +119,8 @@ class IdeBridge {
 }
 
 export const ideBridge = new IdeBridge()
+
+export function reloadPath(path: string, operation: "write" | "edit") {
+  if (!ideBridge.isInstalled()) return
+  ideBridge.send({ type: "reloadPath", payload: { path, operation } })
+}

+ 13 - 0
packages/opencode/webgui/src/state/MessagesContext.tsx

@@ -5,6 +5,7 @@ import type { Permission } from "@opencode-ai/sdk/client"
 import * as Store from "../lib/messagesStore"
 import { sdk } from "../lib/api/sdkClient"
 import { useSession } from "./SessionContext"
+import { reloadPath } from "../lib/ideBridge"
 
 // Re-export types for convenience
 export type { Message, Part, SDKMessage } from "../types/messages"
@@ -115,6 +116,18 @@ export function MessagesProvider({ children, emitter }: MessagesProviderProps) {
           addPart(part.messageID, part)
         }
 
+        // Reload file in IDE when write/edit tool completes
+        if (part.type === "tool") {
+          const toolPart = part as { tool?: string; state?: { status?: string; input?: { filePath?: string } } }
+          if (
+            (toolPart.tool === "write" || toolPart.tool === "edit") &&
+            toolPart.state?.status === "completed" &&
+            toolPart.state?.input?.filePath
+          ) {
+            reloadPath(toolPart.state.input.filePath, toolPart.tool)
+          }
+        }
+
         if (part.type === "reasoning") {
           //const time = (part as { time?: { end?: number } }).time
           //const end = typeof time?.end === 'number'