Browse Source

feat(desktop): better affordance for auto-accept

Adam 1 month ago
parent
commit
d4a2652eda

+ 27 - 27
packages/app/src/app.tsx

@@ -67,36 +67,36 @@ export function App() {
                     <ServerKey>
                     <ServerKey>
                       <GlobalSDKProvider>
                       <GlobalSDKProvider>
                         <GlobalSyncProvider>
                         <GlobalSyncProvider>
-                          <PermissionProvider>
-                            <LayoutProvider>
-                              <NotificationProvider>
-                                <Router
-                                  root={(props) => (
+                          <Router
+                            root={(props) => (
+                              <PermissionProvider>
+                                <LayoutProvider>
+                                  <NotificationProvider>
                                     <CommandProvider>
                                     <CommandProvider>
                                       <Layout>{props.children}</Layout>
                                       <Layout>{props.children}</Layout>
                                     </CommandProvider>
                                     </CommandProvider>
-                                  )}
-                                >
-                                  <Route path="/" component={Home} />
-                                  <Route path="/:dir" component={DirectoryLayout}>
-                                    <Route path="/" component={() => <Navigate href="session" />} />
-                                    <Route
-                                      path="/session/:id?"
-                                      component={(p) => (
-                                        <Show when={p.params.id ?? "new"} keyed>
-                                          <TerminalProvider>
-                                            <PromptProvider>
-                                              <Session />
-                                            </PromptProvider>
-                                          </TerminalProvider>
-                                        </Show>
-                                      )}
-                                    />
-                                  </Route>
-                                </Router>
-                              </NotificationProvider>
-                            </LayoutProvider>
-                          </PermissionProvider>
+                                  </NotificationProvider>
+                                </LayoutProvider>
+                              </PermissionProvider>
+                            )}
+                          >
+                            <Route path="/" component={Home} />
+                            <Route path="/:dir" component={DirectoryLayout}>
+                              <Route path="/" component={() => <Navigate href="session" />} />
+                              <Route
+                                path="/session/:id?"
+                                component={(p) => (
+                                  <Show when={p.params.id ?? "new"} keyed>
+                                    <TerminalProvider>
+                                      <PromptProvider>
+                                        <Session />
+                                      </PromptProvider>
+                                    </TerminalProvider>
+                                  </Show>
+                                )}
+                              />
+                            </Route>
+                          </Router>
                         </GlobalSyncProvider>
                         </GlobalSyncProvider>
                       </GlobalSDKProvider>
                       </GlobalSDKProvider>
                     </ServerKey>
                     </ServerKey>

+ 32 - 3
packages/app/src/components/prompt-input.tsx

@@ -23,6 +23,7 @@ import { useCommand } from "@/context/command"
 import { persisted } from "@/utils/persist"
 import { persisted } from "@/utils/persist"
 import { Identifier } from "@/utils/id"
 import { Identifier } from "@/utils/id"
 import { SessionContextUsage } from "@/components/session-context-usage"
 import { SessionContextUsage } from "@/components/session-context-usage"
+import { usePermission } from "@/context/permission"
 
 
 const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
 const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
 const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
 const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
@@ -80,6 +81,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   const dialog = useDialog()
   const dialog = useDialog()
   const providers = useProviders()
   const providers = useProviders()
   const command = useCommand()
   const command = useCommand()
+  const permission = usePermission()
   let editorRef!: HTMLDivElement
   let editorRef!: HTMLDivElement
   let fileInputRef!: HTMLInputElement
   let fileInputRef!: HTMLInputElement
   let scrollRef!: HTMLDivElement
   let scrollRef!: HTMLDivElement
@@ -1346,7 +1348,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
           </Show>
           </Show>
         </div>
         </div>
         <div class="relative p-3 flex items-center justify-between">
         <div class="relative p-3 flex items-center justify-between">
-          <div class="flex items-center justify-start gap-1">
+          <div class="flex items-center justify-start gap-0.5">
             <Switch>
             <Switch>
               <Match when={store.mode === "shell"}>
               <Match when={store.mode === "shell"}>
                 <div class="flex items-center gap-2 px-2 h-6">
                 <div class="flex items-center gap-2 px-2 h-6">
@@ -1393,16 +1395,43 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                   </Button>
                   </Button>
                 </Tooltip>
                 </Tooltip>
                 <Show when={local.model.variant.list().length > 0}>
                 <Show when={local.model.variant.list().length > 0}>
-                  <TooltipKeybind placement="top" title="Thinking effort" keybind={command.keybind("model.variant")}>
+                  <TooltipKeybind
+                    placement="top"
+                    title="Thinking effort"
+                    keybind={command.keybind("model.variant.cycle")}
+                  >
                     <Button
                     <Button
                       variant="ghost"
                       variant="ghost"
+                      class="text-text-base _hidden group-hover/prompt-input:inline-block"
                       onClick={() => local.model.variant.cycle()}
                       onClick={() => local.model.variant.cycle()}
-                      class="text-text-base hidden group-hover/prompt-input:inline-block"
                     >
                     >
                       <span class="capitalize text-12-regular">{local.model.variant.current() ?? "Default"}</span>
                       <span class="capitalize text-12-regular">{local.model.variant.current() ?? "Default"}</span>
                     </Button>
                     </Button>
                   </TooltipKeybind>
                   </TooltipKeybind>
                 </Show>
                 </Show>
+                <Show when={permission.permissionsEnabled() && params.id}>
+                  <TooltipKeybind
+                    placement="top"
+                    title="Auto-accept edits"
+                    keybind={command.keybind("permissions.autoaccept")}
+                  >
+                    <Button
+                      variant="ghost"
+                      onClick={() => permission.toggleAutoAccept(params.id!, sdk.directory)}
+                      classList={{
+                        "_hidden group-hover/prompt-input:flex size-6 items-center justify-center": true,
+                        "text-text-base": !permission.isAutoAccepting(params.id!),
+                        "bg-surface-success-base": permission.isAutoAccepting(params.id!),
+                      }}
+                    >
+                      <Icon
+                        name="chevron-double-right"
+                        size="small"
+                        classList={{ "text-icon-success-base": permission.isAutoAccepting(params.id!) }}
+                      />
+                    </Button>
+                  </TooltipKeybind>
+                </Show>
               </Match>
               </Match>
             </Switch>
             </Switch>
           </div>
           </div>

+ 13 - 1
packages/app/src/context/permission.tsx

@@ -1,9 +1,11 @@
-import { onCleanup } from "solid-js"
+import { createMemo, onCleanup } from "solid-js"
 import { createStore } from "solid-js/store"
 import { createStore } from "solid-js/store"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import { createSimpleContext } from "@opencode-ai/ui/context"
 import type { Permission } from "@opencode-ai/sdk/v2/client"
 import type { Permission } from "@opencode-ai/sdk/v2/client"
 import { persisted } from "@/utils/persist"
 import { persisted } from "@/utils/persist"
 import { useGlobalSDK } from "@/context/global-sdk"
 import { useGlobalSDK } from "@/context/global-sdk"
+import { useGlobalSync } from "./global-sync"
+import { useParams } from "@solidjs/router"
 
 
 type PermissionRespondFn = (input: {
 type PermissionRespondFn = (input: {
   sessionID: string
   sessionID: string
@@ -21,7 +23,16 @@ function shouldAutoAccept(perm: Permission) {
 export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
 export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({
   name: "Permission",
   name: "Permission",
   init: () => {
   init: () => {
+    const params = useParams()
     const globalSDK = useGlobalSDK()
     const globalSDK = useGlobalSDK()
+    const globalSync = useGlobalSync()
+
+    const permissionsEnabled = createMemo(() => {
+      if (!params.dir) return false
+      const [store] = globalSync.child(params.dir)
+      return store.config.permission !== undefined
+    })
+
     const [store, setStore, _, ready] = persisted(
     const [store, setStore, _, ready] = persisted(
       "permission.v3",
       "permission.v3",
       createStore({
       createStore({
@@ -106,6 +117,7 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
       disableAutoAccept(sessionID: string) {
       disableAutoAccept(sessionID: string) {
         disable(sessionID)
         disable(sessionID)
       },
       },
+      permissionsEnabled,
     }
     }
   },
   },
 })
 })

+ 16 - 17
packages/app/src/pages/session.tsx

@@ -447,21 +447,6 @@ export default function Page() {
       slash: "open",
       slash: "open",
       onSelect: () => dialog.show(() => <DialogSelectFile />),
       onSelect: () => dialog.show(() => <DialogSelectFile />),
     },
     },
-    // {
-    //   id: "theme.toggle",
-    //   title: "Toggle theme",
-    //   description: "Switch between themes",
-    //   category: "View",
-    //   keybind: "ctrl+t",
-    //   slash: "theme",
-    //   onSelect: () => {
-    //     const currentTheme = localStorage.getItem("theme") ?? "oc-1"
-    //     const themes = ["oc-1", "oc-2-paper"]
-    //     const nextTheme = themes[(themes.indexOf(currentTheme) + 1) % themes.length]
-    //     localStorage.setItem("theme", nextTheme)
-    //     document.documentElement.setAttribute("data-theme", nextTheme)
-    //   },
-    // },
     {
     {
       id: "terminal.toggle",
       id: "terminal.toggle",
       title: "Toggle terminal",
       title: "Toggle terminal",
@@ -550,15 +535,29 @@ export default function Page() {
       keybind: "shift+mod+.",
       keybind: "shift+mod+.",
       onSelect: () => local.agent.move(-1),
       onSelect: () => local.agent.move(-1),
     },
     },
+    {
+      id: "model.variant.cycle",
+      title: "Cycle thinking effort",
+      description: "Switch to the next effort level",
+      category: "Model",
+      keybind: "shift+mod+t",
+      onSelect: () => {
+        local.model.variant.cycle()
+        showToast({
+          title: "Thinking effort changed",
+          description: "The thinking effort has been changed to " + (local.model.variant.current() ?? "Default"),
+        })
+      },
+    },
     {
     {
       id: "permissions.autoaccept",
       id: "permissions.autoaccept",
       title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits",
       title: params.id && permission.isAutoAccepting(params.id) ? "Stop auto-accepting edits" : "Auto-accept edits",
       category: "Permissions",
       category: "Permissions",
-      disabled: !params.id,
+      keybind: "mod+shift+a",
+      disabled: !params.id || !permission.permissionsEnabled(),
       onSelect: () => {
       onSelect: () => {
         const sessionID = params.id
         const sessionID = params.id
         if (!sessionID) return
         if (!sessionID) return
-
         permission.toggleAutoAccept(sessionID, sdk.directory)
         permission.toggleAutoAccept(sessionID, sdk.directory)
         showToast({
         showToast({
           title: permission.isAutoAccepting(sessionID) ? "Auto-accepting edits" : "Stopped auto-accepting edits",
           title: permission.isAutoAccepting(sessionID) ? "Auto-accepting edits" : "Stopped auto-accepting edits",

+ 1 - 0
packages/ui/src/components/icon.tsx

@@ -12,6 +12,7 @@ const icons = {
   "chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
   "chevron-down": `<path d="M6.6665 8.33325L9.99984 11.6666L13.3332 8.33325" stroke="currentColor" stroke-linecap="square"/>`,
   "chevron-right": `<path d="M8.33301 13.3327L11.6663 9.99935L8.33301 6.66602" stroke="currentColor" stroke-linecap="square"/>`,
   "chevron-right": `<path d="M8.33301 13.3327L11.6663 9.99935L8.33301 6.66602" stroke="currentColor" stroke-linecap="square"/>`,
   "chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
   "chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
+  "chevron-double-right": `<path d="M11.6654 13.3346L14.9987 10.0013L11.6654 6.66797M5.83203 13.3346L9.16536 10.0013L5.83203 6.66797" stroke="currentColor" stroke-linecap="square"/>`,
   "circle-x": `<path fill-rule="evenodd" clip-rule="evenodd" d="M1.6665 10.0003C1.6665 5.39795 5.39746 1.66699 9.99984 1.66699C14.6022 1.66699 18.3332 5.39795 18.3332 10.0003C18.3332 14.6027 14.6022 18.3337 9.99984 18.3337C5.39746 18.3337 1.6665 14.6027 1.6665 10.0003ZM7.49984 6.91107L6.91058 7.50033L9.41058 10.0003L6.91058 12.5003L7.49984 13.0896L9.99984 10.5896L12.4998 13.0896L13.0891 12.5003L10.5891 10.0003L13.0891 7.50033L12.4998 6.91107L9.99984 9.41107L7.49984 6.91107Z" fill="currentColor"/>`,
   "circle-x": `<path fill-rule="evenodd" clip-rule="evenodd" d="M1.6665 10.0003C1.6665 5.39795 5.39746 1.66699 9.99984 1.66699C14.6022 1.66699 18.3332 5.39795 18.3332 10.0003C18.3332 14.6027 14.6022 18.3337 9.99984 18.3337C5.39746 18.3337 1.6665 14.6027 1.6665 10.0003ZM7.49984 6.91107L6.91058 7.50033L9.41058 10.0003L6.91058 12.5003L7.49984 13.0896L9.99984 10.5896L12.4998 13.0896L13.0891 12.5003L10.5891 10.0003L13.0891 7.50033L12.4998 6.91107L9.99984 9.41107L7.49984 6.91107Z" fill="currentColor"/>`,
   close: `<path d="M3.75 3.75L16.25 16.25M16.25 3.75L3.75 16.25" stroke="currentColor" stroke-linecap="square"/>`,
   close: `<path d="M3.75 3.75L16.25 16.25M16.25 3.75L3.75 16.25" stroke="currentColor" stroke-linecap="square"/>`,
   checklist: `<path d="M9.58342 13.7498H17.0834M9.58342 6.24984H17.0834M2.91675 6.6665L4.58341 7.9165L7.08341 4.1665M2.91675 14.1665L4.58341 15.4165L7.08341 11.6665" stroke="currentColor" stroke-linecap="square"/>`,
   checklist: `<path d="M9.58342 13.7498H17.0834M9.58342 6.24984H17.0834M2.91675 6.6665L4.58341 7.9165L7.08341 4.1665M2.91675 14.1665L4.58341 15.4165L7.08341 11.6665" stroke="currentColor" stroke-linecap="square"/>`,