|
|
@@ -4,8 +4,6 @@ import {
|
|
|
createEffect,
|
|
|
on,
|
|
|
Component,
|
|
|
- splitProps,
|
|
|
- For,
|
|
|
Show,
|
|
|
onCleanup,
|
|
|
createMemo,
|
|
|
@@ -13,10 +11,8 @@ import {
|
|
|
createResource,
|
|
|
Switch,
|
|
|
Match,
|
|
|
- type ComponentProps,
|
|
|
type JSX,
|
|
|
} from "solid-js"
|
|
|
-import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
|
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
|
|
import type { useLocal } from "@/context/local"
|
|
|
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
|
|
@@ -30,15 +26,14 @@ import {
|
|
|
AgentPart,
|
|
|
FileAttachmentPart,
|
|
|
} from "@/context/prompt"
|
|
|
-import { getProjectAvatarVariant, useLayout } from "@/context/layout"
|
|
|
+import { useLayout } from "@/context/layout"
|
|
|
import { useSDK } from "@/context/sdk"
|
|
|
import { useSync } from "@/context/sync"
|
|
|
import { useComments } from "@/context/comments"
|
|
|
import { Button } from "@opencode-ai/ui/button"
|
|
|
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
|
|
-import { Icon, type IconProps } from "@opencode-ai/ui/icon"
|
|
|
+import { Icon } from "@opencode-ai/ui/icon"
|
|
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
|
|
-import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
|
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
|
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
|
|
import { Select } from "@opencode-ai/ui/select"
|
|
|
@@ -71,8 +66,6 @@ import { promptPlaceholder } from "./prompt-input/placeholder"
|
|
|
import { createPromptInputTransientState } from "./prompt-input/transient-state"
|
|
|
import { showToast } from "@/utils/toast"
|
|
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
|
|
-import { pathKey } from "@/utils/path-key"
|
|
|
-import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
|
|
|
|
|
export type PromptInputState = ReturnType<typeof usePrompt>
|
|
|
|
|
|
@@ -100,20 +93,6 @@ export type PromptInputControls = {
|
|
|
paid: boolean
|
|
|
loading: boolean
|
|
|
}
|
|
|
- projects: {
|
|
|
- available: {
|
|
|
- name?: string
|
|
|
- id?: string
|
|
|
- worktree: string
|
|
|
- sandboxes?: string[]
|
|
|
- icon?: { color?: string; url?: string; override?: string }
|
|
|
- server?: { key: string; name: string }
|
|
|
- }[]
|
|
|
- directory: string
|
|
|
- server?: string
|
|
|
- select: (worktree: string, server?: string) => void
|
|
|
- add: (title: string, server?: string) => void
|
|
|
- }
|
|
|
session: {
|
|
|
id?: string
|
|
|
tabs: {
|
|
|
@@ -184,6 +163,7 @@ export interface PromptInputProps {
|
|
|
onQueue?: (draft: FollowupDraft) => void
|
|
|
onAbort?: () => void
|
|
|
onSubmit?: () => void
|
|
|
+ toolbar?: JSX.Element
|
|
|
}
|
|
|
|
|
|
const EXAMPLES = [
|
|
|
@@ -232,7 +212,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
let fileInputRef: HTMLInputElement | undefined
|
|
|
let scrollRef!: HTMLDivElement
|
|
|
let slashPopoverRef!: HTMLDivElement
|
|
|
- let projectSearchRef: HTMLInputElement | undefined
|
|
|
|
|
|
const mirror = { input: false }
|
|
|
const inset = 56
|
|
|
@@ -360,11 +339,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
() => prompt.capture(),
|
|
|
Math.floor(Math.random() * EXAMPLES.length),
|
|
|
)
|
|
|
- const [picker, setPicker] = createStore({
|
|
|
- projectOpen: false,
|
|
|
- projectSearch: "",
|
|
|
- })
|
|
|
-
|
|
|
const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
|
|
const motion = (value: number) => ({
|
|
|
opacity: value,
|
|
|
@@ -1401,137 +1375,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
}))
|
|
|
|
|
|
const newSession = () => props.variant === "new-session"
|
|
|
- const projects = createMemo(() => props.controls.projects.available)
|
|
|
- const projectForDirectory = (directory: string | undefined) => {
|
|
|
- if (!directory) return
|
|
|
- const key = pathKey(directory)
|
|
|
- return projects().find(
|
|
|
- (project) =>
|
|
|
- (!project.server || project.server.key === props.controls.projects.server) &&
|
|
|
- (pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key)),
|
|
|
- )
|
|
|
- }
|
|
|
- const selectedProject = createMemo(() => projectForDirectory(props.controls.projects.directory))
|
|
|
- const projectResults = createMemo(() => {
|
|
|
- const search = picker.projectSearch.trim().toLowerCase()
|
|
|
- if (!search) return projects()
|
|
|
- return projects().filter((project) => displayName(project).toLowerCase().includes(search))
|
|
|
- })
|
|
|
const showAgentControl = createMemo(() => props.controls.agents.visible && props.controls.agents.options.length > 0)
|
|
|
- const selectProject = (worktree: string, server?: string) => {
|
|
|
- setPicker({
|
|
|
- projectOpen: false,
|
|
|
- projectSearch: "",
|
|
|
- })
|
|
|
- if (pathKey(worktree) === pathKey(selectedProject()?.worktree ?? "") && server === selectedProject()?.server?.key) {
|
|
|
- restoreFocus()
|
|
|
- return
|
|
|
- }
|
|
|
- props.controls.projects.select(worktree, server)
|
|
|
- restoreFocus()
|
|
|
- }
|
|
|
- const addProject = (server?: string) => {
|
|
|
- props.controls.projects.add(language.t("command.project.open"), server)
|
|
|
- }
|
|
|
-
|
|
|
- const projectItems = createMemo<ComposerPickerItemState[]>(() =>
|
|
|
- projectResults().map((project) => ({
|
|
|
- leading: (
|
|
|
- <ProjectAvatar
|
|
|
- fallback={displayName(project)}
|
|
|
- src={getProjectAvatarSource(project.id, project.icon)}
|
|
|
- variant={getProjectAvatarVariant(project.icon?.color)}
|
|
|
- />
|
|
|
- ),
|
|
|
- label: displayName(project),
|
|
|
- selected:
|
|
|
- selectedProject()?.worktree === project.worktree && selectedProject()?.server?.key === project.server?.key,
|
|
|
- onSelect: () => selectProject(project.worktree, project.server?.key),
|
|
|
- })),
|
|
|
- )
|
|
|
- const projectServers = createMemo(() =>
|
|
|
- projects()
|
|
|
- .map((project) => project.server)
|
|
|
- .filter((server, index, all) => server && all.findIndex((item) => item?.key === server.key) === index),
|
|
|
- )
|
|
|
- const addProjectLabel = createMemo(() => language.t("session.new.project.add"))
|
|
|
- const projectGroups = createMemo(() => {
|
|
|
- if (projectServers().length <= 1) return
|
|
|
- return projectServers()
|
|
|
- .map((server) => ({
|
|
|
- label: server!.name,
|
|
|
- server: server!.key,
|
|
|
- items: projectItems().filter((_, index) => projectResults()[index]?.server?.key === server!.key),
|
|
|
- action: {
|
|
|
- icon: "plus" as const,
|
|
|
- label: addProjectLabel(),
|
|
|
- onSelect: () => {
|
|
|
- setPicker("projectOpen", false)
|
|
|
- addProject(server!.key)
|
|
|
- },
|
|
|
- },
|
|
|
- }))
|
|
|
- .filter((group) => group.items.length > 0)
|
|
|
- })
|
|
|
- const projectPickerState: ComposerPickerState = {
|
|
|
- get open() {
|
|
|
- return picker.projectOpen
|
|
|
- },
|
|
|
- trigger: {
|
|
|
- action: "prompt-project",
|
|
|
- get icon() {
|
|
|
- return selectedProject() ? undefined : "folder"
|
|
|
- },
|
|
|
- get leading() {
|
|
|
- const project = selectedProject()
|
|
|
- if (!project) return
|
|
|
- return (
|
|
|
- <ProjectAvatar
|
|
|
- fallback={displayName(project)}
|
|
|
- src={getProjectAvatarSource(project.id, project.icon)}
|
|
|
- variant={getProjectAvatarVariant(project.icon?.color)}
|
|
|
- />
|
|
|
- )
|
|
|
- },
|
|
|
- get label() {
|
|
|
- const project = selectedProject()
|
|
|
- return project ? displayName(project) : language.t("session.new.project.new")
|
|
|
- },
|
|
|
- class: "max-w-[203px]",
|
|
|
- get style() {
|
|
|
- return control()
|
|
|
- },
|
|
|
- onPress: () => setPicker("projectOpen", true),
|
|
|
- },
|
|
|
- get search() {
|
|
|
- return picker.projectSearch
|
|
|
- },
|
|
|
- searchPlaceholder: language.t("session.new.project.search"),
|
|
|
- clearLabel: language.t("common.clear"),
|
|
|
- get items() {
|
|
|
- return projectItems()
|
|
|
- },
|
|
|
- get groups() {
|
|
|
- return projectGroups()
|
|
|
- },
|
|
|
- action: {
|
|
|
- icon: "plus",
|
|
|
- get label() {
|
|
|
- return addProjectLabel()
|
|
|
- },
|
|
|
- onSelect: () => {
|
|
|
- setPicker("projectOpen", false)
|
|
|
- addProject(projectServers()[0]?.key)
|
|
|
- },
|
|
|
- },
|
|
|
- onOpenChange: (open) => {
|
|
|
- setPicker("projectOpen", open)
|
|
|
- if (open) requestAnimationFrame(() => projectSearchRef?.focus())
|
|
|
- },
|
|
|
- onSearchInput: (value) => setPicker("projectSearch", value),
|
|
|
- onSearchClear: () => setPicker("projectSearch", ""),
|
|
|
- searchRef: (el) => (projectSearchRef = el),
|
|
|
- }
|
|
|
const agentControlState = createMemo<ComposerAgentControlState>(() => ({
|
|
|
title: language.t("command.agent.cycle"),
|
|
|
keybind: command.keybind("agent.cycle"),
|
|
|
@@ -1543,15 +1387,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
restoreFocus()
|
|
|
},
|
|
|
}))
|
|
|
- const newProjectTriggerState = createMemo<ComposerPickerTriggerState>(() => ({
|
|
|
- action: "prompt-project",
|
|
|
- icon: "folder-add-left",
|
|
|
- label: language.t("session.new.project.new"),
|
|
|
- class: "max-w-[160px]",
|
|
|
- style: control(),
|
|
|
- onPress: () => void addProject(),
|
|
|
- }))
|
|
|
-
|
|
|
return (
|
|
|
<div class="relative size-full flex flex-col gap-0">
|
|
|
{(promptReady(), null)}
|
|
|
@@ -1682,9 +1517,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
<Show when={showAgentControl()}>
|
|
|
<ComposerAgentControl state={agentControlState()} />
|
|
|
</Show>
|
|
|
- <Show when={newSession() && !selectedProject()}>
|
|
|
- <ComposerPickerTrigger state={newProjectTriggerState()} />
|
|
|
- </Show>
|
|
|
+ {props.toolbar}
|
|
|
<ComposerModelControl state={modelControlState()} />
|
|
|
<Show when={store.mode !== "shell" && showVariantControl()}>
|
|
|
<div
|
|
|
@@ -1738,11 +1571,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
</Tooltip>
|
|
|
</div>
|
|
|
</DockShellForm>
|
|
|
- <Show when={newSession() && selectedProject()}>
|
|
|
- <div class="flex h-7 min-w-0 items-center gap-0 px-2">
|
|
|
- <ComposerPicker state={projectPickerState} />
|
|
|
- </div>
|
|
|
- </Show>
|
|
|
</div>
|
|
|
</Match>
|
|
|
<Match when>
|
|
|
@@ -2084,40 +1912,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-type ComposerPickerItemState = {
|
|
|
- icon?: IconProps["name"]
|
|
|
- leading?: JSX.Element
|
|
|
- label: string
|
|
|
- selected?: boolean
|
|
|
- onSelect: () => void
|
|
|
-}
|
|
|
-
|
|
|
-type ComposerPickerTriggerState = {
|
|
|
- action: string
|
|
|
- icon?: IconProps["name"]
|
|
|
- leading?: JSX.Element
|
|
|
- label: string
|
|
|
- class?: string
|
|
|
- style: JSX.CSSProperties | undefined
|
|
|
- onPress: () => void
|
|
|
-}
|
|
|
-
|
|
|
-type ComposerPickerState = {
|
|
|
- open: boolean
|
|
|
- trigger: ComposerPickerTriggerState
|
|
|
- search: string
|
|
|
- searchPlaceholder: string
|
|
|
- clearLabel: string
|
|
|
- items: ComposerPickerItemState[]
|
|
|
- groups?: { label: string; server: string; items: ComposerPickerItemState[]; action: ComposerPickerItemState }[]
|
|
|
- action: ComposerPickerItemState
|
|
|
- listClass?: string
|
|
|
- searchRef: (el: HTMLInputElement) => void
|
|
|
- onOpenChange: (open: boolean) => void
|
|
|
- onSearchInput: (value: string) => void
|
|
|
- onSearchClear: () => void
|
|
|
-}
|
|
|
-
|
|
|
type ComposerAgentControlState = {
|
|
|
title: string
|
|
|
keybind: string
|
|
|
@@ -2140,125 +1934,6 @@ type ComposerModelControlState = {
|
|
|
onUnpaidClick: () => void
|
|
|
}
|
|
|
|
|
|
-function ComposerPickerTrigger(props: ComponentProps<"button"> & { state: ComposerPickerTriggerState }) {
|
|
|
- const [local, rest] = splitProps(props, ["state", "class", "style", "onClick"])
|
|
|
- return (
|
|
|
- <button
|
|
|
- {...rest}
|
|
|
- data-action={local.state.action}
|
|
|
- type="button"
|
|
|
- class={`flex h-7 min-w-0 items-center gap-1.5 rounded-sm px-2 text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none ${local.state.class ?? ""}`}
|
|
|
- style={local.state.style}
|
|
|
- onClick={() => local.state.onPress()}
|
|
|
- >
|
|
|
- <Show
|
|
|
- when={local.state.leading}
|
|
|
- fallback={
|
|
|
- <Show when={local.state.icon}>
|
|
|
- {(icon) => <Icon name={icon()} size="small" class="shrink-0 text-v2-icon-icon-muted" />}
|
|
|
- </Show>
|
|
|
- }
|
|
|
- >
|
|
|
- {(leading) => leading()}
|
|
|
- </Show>
|
|
|
- <span class="min-w-0 truncate leading-5">{local.state.label}</span>
|
|
|
- <Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
|
|
- </button>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function ComposerPickerMenuItem(props: { state: ComposerPickerItemState }) {
|
|
|
- return (
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- class="flex h-7 w-full items-center gap-2 rounded-sm px-3 text-left text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
|
|
|
- onClick={props.state.onSelect}
|
|
|
- >
|
|
|
- <Show
|
|
|
- when={props.state.leading}
|
|
|
- fallback={
|
|
|
- <Show when={props.state.icon}>
|
|
|
- {(icon) => <Icon name={icon()} size="small" class="shrink-0 text-v2-icon-icon-base" />}
|
|
|
- </Show>
|
|
|
- }
|
|
|
- >
|
|
|
- {(leading) => leading()}
|
|
|
- </Show>
|
|
|
- <span class="min-w-0 flex-1 truncate leading-5">{props.state.label}</span>
|
|
|
- <Show when={props.state.selected}>
|
|
|
- <Icon name="check-small" size="small" class="shrink-0 text-v2-icon-icon-base" />
|
|
|
- </Show>
|
|
|
- </button>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-function ComposerPicker(props: { state: ComposerPickerState }) {
|
|
|
- return (
|
|
|
- <KobaltePopover
|
|
|
- open={props.state.open}
|
|
|
- placement="bottom-start"
|
|
|
- gutter={4}
|
|
|
- modal={false}
|
|
|
- onOpenChange={props.state.onOpenChange}
|
|
|
- >
|
|
|
- <KobaltePopover.Trigger as={ComposerPickerTrigger} state={props.state.trigger} />
|
|
|
- <KobaltePopover.Portal>
|
|
|
- <KobaltePopover.Content
|
|
|
- class="w-[243px] overflow-hidden rounded-md bg-v2-background-bg-layer-01 shadow-[var(--v2-elevation-floating)] focus:outline-none"
|
|
|
- onOpenAutoFocus={(event) => event.preventDefault()}
|
|
|
- >
|
|
|
- <div class={`flex flex-col p-0.5 ${props.state.listClass ?? ""}`}>
|
|
|
- <div class="flex h-7 items-center gap-2 rounded px-3 text-v2-icon-icon-muted">
|
|
|
- <Icon name="magnifying-glass" size="small" class="shrink-0" />
|
|
|
- <input
|
|
|
- ref={props.state.searchRef}
|
|
|
- value={props.state.search}
|
|
|
- placeholder={props.state.searchPlaceholder}
|
|
|
- class="h-7 min-w-0 flex-1 border-0 bg-transparent text-[13px] font-[440] leading-5 tracking-[-0.04px] text-v2-text-text-base outline-none placeholder:text-v2-text-text-faint"
|
|
|
- onInput={(event) => props.state.onSearchInput(event.currentTarget.value)}
|
|
|
- />
|
|
|
- <Show when={props.state.search.trim()}>
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- class="flex size-5 items-center justify-center rounded text-v2-icon-icon-muted hover:bg-v2-overlay-simple-overlay-hover"
|
|
|
- onClick={props.state.onSearchClear}
|
|
|
- aria-label={props.state.clearLabel}
|
|
|
- >
|
|
|
- <Icon name="close-small" size="small" />
|
|
|
- </button>
|
|
|
- </Show>
|
|
|
- </div>
|
|
|
- <Show
|
|
|
- when={props.state.groups}
|
|
|
- fallback={<For each={props.state.items}>{(item) => <ComposerPickerMenuItem state={item} />}</For>}
|
|
|
- >
|
|
|
- {(groups) => (
|
|
|
- <For each={groups()}>
|
|
|
- {(group) => (
|
|
|
- <div>
|
|
|
- <div class="flex h-7 select-none items-center pl-1.5 pr-3 text-[11px] font-[530] leading-none tracking-[0.05px] text-v2-text-text-faint">
|
|
|
- {group.label}
|
|
|
- </div>
|
|
|
- <For each={group.items}>{(item) => <ComposerPickerMenuItem state={item} />}</For>
|
|
|
- <ComposerPickerMenuItem state={group.action} />
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </For>
|
|
|
- )}
|
|
|
- </Show>
|
|
|
- </div>
|
|
|
- <Show when={!props.state.groups}>
|
|
|
- <div class="h-px bg-v2-border-border-muted" />
|
|
|
- <div class="flex flex-col p-0.5">
|
|
|
- <ComposerPickerMenuItem state={props.state.action} />
|
|
|
- </div>
|
|
|
- </Show>
|
|
|
- </KobaltePopover.Content>
|
|
|
- </KobaltePopover.Portal>
|
|
|
- </KobaltePopover>
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
|
|
|
return (
|
|
|
<div class="relative">
|