Selaa lähdekoodia

tweak(ui): bump button heights and align permission prompt layout

David Hill 6 kuukautta sitten
vanhempi
commit
b784c923a8

+ 74 - 54
packages/app/src/pages/session/session-prompt-dock.tsx

@@ -1,7 +1,6 @@
 import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
 import type { QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
 import { Button } from "@opencode-ai/ui/button"
-import { BasicTool } from "@opencode-ai/ui/basic-tool"
 import { PromptInput } from "@/components/prompt-input"
 import { QuestionDock } from "@/components/question-dock"
 import { SessionTodoDock } from "@/components/session-todo-dock"
@@ -123,63 +122,84 @@ export function SessionPromptDock(props: {
         </Show>
 
         <Show when={props.permissionRequest()} keyed>
-          {(perm) => (
-            <div data-component="tool-part-wrapper" data-permission="true" class="mb-3">
-              <BasicTool
-                icon="checklist"
-                locked
-                defaultOpen
-                trigger={{
-                  title: props.t("notification.permission.title"),
-                  subtitle:
-                    perm.permission === "doom_loop"
-                      ? props.t("settings.permissions.tool.doom_loop.title")
-                      : perm.permission,
-                }}
-              >
-                <Show when={perm.patterns.length > 0}>
-                  <div class="flex flex-col gap-1 py-2 px-3 max-h-40 overflow-y-auto no-scrollbar">
-                    <For each={perm.patterns}>
-                      {(pattern) => <code class="text-12-regular text-text-base break-all">{pattern}</code>}
-                    </For>
+          {(perm) => {
+            const toolTitle = () => {
+              const key = `settings.permissions.tool.${perm.permission}.title`
+              const value = props.t(key)
+              if (value === key) return perm.permission
+              return value
+            }
+
+            const toolDescription = () => {
+              const key = `settings.permissions.tool.${perm.permission}.description`
+              const value = props.t(key)
+              if (value === key) return ""
+              return value
+            }
+
+            return (
+              <div>
+                <div data-component="question-prompt" data-permission="true">
+                  <div data-slot="question-body">
+                    <div data-slot="question-header">
+                      <div data-slot="question-header-title">
+                        {props.t("notification.permission.title")}{" "}
+                        <span class="text-13-regular text-text-weak">{toolTitle()}</span>
+                      </div>
+                    </div>
+
+                    <div data-slot="question-content">
+                      <Show when={toolDescription()}>
+                        <div data-slot="question-hint">{toolDescription()}</div>
+                      </Show>
+
+                      <Show when={perm.patterns.length > 0}>
+                        <div data-slot="question-options">
+                          <For each={perm.patterns}>
+                            {(pattern) => (
+                              <div class="px-[10px]">
+                                <code class="text-12-regular text-text-base break-all">{pattern}</code>
+                              </div>
+                            )}
+                          </For>
+                        </div>
+                      </Show>
+                    </div>
                   </div>
-                </Show>
-                <Show when={perm.permission === "doom_loop"}>
-                  <div class="text-12-regular text-text-weak pb-2 px-3">
-                    {props.t("settings.permissions.tool.doom_loop.description")}
+
+                  <div data-slot="question-footer">
+                    <div />
+                    <div data-slot="question-footer-actions">
+                      <Button
+                        variant="ghost"
+                        size="normal"
+                        onClick={() => props.onDecide("reject")}
+                        disabled={props.responding}
+                      >
+                        {props.t("ui.permission.deny")}
+                      </Button>
+                      <Button
+                        variant="secondary"
+                        size="normal"
+                        onClick={() => props.onDecide("always")}
+                        disabled={props.responding}
+                      >
+                        {props.t("ui.permission.allowAlways")}
+                      </Button>
+                      <Button
+                        variant="primary"
+                        size="normal"
+                        onClick={() => props.onDecide("once")}
+                        disabled={props.responding}
+                      >
+                        {props.t("ui.permission.allowOnce")}
+                      </Button>
+                    </div>
                   </div>
-                </Show>
-              </BasicTool>
-              <div data-component="permission-prompt">
-                <div data-slot="permission-actions">
-                  <Button
-                    variant="ghost"
-                    size="small"
-                    onClick={() => props.onDecide("reject")}
-                    disabled={props.responding}
-                  >
-                    {props.t("ui.permission.deny")}
-                  </Button>
-                  <Button
-                    variant="secondary"
-                    size="small"
-                    onClick={() => props.onDecide("always")}
-                    disabled={props.responding}
-                  >
-                    {props.t("ui.permission.allowAlways")}
-                  </Button>
-                  <Button
-                    variant="primary"
-                    size="small"
-                    onClick={() => props.onDecide("once")}
-                    disabled={props.responding}
-                  >
-                    {props.t("ui.permission.allowOnce")}
-                  </Button>
                 </div>
               </div>
-            </div>
-          )}
+            )
+          }}
         </Show>
 
         <Show when={!props.blocked}>

+ 243 - 75
packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx

@@ -64,10 +64,6 @@ function EditBody(props: { request: PermissionRequest }) {
 
   return (
     <box flexDirection="column" gap={1}>
-      <box flexDirection="row" gap={1} paddingLeft={1}>
-        <text fg={theme.textMuted}>{"→"}</text>
-        <text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text>
-      </box>
       <Show when={diff()}>
         <scrollbox height="100%">
           <diff
@@ -91,6 +87,11 @@ function EditBody(props: { request: PermissionRequest }) {
           />
         </scrollbox>
       </Show>
+      <Show when={!diff()}>
+        <box paddingLeft={1}>
+          <text fg={theme.textMuted}>No diff provided</text>
+        </box>
+      </Show>
     </box>
   )
 }
@@ -194,76 +195,233 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
       </Match>
       <Match when={store.stage === "permission"}>
         {(() => {
+          const info = () => {
+            const permission = props.request.permission
+            const data = input()
+
+            if (permission === "edit") {
+              const raw = props.request.metadata?.filepath
+              const filepath = typeof raw === "string" ? raw : ""
+              return {
+                icon: "→",
+                title: `Edit ${normalizePath(filepath)}`,
+                body: <EditBody request={props.request} />,
+              }
+            }
+
+            if (permission === "read") {
+              const raw = data.filePath
+              const filePath = typeof raw === "string" ? raw : ""
+              return {
+                icon: "→",
+                title: `Read ${normalizePath(filePath)}`,
+                body: (
+                  <Show when={filePath}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.textMuted}>{"Path: " + normalizePath(filePath)}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "glob") {
+              const pattern = typeof data.pattern === "string" ? data.pattern : ""
+              return {
+                icon: "✱",
+                title: `Glob "${pattern}"`,
+                body: (
+                  <Show when={pattern}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "grep") {
+              const pattern = typeof data.pattern === "string" ? data.pattern : ""
+              return {
+                icon: "✱",
+                title: `Grep "${pattern}"`,
+                body: (
+                  <Show when={pattern}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "list") {
+              const raw = data.path
+              const dir = typeof raw === "string" ? raw : ""
+              return {
+                icon: "→",
+                title: `List ${normalizePath(dir)}`,
+                body: (
+                  <Show when={dir}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.textMuted}>{"Path: " + normalizePath(dir)}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "bash") {
+              const title =
+                typeof data.description === "string" && data.description ? data.description : "Shell command"
+              const command = typeof data.command === "string" ? data.command : ""
+              return {
+                icon: "#",
+                title,
+                body: (
+                  <Show when={command}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.text}>{"$ " + command}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "task") {
+              const type = typeof data.subagent_type === "string" ? data.subagent_type : "Unknown"
+              const desc = typeof data.description === "string" ? data.description : ""
+              return {
+                icon: "#",
+                title: `${Locale.titlecase(type)} Task`,
+                body: (
+                  <Show when={desc}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.text}>{"◉ " + desc}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "webfetch") {
+              const url = typeof data.url === "string" ? data.url : ""
+              return {
+                icon: "%",
+                title: `WebFetch ${url}`,
+                body: (
+                  <Show when={url}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.textMuted}>{"URL: " + url}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "websearch") {
+              const query = typeof data.query === "string" ? data.query : ""
+              return {
+                icon: "◈",
+                title: `Exa Web Search "${query}"`,
+                body: (
+                  <Show when={query}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.textMuted}>{"Query: " + query}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "codesearch") {
+              const query = typeof data.query === "string" ? data.query : ""
+              return {
+                icon: "◇",
+                title: `Exa Code Search "${query}"`,
+                body: (
+                  <Show when={query}>
+                    <box paddingLeft={1}>
+                      <text fg={theme.textMuted}>{"Query: " + query}</text>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "external_directory") {
+              const meta = props.request.metadata ?? {}
+              const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined
+              const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined
+              const pattern = props.request.patterns?.[0]
+              const derived =
+                typeof pattern === "string" ? (pattern.includes("*") ? path.dirname(pattern) : pattern) : undefined
+
+              const raw = parent ?? filepath ?? derived
+              const dir = normalizePath(raw)
+              const patterns = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string")
+
+              return {
+                icon: "←",
+                title: `Access external directory ${dir}`,
+                body: (
+                  <Show when={patterns.length > 0}>
+                    <box paddingLeft={1} gap={1}>
+                      <text fg={theme.textMuted}>Patterns</text>
+                      <box>
+                        <For each={patterns}>{(p) => <text fg={theme.text}>{"- " + p}</text>}</For>
+                      </box>
+                    </box>
+                  </Show>
+                ),
+              }
+            }
+
+            if (permission === "doom_loop") {
+              return {
+                icon: "⟳",
+                title: "Continue after repeated failures",
+                body: (
+                  <box paddingLeft={1}>
+                    <text fg={theme.textMuted}>This keeps the session running despite repeated failures.</text>
+                  </box>
+                ),
+              }
+            }
+
+            return {
+              icon: "⚙",
+              title: `Call tool ${permission}`,
+              body: (
+                <box paddingLeft={1}>
+                  <text fg={theme.textMuted}>{"Tool: " + permission}</text>
+                </box>
+              ),
+            }
+          }
+
+          const current = info()
+
+          const header = () => (
+            <box flexDirection="column" gap={0}>
+              <box flexDirection="row" gap={1} flexShrink={0}>
+                <text fg={theme.warning}>{"△"}</text>
+                <text fg={theme.text}>Permission required</text>
+              </box>
+              <box flexDirection="row" gap={1} paddingLeft={2} flexShrink={0}>
+                <text fg={theme.textMuted} flexShrink={0}>
+                  {current.icon}
+                </text>
+                <text fg={theme.text}>{current.title}</text>
+              </box>
+            </box>
+          )
+
           const body = (
             <Prompt
               title="Permission required"
-              body={
-                <Switch>
-                  <Match when={props.request.permission === "edit"}>
-                    <EditBody request={props.request} />
-                  </Match>
-                  <Match when={props.request.permission === "read"}>
-                    <TextBody icon="→" title={`Read ` + normalizePath(input().filePath as string)} />
-                  </Match>
-                  <Match when={props.request.permission === "glob"}>
-                    <TextBody icon="✱" title={`Glob "` + (input().pattern ?? "") + `"`} />
-                  </Match>
-                  <Match when={props.request.permission === "grep"}>
-                    <TextBody icon="✱" title={`Grep "` + (input().pattern ?? "") + `"`} />
-                  </Match>
-                  <Match when={props.request.permission === "list"}>
-                    <TextBody icon="→" title={`List ` + normalizePath(input().path as string)} />
-                  </Match>
-                  <Match when={props.request.permission === "bash"}>
-                    <TextBody
-                      icon="#"
-                      title={(input().description as string) ?? ""}
-                      description={("$ " + input().command) as string}
-                    />
-                  </Match>
-                  <Match when={props.request.permission === "task"}>
-                    <TextBody
-                      icon="#"
-                      title={`${Locale.titlecase((input().subagent_type as string) ?? "Unknown")} Task`}
-                      description={"◉ " + input().description}
-                    />
-                  </Match>
-                  <Match when={props.request.permission === "webfetch"}>
-                    <TextBody icon="%" title={`WebFetch ` + (input().url ?? "")} />
-                  </Match>
-                  <Match when={props.request.permission === "websearch"}>
-                    <TextBody icon="◈" title={`Exa Web Search "` + (input().query ?? "") + `"`} />
-                  </Match>
-                  <Match when={props.request.permission === "codesearch"}>
-                    <TextBody icon="◇" title={`Exa Code Search "` + (input().query ?? "") + `"`} />
-                  </Match>
-                  <Match when={props.request.permission === "external_directory"}>
-                    {(() => {
-                      const meta = props.request.metadata ?? {}
-                      const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined
-                      const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined
-                      const pattern = props.request.patterns?.[0]
-                      const derived =
-                        typeof pattern === "string"
-                          ? pattern.includes("*")
-                            ? path.dirname(pattern)
-                            : pattern
-                          : undefined
-
-                      const raw = parent ?? filepath ?? derived
-                      const dir = normalizePath(raw)
-
-                      return <TextBody icon="←" title={`Access external directory ` + dir} />
-                    })()}
-                  </Match>
-                  <Match when={props.request.permission === "doom_loop"}>
-                    <TextBody icon="⟳" title="Continue after repeated failures" />
-                  </Match>
-                  <Match when={true}>
-                    <TextBody icon="⚙" title={`Call tool ` + props.request.permission} />
-                  </Match>
-                </Switch>
-              }
+              header={header()}
+              body={current.body}
               options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
               escapeKey="reject"
               fullscreen
@@ -372,6 +530,7 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
 
 function Prompt<const T extends Record<string, string>>(props: {
   title: string
+  header?: JSX.Element
   body: JSX.Element
   options: T
   escapeKey?: keyof T
@@ -445,10 +604,19 @@ function Prompt<const T extends Record<string, string>>(props: {
           })}
     >
       <box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}>
-        <box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
-          <text fg={theme.warning}>{"△"}</text>
-          <text fg={theme.text}>{props.title}</text>
-        </box>
+        <Show
+          when={props.header}
+          fallback={
+            <box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
+              <text fg={theme.warning}>{"△"}</text>
+              <text fg={theme.text}>{props.title}</text>
+            </box>
+          }
+        >
+          <box paddingLeft={1} flexShrink={0}>
+            {props.header}
+          </box>
+        </Show>
         {props.body}
       </box>
       <box

+ 3 - 3
packages/ui/src/components/button.css

@@ -109,7 +109,7 @@
   }
 
   &[data-size="small"] {
-    height: 22px;
+    height: 24px;
     padding: 0 8px;
     &[data-icon] {
       padding: 0 12px 0 4px;
@@ -129,8 +129,8 @@
   }
 
   &[data-size="normal"] {
-    height: 24px;
-    line-height: 24px;
+    height: 28px;
+    line-height: 28px;
     padding: 0 6px;
     &[data-icon] {
       padding: 0 12px 0 4px;

+ 21 - 0
packages/ui/src/components/message-part.css

@@ -745,6 +745,11 @@
     align-items: center;
     gap: 8px;
     justify-content: flex-end;
+
+    [data-component="button"] {
+      padding-left: 12px;
+      padding-right: 12px;
+    }
   }
 }
 
@@ -756,6 +761,22 @@
   min-height: 0;
   max-height: var(--question-prompt-max-height, 100dvh);
 
+  &[data-permission="true"] {
+    [data-slot="question-options"] {
+      code {
+        font-size: 14px;
+        line-height: var(--line-height-large);
+      }
+    }
+
+    [data-slot="question-footer-actions"] {
+      [data-component="button"] {
+        padding-left: 12px;
+        padding-right: 12px;
+      }
+    }
+  }
+
   [data-slot="question-body"] {
     display: flex;
     flex-direction: column;

+ 3 - 3
packages/ui/src/components/message-part.tsx

@@ -891,13 +891,13 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
         <Show when={showPermission() && permission()}>
           <div data-component="permission-prompt">
             <div data-slot="permission-actions">
-              <Button variant="ghost" size="small" onClick={() => respond("reject")}>
+              <Button variant="ghost" size="normal" onClick={() => respond("reject")}>
                 {i18n.t("ui.permission.deny")}
               </Button>
-              <Button variant="secondary" size="small" onClick={() => respond("always")}>
+              <Button variant="secondary" size="normal" onClick={() => respond("always")}>
                 {i18n.t("ui.permission.allowAlways")}
               </Button>
-              <Button variant="primary" size="small" onClick={() => respond("once")}>
+              <Button variant="primary" size="normal" onClick={() => respond("once")}>
                 {i18n.t("ui.permission.allowOnce")}
               </Button>
             </div>