feat(app): session timeline/turn rework (#13196)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
This commit is contained in:
@@ -32,7 +32,6 @@ import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid
|
||||
import { useProviders } from "@/hooks/use-providers"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { Persist, persisted } from "@/utils/persist"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
import { usePermission } from "@/context/permission"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
@@ -94,7 +93,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const local = useLocal()
|
||||
const files = useFile()
|
||||
const prompt = usePrompt()
|
||||
const commentCount = createMemo(() => prompt.context.items().filter((item) => !!item.comment?.trim()).length)
|
||||
const layout = useLayout()
|
||||
const comments = useComments()
|
||||
const params = useParams()
|
||||
@@ -105,7 +103,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
let editorRef!: HTMLDivElement
|
||||
let fileInputRef!: HTMLInputElement
|
||||
let fileInputRef: HTMLInputElement | undefined
|
||||
let scrollRef!: HTMLDivElement
|
||||
let slashPopoverRef!: HTMLDivElement
|
||||
|
||||
@@ -223,14 +221,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
mode: "normal",
|
||||
applyingHistory: false,
|
||||
})
|
||||
const placeholder = createMemo(() =>
|
||||
promptPlaceholder({
|
||||
mode: store.mode,
|
||||
commentCount: commentCount(),
|
||||
example: language.t(EXAMPLES[store.placeholder]),
|
||||
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
||||
}),
|
||||
)
|
||||
|
||||
const commentCount = createMemo(() => {
|
||||
if (store.mode === "shell") return 0
|
||||
return prompt.context.items().filter((item) => !!item.comment?.trim()).length
|
||||
})
|
||||
|
||||
const contextItems = createMemo(() => {
|
||||
const items = prompt.context.items()
|
||||
if (store.mode !== "shell") return items
|
||||
return items.filter((item) => !item.comment?.trim())
|
||||
})
|
||||
|
||||
const hasUserPrompt = createMemo(() => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return false
|
||||
const messages = sync.data.message[sessionID]
|
||||
if (!messages) return false
|
||||
return messages.some((m) => m.role === "user")
|
||||
})
|
||||
|
||||
const MAX_HISTORY = 100
|
||||
const [history, setHistory] = persisted(
|
||||
@@ -250,6 +259,18 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}),
|
||||
)
|
||||
|
||||
const suggest = createMemo(() => !hasUserPrompt())
|
||||
|
||||
const placeholder = createMemo(() =>
|
||||
promptPlaceholder({
|
||||
mode: store.mode,
|
||||
commentCount: commentCount(),
|
||||
example: suggest() ? language.t(EXAMPLES[store.placeholder]) : "",
|
||||
suggest: suggest(),
|
||||
t: (key, params) => language.t(key as Parameters<typeof language.t>[0], params as never),
|
||||
}),
|
||||
)
|
||||
|
||||
const applyHistoryPrompt = (p: Prompt, position: "start" | "end") => {
|
||||
const length = position === "start" ? 0 : promptLength(p)
|
||||
setStore("applyingHistory", true)
|
||||
@@ -282,6 +303,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const isFocused = createFocusSignal(() => editorRef)
|
||||
const escBlur = () => platform.platform === "desktop" && platform.os === "macos"
|
||||
|
||||
const pick = () => fileInputRef?.click()
|
||||
|
||||
const setMode = (mode: "normal" | "shell") => {
|
||||
setStore("mode", mode)
|
||||
setStore("popover", null)
|
||||
requestAnimationFrame(() => editorRef?.focus())
|
||||
}
|
||||
|
||||
command.register("prompt-input", () => [
|
||||
{
|
||||
id: "file.attach",
|
||||
title: language.t("prompt.action.attachFile"),
|
||||
category: language.t("command.category.file"),
|
||||
keybind: "mod+u",
|
||||
disabled: store.mode !== "normal",
|
||||
onSelect: pick,
|
||||
},
|
||||
])
|
||||
|
||||
const closePopover = () => setStore("popover", null)
|
||||
|
||||
const resetHistoryNavigation = (force = false) => {
|
||||
@@ -326,6 +366,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
createEffect(() => {
|
||||
params.id
|
||||
if (params.id) return
|
||||
if (!suggest()) return
|
||||
const interval = setInterval(() => {
|
||||
setStore("placeholder", (prev) => (prev + 1) % EXAMPLES.length)
|
||||
}, 6500)
|
||||
@@ -816,6 +857,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
})
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "u") {
|
||||
event.preventDefault()
|
||||
if (store.mode !== "normal") return
|
||||
pick()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === "Backspace") {
|
||||
const selection = window.getSelection()
|
||||
if (selection && selection.isCollapsed) {
|
||||
@@ -956,8 +1004,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const variants = createMemo(() => ["default", ...local.model.variant.list()])
|
||||
|
||||
return (
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-3">
|
||||
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
|
||||
<PromptPopover
|
||||
popover={store.popover}
|
||||
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
|
||||
@@ -977,8 +1027,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onSubmit={handleSubmit}
|
||||
classList={{
|
||||
"group/prompt-input": true,
|
||||
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true,
|
||||
"rounded-[14px] overflow-clip focus-within:shadow-xs-border": true,
|
||||
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative z-10": true,
|
||||
"rounded-[12px] overflow-clip focus-within:shadow-xs-border": true,
|
||||
"border-icon-info-active border-dashed": store.draggingType !== null,
|
||||
[props.class ?? ""]: !!props.class,
|
||||
}}
|
||||
@@ -988,7 +1038,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
label={language.t(store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label")}
|
||||
/>
|
||||
<PromptContextItems
|
||||
items={prompt.context.items()}
|
||||
items={contextItems()}
|
||||
active={(item) => {
|
||||
const active = comments.active()
|
||||
return !!item.commentID && item.commentID === active?.id && item.path === active?.file
|
||||
@@ -1008,7 +1058,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onRemove={removeImageAttachment}
|
||||
removeLabel={language.t("prompt.attachment.remove")}
|
||||
/>
|
||||
<div class="relative max-h-[240px] overflow-y-auto" ref={(el) => (scrollRef = el)}>
|
||||
<div
|
||||
class="relative max-h-[240px] overflow-y-auto"
|
||||
ref={(el) => (scrollRef = el)}
|
||||
onMouseDown={(e) => {
|
||||
const target = e.target
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
if (
|
||||
target.closest(
|
||||
'[data-action="prompt-attach"], [data-action="prompt-submit"], [data-action="prompt-permissions"]',
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
editorRef?.focus()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-component="prompt-input"
|
||||
ref={(el) => {
|
||||
@@ -1029,141 +1094,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
onKeyDown={handleKeyDown}
|
||||
classList={{
|
||||
"select-text": true,
|
||||
"w-full p-3 pr-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
||||
"w-full pl-3 pr-2 pt-2 pb-12 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",
|
||||
}}
|
||||
/>
|
||||
<Show when={!prompt.dirty()}>
|
||||
<div class="absolute top-0 inset-x-0 p-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
|
||||
<div
|
||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||
classList={{ "font-mono!": store.mode === "shell" }}
|
||||
>
|
||||
{placeholder()}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="relative p-3 flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Switch>
|
||||
<Match when={store.mode === "shell"}>
|
||||
<div class="flex items-center gap-2 px-2 h-6">
|
||||
<Icon name="console" size="small" class="text-icon-primary" />
|
||||
<span class="text-12-regular text-text-primary">{language.t("prompt.mode.shell")}</span>
|
||||
<span class="text-12-regular text-text-weak">{language.t("prompt.mode.shell.exit")}</span>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.mode === "normal"}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.agent.cycle")}
|
||||
keybind={command.keybind("agent.cycle")}
|
||||
>
|
||||
<Select
|
||||
options={agentNames()}
|
||||
current={local.agent.current()?.name ?? ""}
|
||||
onSelect={local.agent.set}
|
||||
class={`capitalize ${local.model.variant.list().length > 0 ? "max-w-full" : "max-w-[120px]"}`}
|
||||
valueClass="truncate"
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<Button
|
||||
as="div"
|
||||
variant="ghost"
|
||||
class="px-2 min-w-0 max-w-[240px]"
|
||||
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-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={8}
|
||||
title={language.t("command.model.choose")}
|
||||
keybind={command.keybind("model.choose")}
|
||||
>
|
||||
<ModelSelectorPopover
|
||||
triggerAs={Button}
|
||||
triggerProps={{ variant: "ghost", class: "min-w-0 max-w-[240px]" }}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-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>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<Show when={local.model.variant.list().length > 0}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<Button
|
||||
data-action="model-variant-cycle"
|
||||
variant="ghost"
|
||||
class="text-text-base _hidden group-hover/prompt-input:inline-block capitalize text-12-regular"
|
||||
onClick={() => local.model.variant.cycle()}
|
||||
>
|
||||
{local.model.variant.current() ?? language.t("common.default")}
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<Show when={permission.permissionsEnabled() && params.id}>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.permissions.autoaccept.enable")}
|
||||
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!, sdk.directory),
|
||||
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
||||
}}
|
||||
aria-label={
|
||||
permission.isAutoAccepting(params.id!, sdk.directory)
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable")
|
||||
}
|
||||
aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-double-right"
|
||||
size="small"
|
||||
classList={{ "text-icon-success-base": permission.isAutoAccepting(params.id!, sdk.directory) }}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
|
||||
<div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
@@ -1175,54 +1121,262 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
e.currentTarget.value = ""
|
||||
}}
|
||||
/>
|
||||
<div class="flex items-center gap-1 mr-1">
|
||||
<SessionContextUsage />
|
||||
<Show when={store.mode === "normal"}>
|
||||
<Tooltip placement="top" value={language.t("prompt.action.attachFile")}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
class="size-6 px-1"
|
||||
onClick={() => fileInputRef.click()}
|
||||
aria-label={language.t("prompt.action.attachFile")}
|
||||
>
|
||||
<Icon name="photo" class="size-4.5" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
|
||||
<div
|
||||
aria-hidden={store.mode !== "normal"}
|
||||
class="flex items-center gap-1 transition-all duration-200 ease-out"
|
||||
classList={{
|
||||
"opacity-100 translate-y-0 scale-100 pointer-events-auto": store.mode === "normal",
|
||||
"opacity-0 translate-y-2 scale-95 pointer-events-none": store.mode !== "normal",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
type="submit"
|
||||
disabled={!prompt.dirty() && !working() && commentCount() === 0}
|
||||
icon={working() ? "stop" : "arrow-up"}
|
||||
variant="primary"
|
||||
class="h-6 w-4.5"
|
||||
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
title={language.t("prompt.action.attachFile")}
|
||||
keybind={command.keybind("file.attach")}
|
||||
>
|
||||
<Button
|
||||
data-action="prompt-attach"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
class="size-8 p-0"
|
||||
onClick={pick}
|
||||
disabled={store.mode !== "normal"}
|
||||
tabIndex={store.mode === "normal" ? undefined : -1}
|
||||
aria-label={language.t("prompt.action.attachFile")}
|
||||
>
|
||||
<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"
|
||||
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={store.mode === "normal" && permission.permissionsEnabled() && params.id}>
|
||||
<div class="pointer-events-none absolute bottom-2 left-2">
|
||||
<div class="pointer-events-auto">
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={8}
|
||||
title={language.t("command.permissions.autoaccept.enable")}
|
||||
keybind={command.keybind("permissions.autoaccept")}
|
||||
>
|
||||
<Button
|
||||
data-action="prompt-permissions"
|
||||
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!, sdk.directory),
|
||||
"hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory),
|
||||
}}
|
||||
aria-label={
|
||||
permission.isAutoAccepting(params.id!, sdk.directory)
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable")
|
||||
}
|
||||
aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-double-right"
|
||||
size="small"
|
||||
classList={{ "text-icon-success-base": permission.isAutoAccepting(params.id!, sdk.directory) }}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipKeybind>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</form>
|
||||
<Show when={store.mode === "normal" || store.mode === "shell"}>
|
||||
<div class="-mt-3.5 bg-background-base border border-border-weak-base relative z-0 rounded-[12px] rounded-tl-0 rounded-tr-0 overflow-clip">
|
||||
<div class="px-2 pt-5.5 pb-2 flex items-center gap-2 min-w-0">
|
||||
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
||||
<Show when={store.mode === "shell"}>
|
||||
<div class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0" style={{ padding: "0 4px 0 8px" }}>
|
||||
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
|
||||
<div class="size-4 shrink-0" />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={store.mode === "normal"}>
|
||||
<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" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
<Show
|
||||
when={providers.paid().length > 0}
|
||||
fallback={
|
||||
<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" }}
|
||||
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
|
||||
>
|
||||
<Show when={local.model.current()?.provider?.id}>
|
||||
<ProviderIcon
|
||||
id={local.model.current()!.provider.id as IconName}
|
||||
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")}
|
||||
>
|
||||
<ModelSelectorPopover
|
||||
triggerAs={Button}
|
||||
triggerProps={{
|
||||
variant: "ghost",
|
||||
size: "normal",
|
||||
style: { height: "28px" },
|
||||
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 as IconName}
|
||||
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>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
title={language.t("command.model.variant.cycle")}
|
||||
keybind={command.keybind("model.variant.cycle")}
|
||||
>
|
||||
<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" }}
|
||||
variant="ghost"
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
data-component="prompt-mode-toggle"
|
||||
class="relative h-8 w-[68px] rounded-[4px] bg-surface-inset-base shadow-[var(--shadow-xs-border-base)] p-0 flex items-center gap-1 overflow-visible"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-y-0 left-0 w-[calc((100%-4px)/2)] rounded-[4px] bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-xs-border)] transition-transform duration-200 ease-out will-change-transform"
|
||||
style={{
|
||||
transform: store.mode === "shell" ? "translateX(0px)" : "translateX(calc(100% + 4px))",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="relative z-10 flex-1 h-full flex items-center justify-center rounded-[4px]"
|
||||
aria-pressed={store.mode === "shell"}
|
||||
onClick={() => setMode("shell")}
|
||||
>
|
||||
<Icon
|
||||
name="console"
|
||||
size="normal"
|
||||
classList={{
|
||||
"text-icon-strong-base": store.mode === "shell",
|
||||
"text-icon-weak": store.mode !== "shell",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="relative z-10 flex-1 h-full flex items-center justify-center rounded-[4px]"
|
||||
aria-pressed={store.mode === "normal"}
|
||||
onClick={() => setMode("normal")}
|
||||
>
|
||||
<Icon
|
||||
name="prompt"
|
||||
size="normal"
|
||||
classList={{
|
||||
"text-icon-interactive-base": store.mode === "normal",
|
||||
"text-icon-weak": store.mode !== "normal",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user