|
|
@@ -1,8 +1,7 @@
|
|
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
|
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
|
|
-import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
|
|
|
+import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
|
|
|
import { createStore } from "solid-js/store"
|
|
|
-import { createFocusSignal } from "@solid-primitives/active-element"
|
|
|
import { useLocal } from "@/context/local"
|
|
|
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
|
|
import {
|
|
|
@@ -17,7 +16,6 @@ import {
|
|
|
} from "@/context/prompt"
|
|
|
import { useLayout } from "@/context/layout"
|
|
|
import { useSDK } from "@/context/sdk"
|
|
|
-import { useParams } from "@solidjs/router"
|
|
|
import { useSync } from "@/context/sync"
|
|
|
import { useComments } from "@/context/comments"
|
|
|
import { Button } from "@opencode-ai/ui/button"
|
|
|
@@ -27,7 +25,6 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
|
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
|
import { Select } from "@opencode-ai/ui/select"
|
|
|
-import { RadioGroup } from "@opencode-ai/ui/radio-group"
|
|
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
|
|
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
|
|
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
|
|
@@ -37,8 +34,12 @@ import { Persist, persisted } from "@/utils/persist"
|
|
|
import { usePermission } from "@/context/permission"
|
|
|
import { useLanguage } from "@/context/language"
|
|
|
import { usePlatform } from "@/context/platform"
|
|
|
+import { useSessionLayout } from "@/pages/session/session-layout"
|
|
|
+import { createSessionTabs } from "@/pages/session/helpers"
|
|
|
+import { promptEnabled, promptProbe } from "@/testing/prompt"
|
|
|
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
|
|
-import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
|
|
|
+import { createPromptAttachments } from "./prompt-input/attachments"
|
|
|
+import { ACCEPTED_FILE_TYPES } from "./prompt-input/files"
|
|
|
import {
|
|
|
canNavigateHistoryAtCursor,
|
|
|
navigatePromptHistory,
|
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
type PromptHistoryStoredEntry,
|
|
|
promptLength,
|
|
|
} from "./prompt-input/history"
|
|
|
-import { createPromptSubmit } from "./prompt-input/submit"
|
|
|
+import { createPromptSubmit, type FollowupDraft } from "./prompt-input/submit"
|
|
|
import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover"
|
|
|
import { PromptContextItems } from "./prompt-input/context-items"
|
|
|
import { PromptImageAttachments } from "./prompt-input/image-attachments"
|
|
|
@@ -61,6 +62,11 @@ interface PromptInputProps {
|
|
|
ref?: (el: HTMLDivElement) => void
|
|
|
newSessionWorktree?: string
|
|
|
onNewSessionWorktreeReset?: () => void
|
|
|
+ edit?: { id: string; prompt: Prompt; context: FollowupDraft["context"] }
|
|
|
+ onEditLoaded?: () => void
|
|
|
+ shouldQueue?: () => boolean
|
|
|
+ onQueue?: (draft: FollowupDraft) => void
|
|
|
+ onAbort?: () => void
|
|
|
onSubmit?: () => void
|
|
|
}
|
|
|
|
|
|
@@ -102,20 +108,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
const prompt = usePrompt()
|
|
|
const layout = useLayout()
|
|
|
const comments = useComments()
|
|
|
- const params = useParams()
|
|
|
const dialog = useDialog()
|
|
|
const providers = useProviders()
|
|
|
const command = useCommand()
|
|
|
const permission = usePermission()
|
|
|
const language = useLanguage()
|
|
|
const platform = usePlatform()
|
|
|
+ const { params, tabs, view } = useSessionLayout()
|
|
|
let editorRef!: HTMLDivElement
|
|
|
let fileInputRef: HTMLInputElement | undefined
|
|
|
let scrollRef!: HTMLDivElement
|
|
|
let slashPopoverRef!: HTMLDivElement
|
|
|
|
|
|
const mirror = { input: false }
|
|
|
- const inset = 44
|
|
|
+ const inset = 56
|
|
|
+ const space = `${inset}px`
|
|
|
|
|
|
const scrollCursorIntoView = () => {
|
|
|
const container = scrollRef
|
|
|
@@ -150,13 +157,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const queueScroll = () => {
|
|
|
- requestAnimationFrame(scrollCursorIntoView)
|
|
|
+ const queueScroll = (count = 2) => {
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ scrollCursorIntoView()
|
|
|
+ if (count > 1) queueScroll(count - 1)
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
|
|
- const tabs = createMemo(() => layout.tabs(sessionKey))
|
|
|
- const view = createMemo(() => layout.view(sessionKey))
|
|
|
+ const activeFileTab = createSessionTabs({
|
|
|
+ tabs,
|
|
|
+ pathFromTab: files.pathFromTab,
|
|
|
+ normalizeTab: (tab) => (tab.startsWith("file://") ? files.tab(tab) : tab),
|
|
|
+ }).activeFileTab
|
|
|
|
|
|
const commentInReview = (path: string) => {
|
|
|
const sessionID = params.id
|
|
|
@@ -209,7 +221,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
|
|
|
const recent = createMemo(() => {
|
|
|
const all = tabs().all()
|
|
|
- const active = tabs().active()
|
|
|
+ const active = activeFileTab()
|
|
|
const order = active ? [active, ...all.filter((x) => x !== active)] : all
|
|
|
const seen = new Set<string>()
|
|
|
const paths: string[] = []
|
|
|
@@ -232,6 +244,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
},
|
|
|
)
|
|
|
const working = createMemo(() => status()?.type !== "idle")
|
|
|
+ const tip = () => {
|
|
|
+ if (working()) {
|
|
|
+ return (
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span>{language.t("prompt.action.stop")}</span>
|
|
|
+ <span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span>{language.t("prompt.action.send")}</span>
|
|
|
+ <Icon name="enter" size="small" class="text-icon-base" />
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
const imageAttachments = createMemo(() =>
|
|
|
prompt.current().filter((part): part is ImageAttachmentPart => part.type === "image"),
|
|
|
)
|
|
|
@@ -255,6 +284,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
})
|
|
|
|
|
|
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
|
|
+ const motion = (value: number) => ({
|
|
|
+ opacity: value,
|
|
|
+ transform: `scale(${0.95 + value * 0.05})`,
|
|
|
+ filter: `blur(${(1 - value) * 2}px)`,
|
|
|
+ "pointer-events": value > 0.5 ? ("auto" as const) : ("none" as const),
|
|
|
+ })
|
|
|
+ const buttons = createMemo(() => motion(buttonsSpring()))
|
|
|
+ const shell = createMemo(() => motion(1 - buttonsSpring()))
|
|
|
+ const control = createMemo(() => ({ height: "28px", ...buttons() }))
|
|
|
|
|
|
const commentCount = createMemo(() => {
|
|
|
if (store.mode === "shell") return 0
|
|
|
@@ -390,7 +428,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const isFocused = createFocusSignal(() => editorRef)
|
|
|
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
|
|
|
|
|
|
const pick = () => fileInputRef?.click()
|
|
|
@@ -490,6 +527,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
setComposing(false)
|
|
|
}
|
|
|
|
|
|
+ const handleCompositionStart = () => {
|
|
|
+ setComposing(true)
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleCompositionEnd = () => {
|
|
|
+ setComposing(false)
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ if (composing()) return
|
|
|
+ reconcile(prompt.current().filter((part) => part.type !== "image"))
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
const agentList = createMemo(() =>
|
|
|
sync.data.agent
|
|
|
.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
|
|
@@ -573,6 +622,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
|
|
|
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
|
|
|
if (!cmd) return
|
|
|
+ promptProbe.select(cmd.id)
|
|
|
closePopover()
|
|
|
|
|
|
if (cmd.type === "custom") {
|
|
|
@@ -661,6 +711,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+ if (promptEnabled()) {
|
|
|
+ createEffect(() => {
|
|
|
+ promptProbe.set({
|
|
|
+ popover: store.popover,
|
|
|
+ slash: {
|
|
|
+ active: slashActive() ?? null,
|
|
|
+ ids: slashFlat().map((cmd) => cmd.id),
|
|
|
+ },
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ onCleanup(() => promptProbe.clear())
|
|
|
+ }
|
|
|
+
|
|
|
const selectPopoverActive = () => {
|
|
|
if (store.popover === "at") {
|
|
|
const items = atFlat()
|
|
|
@@ -680,24 +744,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- createEffect(
|
|
|
- on(
|
|
|
- () => prompt.current(),
|
|
|
- (currentParts) => {
|
|
|
- const inputParts = currentParts.filter((part) => part.type !== "image")
|
|
|
+ const reconcile = (input: Prompt) => {
|
|
|
+ if (mirror.input) {
|
|
|
+ mirror.input = false
|
|
|
+ if (isNormalizedEditor()) return
|
|
|
|
|
|
- if (mirror.input) {
|
|
|
- mirror.input = false
|
|
|
- if (isNormalizedEditor()) return
|
|
|
+ renderEditorWithCursor(input)
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- renderEditorWithCursor(inputParts)
|
|
|
- return
|
|
|
- }
|
|
|
+ const dom = parseFromDOM()
|
|
|
+ if (isNormalizedEditor() && isPromptEqual(input, dom)) return
|
|
|
|
|
|
- const domParts = parseFromDOM()
|
|
|
- if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return
|
|
|
+ renderEditorWithCursor(input)
|
|
|
+ }
|
|
|
|
|
|
- renderEditorWithCursor(inputParts)
|
|
|
+ createEffect(
|
|
|
+ on(
|
|
|
+ () => prompt.current(),
|
|
|
+ (parts) => {
|
|
|
+ if (composing()) return
|
|
|
+ reconcile(parts.filter((part) => part.type !== "image"))
|
|
|
},
|
|
|
),
|
|
|
)
|
|
|
@@ -921,6 +988,45 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
setCurrentHistory("entries", next)
|
|
|
}
|
|
|
|
|
|
+ createEffect(
|
|
|
+ on(
|
|
|
+ () => props.edit?.id,
|
|
|
+ (id) => {
|
|
|
+ const edit = props.edit
|
|
|
+ if (!id || !edit) return
|
|
|
+
|
|
|
+ for (const item of prompt.context.items()) {
|
|
|
+ prompt.context.remove(item.key)
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const item of edit.context) {
|
|
|
+ prompt.context.add({
|
|
|
+ type: item.type,
|
|
|
+ path: item.path,
|
|
|
+ selection: item.selection,
|
|
|
+ comment: item.comment,
|
|
|
+ commentID: item.commentID,
|
|
|
+ commentOrigin: item.commentOrigin,
|
|
|
+ preview: item.preview,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ setStore("mode", "normal")
|
|
|
+ setStore("popover", null)
|
|
|
+ setStore("historyIndex", -1)
|
|
|
+ setStore("savedPrompt", null)
|
|
|
+ prompt.set(edit.prompt, promptLength(edit.prompt))
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ editorRef.focus()
|
|
|
+ setCursorPosition(editorRef, promptLength(edit.prompt))
|
|
|
+ queueScroll()
|
|
|
+ })
|
|
|
+ props.onEditLoaded?.()
|
|
|
+ },
|
|
|
+ { defer: true },
|
|
|
+ ),
|
|
|
+ )
|
|
|
+
|
|
|
const navigateHistory = (direction: "up" | "down") => {
|
|
|
const result = navigatePromptHistory({
|
|
|
direction,
|
|
|
@@ -937,9 +1043,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
- const { addImageAttachment, removeImageAttachment, handlePaste } = createPromptAttachments({
|
|
|
+ const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
|
|
|
editor: () => editorRef,
|
|
|
- isFocused,
|
|
|
isDialogActive: () => !!dialog.active,
|
|
|
setDraggingType: (type) => setStore("draggingType", type),
|
|
|
focusEditor: () => {
|
|
|
@@ -956,6 +1061,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
|
|
|
return permission.isAutoAccepting(id, sdk.directory)
|
|
|
})
|
|
|
+ const acceptLabel = createMemo(() =>
|
|
|
+ language.t(accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable"),
|
|
|
+ )
|
|
|
+ const toggleAccept = () => {
|
|
|
+ if (!params.id) {
|
|
|
+ permission.toggleAutoAcceptDirectory(sdk.directory)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ permission.toggleAutoAccept(params.id, sdk.directory)
|
|
|
+ }
|
|
|
|
|
|
const { abort, handleSubmit } = createPromptSubmit({
|
|
|
info,
|
|
|
@@ -975,6 +1091,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
setPopover: (popover) => setStore("popover", popover),
|
|
|
newSessionWorktree: () => props.newSessionWorktree,
|
|
|
onNewSessionWorktreeReset: props.onNewSessionWorktreeReset,
|
|
|
+ shouldQueue: props.shouldQueue,
|
|
|
+ onQueue: props.onQueue,
|
|
|
+ onAbort: props.onAbort,
|
|
|
onSubmit: props.onSubmit,
|
|
|
})
|
|
|
|
|
|
@@ -1122,6 +1241,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
|
|
|
// Note: Shift+Enter is handled earlier, before IME check
|
|
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
|
+ event.preventDefault()
|
|
|
+ if (event.repeat) return
|
|
|
+ if (
|
|
|
+ working() &&
|
|
|
+ prompt
|
|
|
+ .current()
|
|
|
+ .map((part) => ("content" in part ? part.content : ""))
|
|
|
+ .join("")
|
|
|
+ .trim().length === 0 &&
|
|
|
+ imageAttachments().length === 0 &&
|
|
|
+ commentCount() === 0
|
|
|
+ ) {
|
|
|
+ return
|
|
|
+ }
|
|
|
handleSubmit(event)
|
|
|
}
|
|
|
}
|
|
|
@@ -1174,7 +1307,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
onOpen={(attachment) =>
|
|
|
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
|
|
|
}
|
|
|
- onRemove={removeImageAttachment}
|
|
|
+ onRemove={removeAttachment}
|
|
|
removeLabel={language.t("prompt.attachment.remove")}
|
|
|
/>
|
|
|
<div
|
|
|
@@ -1192,7 +1325,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
editorRef?.focus()
|
|
|
}}
|
|
|
>
|
|
|
- <div class="relative max-h-[240px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
|
|
+ <div
|
|
|
+ class="relative max-h-[240px] overflow-y-auto no-scrollbar"
|
|
|
+ ref={(el) => (scrollRef = el)}
|
|
|
+ style={{ "scroll-padding-bottom": space }}
|
|
|
+ >
|
|
|
<div
|
|
|
data-component="prompt-input"
|
|
|
ref={(el) => {
|
|
|
@@ -1208,28 +1345,40 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
spellcheck={store.mode === "normal"}
|
|
|
onInput={handleInput}
|
|
|
onPaste={handlePaste}
|
|
|
- onCompositionStart={() => setComposing(true)}
|
|
|
- onCompositionEnd={() => setComposing(false)}
|
|
|
+ onCompositionStart={handleCompositionStart}
|
|
|
+ onCompositionEnd={handleCompositionEnd}
|
|
|
onBlur={handleBlur}
|
|
|
onKeyDown={handleKeyDown}
|
|
|
classList={{
|
|
|
"select-text": true,
|
|
|
- "w-full pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
|
|
+ "w-full pl-3 pr-2 pt-2 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
|
|
"[&_[data-type=file]]:text-syntax-property": true,
|
|
|
"[&_[data-type=agent]]:text-syntax-type": true,
|
|
|
"font-mono!": store.mode === "shell",
|
|
|
}}
|
|
|
+ style={{ "padding-bottom": space }}
|
|
|
/>
|
|
|
<Show when={!prompt.dirty()}>
|
|
|
<div
|
|
|
- class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
|
|
+ class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
|
|
classList={{ "font-mono!": store.mode === "shell" }}
|
|
|
+ style={{ "padding-bottom": space }}
|
|
|
>
|
|
|
{placeholder()}
|
|
|
</div>
|
|
|
</Show>
|
|
|
</div>
|
|
|
|
|
|
+ <div
|
|
|
+ aria-hidden="true"
|
|
|
+ class="pointer-events-none absolute inset-x-0 bottom-0"
|
|
|
+ style={{
|
|
|
+ height: space,
|
|
|
+ background:
|
|
|
+ "linear-gradient(to top, var(--surface-raised-stronger-non-alpha) calc(100% - 20px), transparent)",
|
|
|
+ }}
|
|
|
+ />
|
|
|
+
|
|
|
<div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
|
|
|
<input
|
|
|
ref={fileInputRef}
|
|
|
@@ -1238,14 +1387,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
class="hidden"
|
|
|
onChange={(e) => {
|
|
|
const file = e.currentTarget.files?.[0]
|
|
|
- if (file) addImageAttachment(file)
|
|
|
+ if (file) void addAttachment(file)
|
|
|
e.currentTarget.value = ""
|
|
|
}}
|
|
|
/>
|
|
|
|
|
|
+ <div class="flex items-center gap-1 pointer-events-auto">
|
|
|
+ <Tooltip placement="top" inactive={!prompt.dirty() && !working()} value={tip()}>
|
|
|
+ <IconButton
|
|
|
+ data-action="prompt-submit"
|
|
|
+ type="submit"
|
|
|
+ disabled={store.mode !== "normal" || (!prompt.dirty() && !working() && commentCount() === 0)}
|
|
|
+ tabIndex={store.mode === "normal" ? undefined : -1}
|
|
|
+ icon={working() ? "stop" : "arrow-up"}
|
|
|
+ variant="primary"
|
|
|
+ class="size-8"
|
|
|
+ style={buttons()}
|
|
|
+ aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
|
|
+ />
|
|
|
+ </Tooltip>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="pointer-events-none absolute bottom-2 left-2">
|
|
|
<div
|
|
|
aria-hidden={store.mode !== "normal"}
|
|
|
- class="flex items-center gap-1"
|
|
|
+ class="pointer-events-auto"
|
|
|
style={{
|
|
|
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
|
|
}}
|
|
|
@@ -1260,11 +1427,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
type="button"
|
|
|
variant="ghost"
|
|
|
class="size-8 p-0"
|
|
|
- style={{
|
|
|
- opacity: buttonsSpring(),
|
|
|
- transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
|
|
- filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
|
|
- }}
|
|
|
+ style={buttons()}
|
|
|
onClick={pick}
|
|
|
disabled={store.mode !== "normal"}
|
|
|
tabIndex={store.mode === "normal" ? undefined : -1}
|
|
|
@@ -1273,85 +1436,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
<Icon name="plus" class="size-4.5" />
|
|
|
</Button>
|
|
|
</TooltipKeybind>
|
|
|
-
|
|
|
- <Tooltip
|
|
|
- placement="top"
|
|
|
- inactive={!prompt.dirty() && !working()}
|
|
|
- value={
|
|
|
- <Switch>
|
|
|
- <Match when={working()}>
|
|
|
- <div class="flex items-center gap-2">
|
|
|
- <span>{language.t("prompt.action.stop")}</span>
|
|
|
- <span class="text-icon-base text-12-medium text-[10px]!">{language.t("common.key.esc")}</span>
|
|
|
- </div>
|
|
|
- </Match>
|
|
|
- <Match when={true}>
|
|
|
- <div class="flex items-center gap-2">
|
|
|
- <span>{language.t("prompt.action.send")}</span>
|
|
|
- <Icon name="enter" size="small" class="text-icon-base" />
|
|
|
- </div>
|
|
|
- </Match>
|
|
|
- </Switch>
|
|
|
- }
|
|
|
- >
|
|
|
- <IconButton
|
|
|
- data-action="prompt-submit"
|
|
|
- type="submit"
|
|
|
- disabled={store.mode !== "normal" || (!prompt.dirty() && !working() && commentCount() === 0)}
|
|
|
- tabIndex={store.mode === "normal" ? undefined : -1}
|
|
|
- icon={working() ? "stop" : "arrow-up"}
|
|
|
- variant="primary"
|
|
|
- class="size-8"
|
|
|
- style={{
|
|
|
- opacity: buttonsSpring(),
|
|
|
- transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
|
|
- filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
|
|
- }}
|
|
|
- aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
|
|
- />
|
|
|
- </Tooltip>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="pointer-events-none absolute bottom-2 left-2">
|
|
|
- <div class="pointer-events-auto">
|
|
|
- <TooltipKeybind
|
|
|
- placement="top"
|
|
|
- gutter={8}
|
|
|
- title={language.t(
|
|
|
- accepting() ? "command.permissions.autoaccept.disable" : "command.permissions.autoaccept.enable",
|
|
|
- )}
|
|
|
- keybind={command.keybind("permissions.autoaccept")}
|
|
|
- >
|
|
|
- <Button
|
|
|
- data-action="prompt-permissions"
|
|
|
- variant="ghost"
|
|
|
- onClick={() => {
|
|
|
- if (!params.id) {
|
|
|
- permission.toggleAutoAcceptDirectory(sdk.directory)
|
|
|
- return
|
|
|
- }
|
|
|
- permission.toggleAutoAccept(params.id, sdk.directory)
|
|
|
- }}
|
|
|
- classList={{
|
|
|
- "size-6 flex items-center justify-center": true,
|
|
|
- "text-text-base": !accepting(),
|
|
|
- "hover:bg-surface-success-base": accepting(),
|
|
|
- }}
|
|
|
- aria-label={
|
|
|
- accepting()
|
|
|
- ? language.t("command.permissions.autoaccept.disable")
|
|
|
- : language.t("command.permissions.autoaccept.enable")
|
|
|
- }
|
|
|
- aria-pressed={accepting()}
|
|
|
- >
|
|
|
- <Icon
|
|
|
- name="chevron-double-right"
|
|
|
- size="small"
|
|
|
- classList={{ "text-icon-success-base": accepting() }}
|
|
|
- />
|
|
|
- </Button>
|
|
|
- </TooltipKeybind>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -1364,61 +1448,83 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
|
|
|
style={{
|
|
|
padding: "0 4px 0 8px",
|
|
|
- opacity: 1 - buttonsSpring(),
|
|
|
- transform: `scale(${0.95 + (1 - buttonsSpring()) * 0.05})`,
|
|
|
- filter: `blur(${buttonsSpring() * 2}px)`,
|
|
|
- "pointer-events": buttonsSpring() < 0.5 ? "auto" : "none",
|
|
|
+ ...shell(),
|
|
|
}}
|
|
|
>
|
|
|
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
|
|
<div class="size-4 shrink-0" />
|
|
|
</div>
|
|
|
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
|
|
- <TooltipKeybind
|
|
|
- placement="top"
|
|
|
- gutter={4}
|
|
|
- title={language.t("command.agent.cycle")}
|
|
|
- keybind={command.keybind("agent.cycle")}
|
|
|
- >
|
|
|
- <Select
|
|
|
- size="normal"
|
|
|
- options={agentNames()}
|
|
|
- current={local.agent.current()?.name ?? ""}
|
|
|
- onSelect={local.agent.set}
|
|
|
- class="capitalize max-w-[160px]"
|
|
|
- valueClass="truncate text-13-regular"
|
|
|
- triggerStyle={{
|
|
|
- height: "28px",
|
|
|
- opacity: buttonsSpring(),
|
|
|
- transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
|
|
- filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
|
|
- "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
|
|
- }}
|
|
|
- variant="ghost"
|
|
|
- />
|
|
|
- </TooltipKeybind>
|
|
|
- <Show
|
|
|
- when={providers.paid().length > 0}
|
|
|
- fallback={
|
|
|
+ <div data-component="prompt-agent-control">
|
|
|
+ <TooltipKeybind
|
|
|
+ placement="top"
|
|
|
+ gutter={4}
|
|
|
+ title={language.t("command.agent.cycle")}
|
|
|
+ keybind={command.keybind("agent.cycle")}
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ size="normal"
|
|
|
+ options={agentNames()}
|
|
|
+ current={local.agent.current()?.name ?? ""}
|
|
|
+ onSelect={local.agent.set}
|
|
|
+ class="capitalize max-w-[160px] text-text-base"
|
|
|
+ valueClass="truncate text-13-regular text-text-base"
|
|
|
+ triggerStyle={control()}
|
|
|
+ triggerProps={{ "data-action": "prompt-agent" }}
|
|
|
+ variant="ghost"
|
|
|
+ />
|
|
|
+ </TooltipKeybind>
|
|
|
+ </div>
|
|
|
+ <div data-component="prompt-model-control">
|
|
|
+ <Show
|
|
|
+ when={providers.paid().length > 0}
|
|
|
+ fallback={
|
|
|
+ <TooltipKeybind
|
|
|
+ placement="top"
|
|
|
+ gutter={4}
|
|
|
+ title={language.t("command.model.choose")}
|
|
|
+ keybind={command.keybind("model.choose")}
|
|
|
+ >
|
|
|
+ <Button
|
|
|
+ data-action="prompt-model"
|
|
|
+ as="div"
|
|
|
+ variant="ghost"
|
|
|
+ size="normal"
|
|
|
+ class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
|
|
|
+ style={control()}
|
|
|
+ onClick={() => dialog.show(() => <DialogSelectModelUnpaid model={local.model} />)}
|
|
|
+ >
|
|
|
+ <Show when={local.model.current()?.provider?.id}>
|
|
|
+ <ProviderIcon
|
|
|
+ id={local.model.current()!.provider.id}
|
|
|
+ class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
|
|
+ style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
|
|
+ />
|
|
|
+ </Show>
|
|
|
+ <span class="truncate">
|
|
|
+ {local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
|
|
+ </span>
|
|
|
+ <Icon name="chevron-down" size="small" class="shrink-0" />
|
|
|
+ </Button>
|
|
|
+ </TooltipKeybind>
|
|
|
+ }
|
|
|
+ >
|
|
|
<TooltipKeybind
|
|
|
placement="top"
|
|
|
gutter={4}
|
|
|
title={language.t("command.model.choose")}
|
|
|
keybind={command.keybind("model.choose")}
|
|
|
>
|
|
|
- <Button
|
|
|
- as="div"
|
|
|
- variant="ghost"
|
|
|
- size="normal"
|
|
|
- class="min-w-0 max-w-[320px] text-13-regular group"
|
|
|
- style={{
|
|
|
- height: "28px",
|
|
|
- opacity: buttonsSpring(),
|
|
|
- transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
|
|
- filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
|
|
- "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
|
|
+ <ModelSelectorPopover
|
|
|
+ model={local.model}
|
|
|
+ triggerAs={Button}
|
|
|
+ triggerProps={{
|
|
|
+ variant: "ghost",
|
|
|
+ size: "normal",
|
|
|
+ style: control(),
|
|
|
+ class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
|
|
|
+ "data-action": "prompt-model",
|
|
|
}}
|
|
|
- onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
|
|
|
>
|
|
|
<Show when={local.model.current()?.provider?.id}>
|
|
|
<ProviderIcon
|
|
|
@@ -1431,101 +1537,55 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
|
|
</span>
|
|
|
<Icon name="chevron-down" size="small" class="shrink-0" />
|
|
|
- </Button>
|
|
|
+ </ModelSelectorPopover>
|
|
|
</TooltipKeybind>
|
|
|
- }
|
|
|
- >
|
|
|
+ </Show>
|
|
|
+ </div>
|
|
|
+ <div data-component="prompt-variant-control">
|
|
|
<TooltipKeybind
|
|
|
placement="top"
|
|
|
gutter={4}
|
|
|
- title={language.t("command.model.choose")}
|
|
|
- keybind={command.keybind("model.choose")}
|
|
|
+ title={language.t("command.model.variant.cycle")}
|
|
|
+ keybind={command.keybind("model.variant.cycle")}
|
|
|
>
|
|
|
- <ModelSelectorPopover
|
|
|
- triggerAs={Button}
|
|
|
- triggerProps={{
|
|
|
- variant: "ghost",
|
|
|
- size: "normal",
|
|
|
- style: {
|
|
|
- height: "28px",
|
|
|
- opacity: buttonsSpring(),
|
|
|
- transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
|
|
- filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
|
|
- "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
|
|
- },
|
|
|
- class: "min-w-0 max-w-[320px] text-13-regular group",
|
|
|
- }}
|
|
|
- >
|
|
|
- <Show when={local.model.current()?.provider?.id}>
|
|
|
- <ProviderIcon
|
|
|
- id={local.model.current()!.provider.id}
|
|
|
- class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
|
|
|
- style={{ "will-change": "opacity", transform: "translateZ(0)" }}
|
|
|
- />
|
|
|
- </Show>
|
|
|
- <span class="truncate">
|
|
|
- {local.model.current()?.name ?? language.t("dialog.model.select.title")}
|
|
|
- </span>
|
|
|
- <Icon name="chevron-down" size="small" class="shrink-0" />
|
|
|
- </ModelSelectorPopover>
|
|
|
+ <Select
|
|
|
+ size="normal"
|
|
|
+ options={variants()}
|
|
|
+ current={local.model.variant.current() ?? "default"}
|
|
|
+ label={(x) => (x === "default" ? language.t("common.default") : x)}
|
|
|
+ onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
|
|
|
+ class="capitalize max-w-[160px] text-text-base"
|
|
|
+ valueClass="truncate text-13-regular text-text-base"
|
|
|
+ triggerStyle={control()}
|
|
|
+ triggerProps={{ "data-action": "prompt-model-variant" }}
|
|
|
+ variant="ghost"
|
|
|
+ />
|
|
|
</TooltipKeybind>
|
|
|
- </Show>
|
|
|
+ </div>
|
|
|
<TooltipKeybind
|
|
|
placement="top"
|
|
|
- gutter={4}
|
|
|
- title={language.t("command.model.variant.cycle")}
|
|
|
- keybind={command.keybind("model.variant.cycle")}
|
|
|
+ gutter={8}
|
|
|
+ title={acceptLabel()}
|
|
|
+ keybind={command.keybind("permissions.autoaccept")}
|
|
|
>
|
|
|
- <Select
|
|
|
- size="normal"
|
|
|
- options={variants()}
|
|
|
- current={local.model.variant.current() ?? "default"}
|
|
|
- label={(x) => (x === "default" ? language.t("common.default") : x)}
|
|
|
- onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
|
|
|
- class="capitalize max-w-[160px]"
|
|
|
- valueClass="truncate text-13-regular"
|
|
|
- triggerStyle={{
|
|
|
- height: "28px",
|
|
|
- opacity: buttonsSpring(),
|
|
|
- transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
|
|
|
- filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
|
|
|
- "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
|
|
|
- }}
|
|
|
+ <Button
|
|
|
+ data-action="prompt-permissions"
|
|
|
variant="ghost"
|
|
|
- />
|
|
|
+ onClick={toggleAccept}
|
|
|
+ classList={{
|
|
|
+ "h-7 w-7 p-0 shrink-0 flex items-center justify-center": true,
|
|
|
+ "text-text-base": !accepting(),
|
|
|
+ "hover:bg-surface-success-base": accepting(),
|
|
|
+ }}
|
|
|
+ style={control()}
|
|
|
+ aria-label={acceptLabel()}
|
|
|
+ aria-pressed={accepting()}
|
|
|
+ >
|
|
|
+ <Icon name="shield" size="small" classList={{ "text-icon-success-base": accepting() }} />
|
|
|
+ </Button>
|
|
|
</TooltipKeybind>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="shrink-0">
|
|
|
- <RadioGroup
|
|
|
- options={["shell", "normal"] as const}
|
|
|
- current={store.mode}
|
|
|
- value={(mode) => mode}
|
|
|
- label={(mode) => (
|
|
|
- <TooltipKeybind
|
|
|
- placement="top"
|
|
|
- gutter={4}
|
|
|
- openDelay={2000}
|
|
|
- title={language.t(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
|
|
- keybind={command.keybind(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
|
|
- class="size-full flex items-center justify-center"
|
|
|
- >
|
|
|
- <Icon
|
|
|
- name={mode === "shell" ? "console" : "prompt"}
|
|
|
- class="size-[18px]"
|
|
|
- classList={{
|
|
|
- "text-icon-strong-base": store.mode === mode,
|
|
|
- "text-icon-weak": store.mode !== mode,
|
|
|
- }}
|
|
|
- />
|
|
|
- </TooltipKeybind>
|
|
|
- )}
|
|
|
- onSelect={(mode) => mode && setMode(mode)}
|
|
|
- fill
|
|
|
- pad="none"
|
|
|
- class="w-[68px]"
|
|
|
- />
|
|
|
- </div>
|
|
|
</div>
|
|
|
</DockTray>
|
|
|
</Show>
|