paviko пре 2 месеци
родитељ
комит
ae64e68c63

+ 1 - 1
hosts/jetbrains-plugin/build.gradle.kts

@@ -5,7 +5,7 @@ plugins {
 }
 
 group = "paviko.opencode"
-version = "26.1.24"
+version = "26.1.27"
 
 repositories {
     mavenCentral()

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

@@ -1,5 +1,10 @@
 <h2>OpenCode UX+ (unofficial) JetBrains Plugin Changelog</h2>
 
+<h3>26.1.27</h3>
+<ul>
+  <li>apply_patch tool fallback count for Changed Files panel</li>
+</ul>
+
 <h3>26.1.24</h3>
 <ul>
   <li>Share / Unshare session</li>
@@ -93,3 +98,8 @@
 <ul>
   <li>First release of the OpenCode JetBrains plugin, based on OpenCode v1.0.68</li>
 </ul>
+
+<h3>25.11.18</h3>
+<ul>
+  <li>First release of the OpenCode JetBrains plugin, based on OpenCode v1.0.68</li>
+</ul>

+ 0 - 2
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/ChatToolWindowFactory.kt

@@ -30,7 +30,6 @@ import javax.swing.*
 class ChatToolWindowFactory : ToolWindowFactory, DumbAware {
     private var connectionInfo: ConnInfo? = null
     private val logger = Logger.getInstance(ChatToolWindowFactory::class.java)
-
     private val maxLogChars = 200_000
 
     private fun showError(mainPanel: JPanel, hideableLogs: JComponent, message: String) {
@@ -50,7 +49,6 @@ class ChatToolWindowFactory : ToolWindowFactory, DumbAware {
         val descriptor = PluginManagerCore.getPlugin(pluginId) ?: return "dev"
         return descriptor.version
     }
-
     private fun withCacheBuster(url: String, version: String): String {
         val encodedVersion = URLEncoder.encode(version, StandardCharsets.UTF_8)
         val sep = if (url.contains("?")) "&" else "?"

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

@@ -5,6 +5,11 @@ 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/spec2.0.0.html).
 
+### [26.1.27] - 2026-01-27
+- apply_patch tool fallback count for Changed Files panel
+- removed forbidden getPluginId()
+- fixed not showing tooltips
+
 ### [26.1.24] - 2026-01-24
 - Share / Unshare session
 - Added button"Copy to clipboard" message content

+ 1 - 1
hosts/vscode-plugin/package.json

@@ -2,7 +2,7 @@
   "name": "opencode-ux-plus",
   "displayName": "OpenCode UX+ (unofficial)",
   "description": "Unofficial OpenCode VSCode extension",
-  "version": "26.1.24",
+  "version": "26.1.27",
   "publisher": "paviko",
   "author": {
     "name": "paviko"

+ 11 - 13
hosts/vscode-plugin/resources/webview/index.html

@@ -178,7 +178,7 @@
           // Handle keyboard events from iframe (macOS fix)
           if (message && (message.type === "keydown-event" || message.type === "keyup-event")) {
             try {
-              const { ctrlKey, metaKey, shiftKey, altKey, code, key, hasSelection } = message.payload
+              const { ctrlKey, metaKey, shiftKey, altKey, code, key, hasSelection, inEditable } = message.payload
 
               // Handle specific VSCode shortcuts that need to be forwarded
               if (message.type === "keydown-event") {
@@ -254,21 +254,19 @@
                   return
                 }
 
-                // Copy (Cmd+C) - macOS only
-                if (metaKey && code === "KeyC") {
-                  window.vscode.postMessage({
-                    type: "executeCommand",
-                    command: "editor.action.clipboardCopyAction",
-                  })
+                // Copy/Cut/Paste: forward to VSCode only when iframe isn't editing
+                if (metaKey && code === "KeyC" && !inEditable && !hasSelection) {
+                  window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardCopyAction" })
                   return
                 }
 
-                // Paste (Cmd+V) - macOS only
-                if (metaKey && code === "KeyV") {
-                  window.vscode.postMessage({
-                    type: "executeCommand",
-                    command: "editor.action.clipboardPasteAction",
-                  })
+                if (metaKey && code === "KeyX" && !inEditable && !hasSelection) {
+                  window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardCutAction" })
+                  return
+                }
+
+                if (metaKey && code === "KeyV" && !inEditable) {
+                  window.vscode.postMessage({ type: "executeCommand", command: "editor.action.clipboardPasteAction" })
                   return
                 }
               }

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

@@ -543,6 +543,7 @@ export class CommunicationBridge implements PluginCommunicator {
                   "redo",
                   // Clipboard actions for macOS handling
                   "editor.action.clipboardCopyAction",
+                  "editor.action.clipboardCutAction",
                   "editor.action.clipboardPasteAction",
                 ])
                 const cmd = command as string // safe after type guard above

+ 10 - 0
packages/opencode/webgui/src/App.tsx

@@ -15,6 +15,7 @@ import { KeyboardShortcutsHelp } from "./components/KeyboardShortcutsHelp"
 import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"
 import { ideBridge } from "./lib/ideBridge"
 import { extractPathsFromDrop } from "./lib/dnd"
+import { initKeyboardHandler, destroyKeyboardHandler } from "./lib/keyboardHandler"
 
 const isMac = typeof navigator !== "undefined" && navigator.platform.includes("Mac")
 
@@ -68,6 +69,15 @@ function AppInner({ connectionState }: { connectionState: ConnectionState }) {
     isModalOpen: isAnyModalOpen,
   })
 
+  // Fix Cmd/Ctrl clipboard shortcuts in VSCode webview iframe (macOS)
+  useEffect(() => {
+    const handler = initKeyboardHandler()
+    return () => {
+      handler.destroy()
+      destroyKeyboardHandler()
+    }
+  }, [])
+
   // Host → UI bridge messages
   useEffect(() => {
     const handler = (msg: any) => {

+ 11 - 0
packages/opencode/webgui/src/components/MessageInput/hooks/useEditorKeyboard.ts

@@ -31,6 +31,15 @@ export function useEditorKeyboard({ editor, contentEditableRef, parseWithRange,
     const el = contentEditableRef.current
     if (!el) return
 
+    const onPasteText = (e: Event) => {
+      const ev = e as CustomEvent<{ text?: string }>
+      const text = ev.detail?.text
+      if (!text) return
+      e.preventDefault()
+      e.stopPropagation()
+      insertPlainWithMentionsImpl(editor, parseWithRange, text)
+    }
+
     const onPaste = (e: ClipboardEvent) => {
       if (!e.clipboardData) return
       const plain = e.clipboardData.getData("text/plain")
@@ -40,8 +49,10 @@ export function useEditorKeyboard({ editor, contentEditableRef, parseWithRange,
       insertPlainWithMentionsImpl(editor, parseWithRange, plain)
     }
 
+    el.addEventListener("opencode:paste-text", onPasteText as any, true)
     el.addEventListener("paste", onPaste as any, true)
     return () => {
+      el.removeEventListener("opencode:paste-text", onPasteText as any, true)
       el.removeEventListener("paste", onPaste as any, true)
     }
   }, [contentEditableRef.current, editor, parseWithRange])

+ 226 - 0
packages/opencode/webgui/src/lib/keyboardHandler.ts

@@ -0,0 +1,226 @@
+type KeyPayload = {
+  ctrlKey: boolean
+  metaKey: boolean
+  shiftKey: boolean
+  altKey: boolean
+  code: string
+  key: string
+  hasSelection: boolean
+  inEditable: boolean
+}
+
+function isIframe(): boolean {
+  try {
+    return window.parent !== window
+  } catch {
+    return false
+  }
+}
+
+function isEditableElement(el: Element | null): boolean {
+  if (!el) return false
+  if (el instanceof HTMLInputElement) return !el.readOnly && !el.disabled
+  if (el instanceof HTMLTextAreaElement) return !el.readOnly && !el.disabled
+  if (el instanceof HTMLElement) {
+    if (el.isContentEditable) return true
+    if (el.closest('[contenteditable="true"]')) return true
+  }
+  return false
+}
+
+function selectionLengthInInput(el: Element | null): number {
+  if (!el) return 0
+  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
+    const start = el.selectionStart ?? 0
+    const end = el.selectionEnd ?? 0
+    return Math.abs(end - start)
+  }
+  return 0
+}
+
+function hasDomSelection(): boolean {
+  const sel = window.getSelection?.()
+  if (!sel) return false
+  if (sel.isCollapsed) return false
+  return (sel.toString() || "").length > 0
+}
+
+function computeState(): { inEditable: boolean; hasSelection: boolean; active: Element | null } {
+  const active = document.activeElement
+  const inEditable = isEditableElement(active)
+  const hasSelection = selectionLengthInInput(active) > 0 || hasDomSelection()
+  return { inEditable, hasSelection, active }
+}
+
+function forwardToParent(type: "keydown-event" | "keyup-event", payload: KeyPayload) {
+  try {
+    if (!isIframe()) return
+    window.parent.postMessage({ type, payload }, "*")
+  } catch {}
+}
+
+function stop(ev: KeyboardEvent) {
+  ev.preventDefault()
+  ev.stopPropagation()
+  try {
+    ev.stopImmediatePropagation()
+  } catch {}
+}
+
+function dispatchPasteText(target: Element | null, text: string): boolean {
+  try {
+    const ev = new CustomEvent("opencode:paste-text", {
+      detail: { text },
+      bubbles: true,
+      cancelable: true,
+    })
+    ;(target ?? document.body).dispatchEvent(ev)
+    return ev.defaultPrevented
+  } catch {
+    return false
+  }
+}
+
+export class KeyboardHandler {
+  private onKeyDown: ((ev: KeyboardEvent) => void) | null = null
+  private onKeyUp: ((ev: KeyboardEvent) => void) | null = null
+  private onMessage: ((ev: MessageEvent) => void) | null = null
+
+  constructor() {
+    this.install()
+  }
+
+  private install() {
+    if (!isIframe()) return
+
+    this.onMessage = (ev: MessageEvent) => {
+      const msg = ev.data as any
+      if (!msg || typeof msg !== "object") return
+      if (msg.type !== "setParentOrigin") return
+      // We currently postMessage with '*' targetOrigin; parent validates our origin.
+      // Keeping this handler allows a future tighten-up without breaking protocol.
+    }
+    window.addEventListener("message", this.onMessage)
+
+    this.onKeyDown = (ev: KeyboardEvent) => {
+      const mod = ev.metaKey || ev.ctrlKey
+      const { inEditable, hasSelection, active } = computeState()
+
+      if (mod && (ev.code === "KeyC" || ev.code === "KeyX" || ev.code === "KeyV")) {
+        // Only take over clipboard shortcuts when the iframe is the intended target.
+        // Otherwise, let VSCode handle them (forwarded below).
+        if (inEditable || hasSelection) {
+          if (ev.code === "KeyC") {
+            try {
+              document.execCommand("copy")
+            } catch {}
+            stop(ev)
+            return
+          }
+
+          if (ev.code === "KeyX") {
+            try {
+              document.execCommand("cut")
+            } catch {}
+            stop(ev)
+            return
+          }
+
+          if (ev.code === "KeyV") {
+            const insert = async () => {
+              try {
+                const clip = navigator.clipboard
+                if (clip?.readText) {
+                  const text = await clip.readText()
+                  if (text) {
+                    const handled = dispatchPasteText(active, text)
+                    if (handled) return
+                    try {
+                      document.execCommand("insertText", false, text)
+                    } catch {}
+                    return
+                  }
+                }
+              } catch {}
+              try {
+                document.execCommand("paste")
+              } catch {}
+            }
+            stop(ev)
+            void insert()
+            return
+          }
+        }
+      }
+
+      if (mod && ev.code === "KeyA" && inEditable) {
+        try {
+          document.execCommand("selectAll")
+        } catch {}
+        stop(ev)
+        return
+      }
+
+      if (mod && ev.code === "KeyZ" && inEditable) {
+        try {
+          document.execCommand(ev.shiftKey ? "redo" : "undo")
+        } catch {}
+        stop(ev)
+        return
+      }
+
+      const payload: KeyPayload = {
+        ctrlKey: ev.ctrlKey,
+        metaKey: ev.metaKey,
+        shiftKey: ev.shiftKey,
+        altKey: ev.altKey,
+        code: ev.code,
+        key: ev.key,
+        hasSelection,
+        inEditable,
+      }
+      if (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) forwardToParent("keydown-event", payload)
+    }
+
+    this.onKeyUp = (ev: KeyboardEvent) => {
+      const { inEditable, hasSelection } = computeState()
+      const payload: KeyPayload = {
+        ctrlKey: ev.ctrlKey,
+        metaKey: ev.metaKey,
+        shiftKey: ev.shiftKey,
+        altKey: ev.altKey,
+        code: ev.code,
+        key: ev.key,
+        hasSelection,
+        inEditable,
+      }
+      if (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) forwardToParent("keyup-event", payload)
+    }
+
+    // Capture phase so we run before VSCode webview eats the event.
+    window.addEventListener("keydown", this.onKeyDown, true)
+    window.addEventListener("keyup", this.onKeyUp, true)
+  }
+
+  destroy() {
+    if (this.onMessage) window.removeEventListener("message", this.onMessage)
+    if (this.onKeyDown) window.removeEventListener("keydown", this.onKeyDown, true)
+    if (this.onKeyUp) window.removeEventListener("keyup", this.onKeyUp, true)
+    this.onMessage = null
+    this.onKeyDown = null
+    this.onKeyUp = null
+  }
+}
+
+let instance: KeyboardHandler | null = null
+
+export function initKeyboardHandler() {
+  if (instance) return instance
+  instance = new KeyboardHandler()
+  return instance
+}
+
+export function destroyKeyboardHandler() {
+  instance?.destroy()
+  instance = null
+}