Преглед изворни кода

Add modified files panel and fix Model/Agent selector placement

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

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

@@ -3,6 +3,8 @@
 <h3>2025.12.xx</h3>
 <ul>
   <li>Auto refresh files in IDE on edit/write</li>
+  <li>New panel with all modified files in session</li>
+  <li>Fixed placement of Model/Agent selector</li>
 </ul>
 
 <h3>2025.11.30</h3>

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

@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [25.12.xx] - 2025-12-xx
 
 - Auto refresh files in IDE on edit/write
+- New panel with all modified files in session
+- Fixed placement of Model/Agent selector
 
 ## [25.11.30] - 2025-11-30
 

+ 21 - 7
packages/opencode/webgui/src/components/MessageInput/EditorToolbar.tsx

@@ -1,6 +1,7 @@
 import { ModelSelector } from "../ModelSelector"
 import { AgentSelector } from "../AgentSelector"
 import { IconButton } from "../common"
+import { MessageActions } from "./MessageActions"
 
 interface EditorToolbarProps {
   selectedProviderId: string | undefined
@@ -15,6 +16,12 @@ interface EditorToolbarProps {
   onRetry: () => void
   fileInputRef: React.RefObject<HTMLInputElement | null>
   onFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void
+  isIdle: boolean
+  isButtonDisabled: boolean
+  isCompactDisabled: boolean
+  onSubmit: () => void
+  onAbort: () => void
+  onCompactClick: () => void
 }
 
 export function EditorToolbar({
@@ -30,11 +37,16 @@ export function EditorToolbar({
   onRetry,
   fileInputRef,
   onFileChange,
+  isIdle,
+  isButtonDisabled,
+  isCompactDisabled,
+  onSubmit,
+  onAbort,
+  onCompactClick,
 }: EditorToolbarProps) {
   return (
     <div className="h-8 px-2 flex items-center justify-between border-t border-gray-100 dark:border-gray-800">
       <div className="flex items-center gap-1">
-        {/* Retry button (visible when there's a failed message) */}
         {lastFailedMessage && (
           <button
             onClick={onRetry}
@@ -52,8 +64,6 @@ export function EditorToolbar({
             Retry
           </button>
         )}
-
-        {/* Model selector */}
         <ModelSelector
           key={modelSelectorKey}
           selectedProviderId={selectedProviderId}
@@ -61,11 +71,7 @@ export function EditorToolbar({
           onSelect={onModelSelect}
           disabled={isDisabled}
         />
-
-        {/* Agent selector */}
         <AgentSelector selectedAgent={selectedAgent} onSelect={onAgentSelect} disabled={isDisabled} />
-
-        {/* Add file button */}
         <IconButton
           onClick={onFileSelect}
           size="sm"
@@ -92,6 +98,14 @@ export function EditorToolbar({
           className="hidden"
         />
       </div>
+      <MessageActions
+        isIdle={isIdle}
+        isButtonDisabled={isButtonDisabled}
+        isCompactDisabled={isCompactDisabled}
+        onSubmit={onSubmit}
+        onAbort={onAbort}
+        onCompactClick={onCompactClick}
+      />
     </div>
   )
 }

+ 91 - 0
packages/opencode/webgui/src/components/MessageInput/ModifiedFilesPanel.tsx

@@ -0,0 +1,91 @@
+import { useState, useMemo, useCallback, type KeyboardEvent } from "react"
+import { useMessages } from "../../state/MessagesContext"
+import { useOpenFile } from "../../hooks/useOpenFile"
+
+interface ModifiedFilesPanelProps {
+  sessionID: string | null
+}
+
+export function ModifiedFilesPanel({ sessionID }: ModifiedFilesPanelProps) {
+  const [expanded, setExpanded] = useState(false)
+  const { getMessagesBySession } = useMessages()
+  const openFile = useOpenFile()
+
+  const modifiedFiles = useMemo(() => {
+    if (!sessionID) return []
+    const messages = getMessagesBySession(sessionID)
+    const files: string[] = []
+    const seen = new Set<string>()
+
+    for (const msg of messages) {
+      for (const part of msg.parts) {
+        if (part.type !== "tool") continue
+        const toolPart = part as { tool?: string; state?: { input?: { filePath?: string } } }
+        if (toolPart.tool !== "write" && toolPart.tool !== "edit") continue
+        const path = toolPart.state?.input?.filePath
+        if (path && !seen.has(path)) {
+          seen.add(path)
+          files.push(path)
+        }
+      }
+    }
+    return files
+  }, [sessionID, getMessagesBySession])
+
+  const handleFileClick = useCallback(
+    (path: string) => {
+      openFile({ path })
+    },
+    [openFile],
+  )
+
+  const handleKeyDown = useCallback(
+    (path: string) => (e: KeyboardEvent) => {
+      if (e.key === "Enter" || e.key === " ") {
+        e.preventDefault()
+        handleFileClick(path)
+      }
+    },
+    [handleFileClick],
+  )
+
+  if (modifiedFiles.length === 0) return null
+
+  return (
+    <div className="px-2 py-1 border-b border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-900/50">
+      <div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
+        <button
+          onClick={() => setExpanded(!expanded)}
+          className="flex items-center gap-1 hover:text-gray-900 dark:hover:text-gray-200"
+        >
+          <svg
+            className={`w-3 h-3 transition-transform ${expanded ? "rotate-90" : ""}`}
+            fill="none"
+            stroke="currentColor"
+            viewBox="0 0 24 24"
+          >
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
+          </svg>
+          <span>{modifiedFiles.length} file{modifiedFiles.length !== 1 ? "s" : ""} changed</span>
+        </button>
+      </div>
+      {expanded && (
+        <div className="mt-1 flex flex-wrap gap-1">
+          {modifiedFiles.map((path) => (
+            <span
+              key={path}
+              role="button"
+              tabIndex={0}
+              onClick={() => handleFileClick(path)}
+              onKeyDown={handleKeyDown(path)}
+              className="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs font-mono bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded cursor-pointer hover:bg-blue-200 dark:hover:bg-blue-900/60"
+              title={path}
+            >
+              {path.split("/").pop()}
+            </span>
+          ))}
+        </div>
+      )}
+    </div>
+  )
+}

+ 8 - 11
packages/opencode/webgui/src/components/MessageInput/index.tsx

@@ -11,7 +11,7 @@ import { ConfirmModal } from "../ConfirmModal"
 import { createEditorConfig } from "./EditorConfig"
 import { EditorContent } from "./EditorContent"
 import { EditorToolbar } from "./EditorToolbar"
-import { MessageActions } from "./MessageActions"
+import { ModifiedFilesPanel } from "./ModifiedFilesPanel"
 import { useMessageInput } from "./hooks/useMessageInput"
 import { useFileAttachment } from "./hooks/useFileAttachment"
 import { useDragDrop } from "./hooks/useDragDrop"
@@ -250,6 +250,7 @@ const MessageInputInner = forwardRef<
   return (
     <>
       <footer className="border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 flex-shrink-0">
+        <ModifiedFilesPanel sessionID={sessionID} />
         <EditorContent
           contentEditableRef={contentEditableRef}
           containerRef={containerRef}
@@ -268,17 +269,13 @@ const MessageInputInner = forwardRef<
           onRetry={handleRetry}
           fileInputRef={fileInputRef}
           onFileChange={handleFileChange}
+          isIdle={isIdle}
+          isButtonDisabled={isButtonDisabled}
+          isCompactDisabled={isCompactDisabled}
+          onSubmit={handleSubmit}
+          onAbort={handleAbort}
+          onCompactClick={() => setIsCompactConfirmOpen(true)}
         />
-        <div className="h-8 px-2 flex items-center justify-end border-t border-gray-100 dark:border-gray-800">
-          <MessageActions
-            isIdle={isIdle}
-            isButtonDisabled={isButtonDisabled}
-            isCompactDisabled={isCompactDisabled}
-            onSubmit={handleSubmit}
-            onAbort={handleAbort}
-            onCompactClick={() => setIsCompactConfirmOpen(true)}
-          />
-        </div>
       </footer>
 
       <ConfirmModal