refactor(app): split session composer (#33954)
This commit is contained in:
parent
fae100fe72
commit
a2b847e29e
11 changed files with 710 additions and 701 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import type { Todo } from "@opencode-ai/sdk/v2"
|
import type { Todo } from "@opencode-ai/sdk/v2"
|
||||||
import { createPromptState } from "@/context/prompt"
|
import { createPromptState } from "@/context/prompt"
|
||||||
import { SessionComposerRegion } from "@/pages/session/composer"
|
import { SessionComposerRegion, createSessionComposerRegionController } from "@/pages/session/composer"
|
||||||
import { createPromptInputHistory, PromptInput } from "./prompt-input"
|
import { createPromptInputHistory, PromptInput } from "./prompt-input"
|
||||||
|
|
||||||
function createPromptInputStoryRuntime() {
|
function createPromptInputStoryRuntime() {
|
||||||
|
|
@ -54,12 +54,6 @@ function PromptInputExample() {
|
||||||
paid: true,
|
paid: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
},
|
},
|
||||||
projects: {
|
|
||||||
available: [{ name: "Story project", worktree: "/tmp/story", sandboxes: [] }],
|
|
||||||
directory: "/tmp/story",
|
|
||||||
select() {},
|
|
||||||
add() {},
|
|
||||||
},
|
|
||||||
session: {
|
session: {
|
||||||
id: "story-session",
|
id: "story-session",
|
||||||
tabs: {
|
tabs: {
|
||||||
|
|
@ -142,7 +136,6 @@ function PromptInputWithOpenDock() {
|
||||||
paid: true,
|
paid: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
},
|
},
|
||||||
projects: { available: [], directory: "/tmp/story", select: () => {}, add: () => {} },
|
|
||||||
session: {
|
session: {
|
||||||
id: "story-session",
|
id: "story-session",
|
||||||
tabs: {
|
tabs: {
|
||||||
|
|
@ -166,22 +159,37 @@ function PromptInputWithOpenDock() {
|
||||||
closing: () => false,
|
closing: () => false,
|
||||||
opening: () => false,
|
opening: () => false,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionComposerRegion
|
<SessionComposerRegion
|
||||||
state={state}
|
controller={
|
||||||
sessionKey="story-session"
|
createSessionComposerRegionController({
|
||||||
sessionID="story-session"
|
state,
|
||||||
controls={inputControls}
|
sessionKey: () => "story-session",
|
||||||
promptInput={{ ...input, ref: () => {}, newSessionWorktree: "", onNewSessionWorktreeReset: () => {} }}
|
sessionID: () => "story-session",
|
||||||
todo={{
|
prompt: input.state,
|
||||||
collapsed: controls.todoCollapsed,
|
ready: () => true,
|
||||||
onToggle: () => setControls("todoCollapsed", (collapsed) => !collapsed),
|
centered: () => false,
|
||||||
}}
|
todo: {
|
||||||
ready
|
collapsed: () => controls.todoCollapsed,
|
||||||
centered={false}
|
onToggle: () => setControls("todoCollapsed", (collapsed) => !collapsed),
|
||||||
onResponseSubmit={() => {}}
|
},
|
||||||
setPromptDockRef={() => {}}
|
followup: () => undefined,
|
||||||
|
revert: () => undefined,
|
||||||
|
onResponseSubmit: () => {},
|
||||||
|
openParent: () => {},
|
||||||
|
setPromptRef: () => {},
|
||||||
|
setDockRef: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
promptInput={
|
||||||
|
<PromptInput
|
||||||
|
controls={inputControls}
|
||||||
|
{...input}
|
||||||
|
ref={() => {}}
|
||||||
|
newSessionWorktree=""
|
||||||
|
onNewSessionWorktreeReset={() => {}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import {
|
||||||
createEffect,
|
createEffect,
|
||||||
on,
|
on,
|
||||||
Component,
|
Component,
|
||||||
splitProps,
|
|
||||||
For,
|
|
||||||
Show,
|
Show,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
createMemo,
|
createMemo,
|
||||||
|
|
@ -13,10 +11,8 @@ import {
|
||||||
createResource,
|
createResource,
|
||||||
Switch,
|
Switch,
|
||||||
Match,
|
Match,
|
||||||
type ComponentProps,
|
|
||||||
type JSX,
|
type JSX,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import { Popover as KobaltePopover } from "@kobalte/core/popover"
|
|
||||||
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
import { createStore, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
import type { useLocal } from "@/context/local"
|
import type { useLocal } from "@/context/local"
|
||||||
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
|
||||||
|
|
@ -30,15 +26,14 @@ import {
|
||||||
AgentPart,
|
AgentPart,
|
||||||
FileAttachmentPart,
|
FileAttachmentPart,
|
||||||
} from "@/context/prompt"
|
} from "@/context/prompt"
|
||||||
import { getProjectAvatarVariant, useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { DockShellForm, DockTray } from "@opencode-ai/ui/dock-surface"
|
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 { 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 { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
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 { createPromptInputTransientState } from "./prompt-input/transient-state"
|
||||||
import { showToast } from "@/utils/toast"
|
import { showToast } from "@/utils/toast"
|
||||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
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>
|
export type PromptInputState = ReturnType<typeof usePrompt>
|
||||||
|
|
||||||
|
|
@ -100,20 +93,6 @@ export type PromptInputControls = {
|
||||||
paid: boolean
|
paid: boolean
|
||||||
loading: 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: {
|
session: {
|
||||||
id?: string
|
id?: string
|
||||||
tabs: {
|
tabs: {
|
||||||
|
|
@ -184,6 +163,7 @@ export interface PromptInputProps {
|
||||||
onQueue?: (draft: FollowupDraft) => void
|
onQueue?: (draft: FollowupDraft) => void
|
||||||
onAbort?: () => void
|
onAbort?: () => void
|
||||||
onSubmit?: () => void
|
onSubmit?: () => void
|
||||||
|
toolbar?: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXAMPLES = [
|
const EXAMPLES = [
|
||||||
|
|
@ -232,7 +212,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
let fileInputRef: HTMLInputElement | undefined
|
let fileInputRef: HTMLInputElement | undefined
|
||||||
let scrollRef!: HTMLDivElement
|
let scrollRef!: HTMLDivElement
|
||||||
let slashPopoverRef!: HTMLDivElement
|
let slashPopoverRef!: HTMLDivElement
|
||||||
let projectSearchRef: HTMLInputElement | undefined
|
|
||||||
|
|
||||||
const mirror = { input: false }
|
const mirror = { input: false }
|
||||||
const inset = 56
|
const inset = 56
|
||||||
|
|
@ -360,11 +339,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
() => prompt.capture(),
|
() => prompt.capture(),
|
||||||
Math.floor(Math.random() * EXAMPLES.length),
|
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 buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
|
||||||
const motion = (value: number) => ({
|
const motion = (value: number) => ({
|
||||||
opacity: value,
|
opacity: value,
|
||||||
|
|
@ -1401,137 +1375,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const newSession = () => props.variant === "new-session"
|
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 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>(() => ({
|
const agentControlState = createMemo<ComposerAgentControlState>(() => ({
|
||||||
title: language.t("command.agent.cycle"),
|
title: language.t("command.agent.cycle"),
|
||||||
keybind: command.keybind("agent.cycle"),
|
keybind: command.keybind("agent.cycle"),
|
||||||
|
|
@ -1543,15 +1387,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
restoreFocus()
|
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 (
|
return (
|
||||||
<div class="relative size-full flex flex-col gap-0">
|
<div class="relative size-full flex flex-col gap-0">
|
||||||
{(promptReady(), null)}
|
{(promptReady(), null)}
|
||||||
|
|
@ -1682,9 +1517,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
<Show when={showAgentControl()}>
|
<Show when={showAgentControl()}>
|
||||||
<ComposerAgentControl state={agentControlState()} />
|
<ComposerAgentControl state={agentControlState()} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={newSession() && !selectedProject()}>
|
{props.toolbar}
|
||||||
<ComposerPickerTrigger state={newProjectTriggerState()} />
|
|
||||||
</Show>
|
|
||||||
<ComposerModelControl state={modelControlState()} />
|
<ComposerModelControl state={modelControlState()} />
|
||||||
<Show when={store.mode !== "shell" && showVariantControl()}>
|
<Show when={store.mode !== "shell" && showVariantControl()}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -1738,11 +1571,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</DockShellForm>
|
</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>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when>
|
<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 = {
|
type ComposerAgentControlState = {
|
||||||
title: string
|
title: string
|
||||||
keybind: string
|
keybind: string
|
||||||
|
|
@ -2140,125 +1934,6 @@ type ComposerModelControlState = {
|
||||||
onUnpaidClick: () => void
|
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 }) {
|
function ComposerAgentControl(props: { state: ComposerAgentControlState }) {
|
||||||
return (
|
return (
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|
|
||||||
278
packages/app/src/components/prompt-project-selector.tsx
Normal file
278
packages/app/src/components/prompt-project-selector.tsx
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
import { Popover } from "@kobalte/core/popover"
|
||||||
|
import { For, Show, splitProps, type Accessor, type ComponentProps } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
|
import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2"
|
||||||
|
import { getProjectAvatarVariant } from "@/context/layout"
|
||||||
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers"
|
||||||
|
import { pathKey } from "@/utils/path-key"
|
||||||
|
|
||||||
|
export type PromptProject = {
|
||||||
|
name?: string
|
||||||
|
id?: string
|
||||||
|
worktree: string
|
||||||
|
sandboxes?: string[]
|
||||||
|
icon?: { color?: string; url?: string; override?: string }
|
||||||
|
server?: { key: string; name: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PromptProjectControls = {
|
||||||
|
available: PromptProject[]
|
||||||
|
directory: string
|
||||||
|
server?: string
|
||||||
|
select: (worktree: string, server?: string) => void
|
||||||
|
add: (title: string, server?: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPromptProjectController(input: {
|
||||||
|
controls: Accessor<PromptProjectControls>
|
||||||
|
onDone: () => void
|
||||||
|
}) {
|
||||||
|
const language = useLanguage()
|
||||||
|
const [store, setStore] = createStore({ open: false, search: "" })
|
||||||
|
let searchRef: HTMLInputElement | undefined
|
||||||
|
|
||||||
|
const selected = () => {
|
||||||
|
const key = pathKey(input.controls().directory)
|
||||||
|
return input
|
||||||
|
.controls()
|
||||||
|
.available.find(
|
||||||
|
(project) =>
|
||||||
|
(!project.server || project.server.key === input.controls().server) &&
|
||||||
|
(pathKey(project.worktree) === key || project.sandboxes?.some((sandbox) => pathKey(sandbox) === key)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const projects = () => {
|
||||||
|
const search = store.search.trim().toLowerCase()
|
||||||
|
if (!search) return input.controls().available
|
||||||
|
return input.controls().available.filter((project) => displayName(project).toLowerCase().includes(search))
|
||||||
|
}
|
||||||
|
const servers = () =>
|
||||||
|
input
|
||||||
|
.controls()
|
||||||
|
.available.map((project) => project.server)
|
||||||
|
.filter((server, index, all) => server && all.findIndex((item) => item?.key === server.key) === index)
|
||||||
|
const close = () => {
|
||||||
|
setStore({ open: false, search: "" })
|
||||||
|
input.onDone()
|
||||||
|
}
|
||||||
|
const select = (project: PromptProject) => {
|
||||||
|
if (
|
||||||
|
pathKey(project.worktree) !== pathKey(selected()?.worktree ?? "") ||
|
||||||
|
project.server?.key !== selected()?.server?.key
|
||||||
|
) {
|
||||||
|
input.controls().select(project.worktree, project.server?.key)
|
||||||
|
}
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
const add = (server?: string) => {
|
||||||
|
setStore("open", false)
|
||||||
|
input.controls().add(language.t("command.project.open"), server)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selected,
|
||||||
|
projects,
|
||||||
|
servers,
|
||||||
|
open: () => store.open,
|
||||||
|
search: () => store.search,
|
||||||
|
labels: {
|
||||||
|
add: () => language.t("session.new.project.add"),
|
||||||
|
clear: () => language.t("common.clear"),
|
||||||
|
new: () => language.t("session.new.project.new"),
|
||||||
|
search: () => language.t("session.new.project.search"),
|
||||||
|
},
|
||||||
|
add,
|
||||||
|
select,
|
||||||
|
setOpen(open: boolean) {
|
||||||
|
setStore("open", open)
|
||||||
|
if (open) requestAnimationFrame(() => searchRef?.focus())
|
||||||
|
},
|
||||||
|
setSearch(value: string) {
|
||||||
|
setStore("search", value)
|
||||||
|
},
|
||||||
|
setSearchRef(el: HTMLInputElement) {
|
||||||
|
searchRef = el
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PromptProjectController = ReturnType<typeof createPromptProjectController>
|
||||||
|
|
||||||
|
export function PromptProjectSelector(props: { controller: PromptProjectController }) {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
open={props.controller.open()}
|
||||||
|
placement="bottom-start"
|
||||||
|
gutter={4}
|
||||||
|
modal={false}
|
||||||
|
onOpenChange={(open) => props.controller.setOpen(open)}
|
||||||
|
>
|
||||||
|
<Popover.Trigger as={ProjectTrigger} controller={props.controller} />
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.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">
|
||||||
|
<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={(el) => props.controller.setSearchRef(el)}
|
||||||
|
value={props.controller.search()}
|
||||||
|
placeholder={props.controller.labels.search()}
|
||||||
|
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.controller.setSearch(event.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Show when={props.controller.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.controller.setSearch("")}
|
||||||
|
aria-label={props.controller.labels.clear()}
|
||||||
|
>
|
||||||
|
<Icon name="close-small" size="small" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={props.controller.servers().length > 1}
|
||||||
|
fallback={
|
||||||
|
<For each={props.controller.projects()}>
|
||||||
|
{(project) => (
|
||||||
|
<ProjectItem
|
||||||
|
project={project}
|
||||||
|
selected={props.controller.selected()}
|
||||||
|
onSelect={props.controller.select}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={props.controller.servers()}>
|
||||||
|
{(server) => (
|
||||||
|
<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">
|
||||||
|
{server!.name}
|
||||||
|
</div>
|
||||||
|
<For each={props.controller.projects().filter((project) => project.server?.key === server!.key)}>
|
||||||
|
{(project) => (
|
||||||
|
<ProjectItem
|
||||||
|
project={project}
|
||||||
|
selected={props.controller.selected()}
|
||||||
|
onSelect={props.controller.select}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<ProjectAction
|
||||||
|
label={props.controller.labels.add()}
|
||||||
|
onSelect={() => props.controller.add(server!.key)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show when={props.controller.servers().length <= 1}>
|
||||||
|
<div class="h-px bg-v2-border-border-muted" />
|
||||||
|
<div class="flex flex-col p-0.5">
|
||||||
|
<ProjectAction
|
||||||
|
label={props.controller.labels.add()}
|
||||||
|
onSelect={() => props.controller.add(props.controller.servers()[0]?.key)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PromptProjectAddButton(props: { controller: PromptProjectController }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
data-action="prompt-project"
|
||||||
|
type="button"
|
||||||
|
class="flex h-7 min-w-0 max-w-[160px] 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"
|
||||||
|
onClick={() => props.controller.add()}
|
||||||
|
>
|
||||||
|
<Icon name="folder-add-left" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
|
<span class="min-w-0 truncate leading-5">{props.controller.labels.new()}</span>
|
||||||
|
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectTrigger(props: ComponentProps<"button"> & { controller: PromptProjectController }) {
|
||||||
|
const [local, rest] = splitProps(props, ["controller", "class", "onClick"])
|
||||||
|
const project = () => local.controller.selected()
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...rest}
|
||||||
|
data-action="prompt-project"
|
||||||
|
type="button"
|
||||||
|
class="flex h-7 min-w-0 max-w-[203px] 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"
|
||||||
|
onClick={() => local.controller.setOpen(true)}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={project()}
|
||||||
|
fallback={<Icon name="folder-add-left" size="small" class="shrink-0 text-v2-icon-icon-muted" />}
|
||||||
|
>
|
||||||
|
{(item) => (
|
||||||
|
<ProjectAvatar
|
||||||
|
fallback={displayName(item())}
|
||||||
|
src={getProjectAvatarSource(item().id, item().icon)}
|
||||||
|
variant={getProjectAvatarVariant(item().icon?.color)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
<span class="min-w-0 truncate leading-5">
|
||||||
|
{project() ? displayName(project()!) : local.controller.labels.new()}
|
||||||
|
</span>
|
||||||
|
<Icon name="chevron-down" size="small" class="shrink-0 text-v2-icon-icon-muted" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectItem(props: {
|
||||||
|
project: PromptProject
|
||||||
|
selected?: PromptProject
|
||||||
|
onSelect: (project: PromptProject) => void
|
||||||
|
}) {
|
||||||
|
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.onSelect(props.project)}
|
||||||
|
>
|
||||||
|
<ProjectAvatar
|
||||||
|
fallback={displayName(props.project)}
|
||||||
|
src={getProjectAvatarSource(props.project.id, props.project.icon)}
|
||||||
|
variant={getProjectAvatarVariant(props.project.icon?.color)}
|
||||||
|
/>
|
||||||
|
<span class="min-w-0 flex-1 truncate leading-5">{displayName(props.project)}</span>
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
props.selected?.worktree === props.project.worktree &&
|
||||||
|
props.selected?.server?.key === props.project.server?.key
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon name="check-small" size="small" class="shrink-0 text-v2-icon-icon-base" />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectAction(props: { label: string; onSelect: () => void }) {
|
||||||
|
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.onSelect}
|
||||||
|
>
|
||||||
|
<Icon name="plus" size="small" />
|
||||||
|
<span class="min-w-0 flex-1 truncate leading-5">{props.label}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
import { createEffect, createMemo, onMount, untrack } from "solid-js"
|
import { Show, createEffect, createMemo, createResource, untrack } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useSearchParams } from "@solidjs/router"
|
import { useSearchParams } from "@solidjs/router"
|
||||||
import { NewSessionDesignView } from "@/components/session"
|
import { NewSessionDesignView } from "@/components/session"
|
||||||
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
|
import {
|
||||||
|
PromptProjectAddButton,
|
||||||
|
PromptProjectSelector,
|
||||||
|
createPromptProjectController,
|
||||||
|
} from "@/components/prompt-project-selector"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { usePrompt } from "@/context/prompt"
|
import { usePrompt } from "@/context/prompt"
|
||||||
import { useSDK } from "@/context/sdk"
|
import { useSDK } from "@/context/sdk"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useServerSync } from "@/context/server-sync"
|
import { useServerSync } from "@/context/server-sync"
|
||||||
import {
|
import { useLanguage } from "@/context/language"
|
||||||
createSessionComposerControls,
|
import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer"
|
||||||
createSessionComposerState,
|
|
||||||
SessionComposerRegion,
|
|
||||||
} from "@/pages/session/composer"
|
|
||||||
import { useSessionKey } from "@/pages/session/session-layout"
|
import { useSessionKey } from "@/pages/session/session-layout"
|
||||||
import { useComposerCommands } from "@/pages/session/use-composer-commands"
|
import { useComposerCommands } from "@/pages/session/use-composer-commands"
|
||||||
|
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt
|
* The `/new-session` draft page. Unlike `session.tsx`, this only renders the prompt
|
||||||
|
|
@ -26,6 +30,7 @@ export default function NewSessionPage() {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
const serverSync = useServerSync()
|
const serverSync = useServerSync()
|
||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
|
const language = useLanguage()
|
||||||
const route = useSessionKey()
|
const route = useSessionKey()
|
||||||
const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>()
|
||||||
|
|
||||||
|
|
@ -33,12 +38,16 @@ export default function NewSessionPage() {
|
||||||
|
|
||||||
let inputRef: HTMLDivElement | undefined
|
let inputRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
const composer = createSessionComposerState()
|
const inputController = createPromptInputController({
|
||||||
const composerControls = createSessionComposerControls({
|
|
||||||
sessionKey: route.sessionKey,
|
sessionKey: route.sessionKey,
|
||||||
sessionID: () => route.params.id,
|
sessionID: () => route.params.id,
|
||||||
queryOptions: serverSync().queryOptions,
|
queryOptions: serverSync().queryOptions,
|
||||||
})
|
})
|
||||||
|
const projectControls = createPromptProjectControls()
|
||||||
|
const projectController = createPromptProjectController({
|
||||||
|
controls: projectControls,
|
||||||
|
onDone: () => inputRef?.focus(),
|
||||||
|
})
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
worktree: "main",
|
worktree: "main",
|
||||||
|
|
@ -61,9 +70,15 @@ export default function NewSessionPage() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
createEffect(() => {
|
||||||
|
if (!prompt.ready()) return
|
||||||
requestAnimationFrame(() => inputRef?.focus())
|
requestAnimationFrame(() => inputRef?.focus())
|
||||||
})
|
})
|
||||||
|
const ready = Promise.resolve()
|
||||||
|
const [promptReady] = createResource(
|
||||||
|
() => prompt.ready.promise ?? ready,
|
||||||
|
(promise) => promise.then(() => true),
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="relative size-full overflow-hidden flex flex-col">
|
<div class="relative size-full overflow-hidden flex flex-col">
|
||||||
|
|
@ -71,26 +86,39 @@ export default function NewSessionPage() {
|
||||||
<div class="@container relative flex flex-col min-h-0 h-full bg-background-stronger flex-1">
|
<div class="@container relative flex flex-col min-h-0 h-full bg-background-stronger flex-1">
|
||||||
<div class="flex-1 min-h-0 overflow-hidden rounded-[10px]">
|
<div class="flex-1 min-h-0 overflow-hidden rounded-[10px]">
|
||||||
<NewSessionDesignView>
|
<NewSessionDesignView>
|
||||||
<SessionComposerRegion
|
<div class={NEW_SESSION_CONTENT_WIDTH}>
|
||||||
state={composer}
|
<Show
|
||||||
sessionKey={route.sessionKey()}
|
when={prompt.ready() || promptReady()}
|
||||||
sessionID={route.params.id}
|
fallback={
|
||||||
controls={composerControls()}
|
<div class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak pointer-events-none">
|
||||||
promptInput={{
|
{language.t("prompt.loading")}
|
||||||
ref: (el) => {
|
</div>
|
||||||
inputRef = el
|
}
|
||||||
},
|
>
|
||||||
newSessionWorktree: newSessionWorktree(),
|
<div class="flex flex-col gap-3">
|
||||||
onNewSessionWorktreeReset: () => setStore("worktree", "main"),
|
<PromptInput
|
||||||
onSubmit: () => comments.clear(),
|
controls={inputController()}
|
||||||
}}
|
variant="new-session"
|
||||||
todo={{ collapsed: false, onToggle: () => {} }}
|
ref={(el) => {
|
||||||
ready
|
inputRef = el
|
||||||
centered={false}
|
}}
|
||||||
placement="inline"
|
newSessionWorktree={newSessionWorktree()}
|
||||||
onResponseSubmit={() => {}}
|
onNewSessionWorktreeReset={() => setStore("worktree", "main")}
|
||||||
setPromptDockRef={() => {}}
|
onSubmit={() => comments.clear()}
|
||||||
/>
|
toolbar={
|
||||||
|
<Show when={!projectController.selected()}>
|
||||||
|
<PromptProjectAddButton controller={projectController} />
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Show when={projectController.selected()}>
|
||||||
|
<div class="flex h-7 min-w-0 items-center gap-0 px-2">
|
||||||
|
<PromptProjectSelector controller={projectController} />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</NewSessionDesignView>
|
</NewSessionDesignView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,12 @@ import { useServerSDK } from "@/context/server-sdk"
|
||||||
import { useSettings } from "@/context/settings"
|
import { useSettings } from "@/context/settings"
|
||||||
import { useSync } from "@/context/sync"
|
import { useSync } from "@/context/sync"
|
||||||
import { useTerminal } from "@/context/terminal"
|
import { useTerminal } from "@/context/terminal"
|
||||||
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
import { type FollowupDraft, sendFollowupDraft } from "@/components/prompt-input/submit"
|
||||||
import {
|
import {
|
||||||
createSessionComposerControls,
|
createPromptInputController,
|
||||||
createSessionComposerState,
|
createSessionComposerController,
|
||||||
|
createSessionComposerRegionController,
|
||||||
SessionComposerRegion,
|
SessionComposerRegion,
|
||||||
} from "@/pages/session/composer"
|
} from "@/pages/session/composer"
|
||||||
import {
|
import {
|
||||||
|
|
@ -156,8 +158,8 @@ export default function Page() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const composer = createSessionComposerState()
|
const composer = createSessionComposerController()
|
||||||
const composerControls = createSessionComposerControls({
|
const inputController = createPromptInputController({
|
||||||
sessionKey,
|
sessionKey,
|
||||||
sessionID: () => params.id,
|
sessionID: () => params.id,
|
||||||
queryOptions: serverSync().queryOptions,
|
queryOptions: serverSync().queryOptions,
|
||||||
|
|
@ -1569,62 +1571,28 @@ export default function Page() {
|
||||||
|
|
||||||
useUsageExceededDialogs()
|
useUsageExceededDialogs()
|
||||||
|
|
||||||
const composerRegion = (placement: "dock" | "inline") => (
|
const composerRegion = () => {
|
||||||
<SessionComposerRegion
|
const controller = createSessionComposerRegionController({
|
||||||
state={composer}
|
state: composer,
|
||||||
sessionKey={sessionKey()}
|
sessionKey,
|
||||||
sessionID={params.id}
|
sessionID: () => params.id,
|
||||||
controls={composerControls()}
|
prompt,
|
||||||
promptInput={{
|
ready: () => !store.deferRender && messagesReady(),
|
||||||
ref: (el) => {
|
centered,
|
||||||
inputRef = el
|
todo: {
|
||||||
},
|
collapsed: () => view().todoCollapsed.get(),
|
||||||
newSessionWorktree: newSessionWorktree(),
|
|
||||||
onNewSessionWorktreeReset: () => setStore("newSessionWorktree", "main"),
|
|
||||||
onSubmit: () => {
|
|
||||||
comments.clear()
|
|
||||||
resumeScroll()
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
todo={{
|
|
||||||
collapsed: view().todoCollapsed.get(),
|
|
||||||
onToggle: () => view().todoCollapsed.set(!view().todoCollapsed.get()),
|
onToggle: () => view().todoCollapsed.set(!view().todoCollapsed.get()),
|
||||||
}}
|
},
|
||||||
ready={!store.deferRender && messagesReady()}
|
followup: () =>
|
||||||
centered={placement === "dock" && centered()}
|
|
||||||
placement={placement}
|
|
||||||
openParent={() => {
|
|
||||||
const id = info()?.parentID
|
|
||||||
if (!id) return
|
|
||||||
navigate(
|
|
||||||
params.serverKey
|
|
||||||
? sessionHref(requireServerKey(params.serverKey), id)
|
|
||||||
: legacySessionHref(sdk().directory, id),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
onResponseSubmit={resumeScroll}
|
|
||||||
followup={
|
|
||||||
params.id && !isChildSession()
|
params.id && !isChildSession()
|
||||||
? {
|
? {
|
||||||
queue: queueEnabled,
|
|
||||||
items: followupDock(),
|
items: followupDock(),
|
||||||
sending: sendingFollowup(),
|
sending: sendingFollowup(),
|
||||||
edit: editingFollowup(),
|
onSend: (id) => void sendFollowup(params.id!, id, { manual: true }),
|
||||||
onQueue: queueFollowup,
|
|
||||||
onAbort: () => {
|
|
||||||
const id = params.id
|
|
||||||
if (!id) return
|
|
||||||
setFollowup("paused", id, true)
|
|
||||||
},
|
|
||||||
onSend: (id) => {
|
|
||||||
void sendFollowup(params.id!, id, { manual: true })
|
|
||||||
},
|
|
||||||
onEdit: editFollowup,
|
onEdit: editFollowup,
|
||||||
onEditLoaded: clearFollowupEdit,
|
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined,
|
||||||
}
|
revert: () =>
|
||||||
revert={
|
|
||||||
rolled().length > 0
|
rolled().length > 0
|
||||||
? {
|
? {
|
||||||
items: rolled(),
|
items: rolled(),
|
||||||
|
|
@ -1632,13 +1600,53 @@ export default function Page() {
|
||||||
disabled: reverting(),
|
disabled: reverting(),
|
||||||
onRestore: restore,
|
onRestore: restore,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined,
|
||||||
}
|
onResponseSubmit: resumeScroll,
|
||||||
setPromptDockRef={(el) => {
|
openParent: () => {
|
||||||
|
const id = info()?.parentID
|
||||||
|
if (!id) return
|
||||||
|
navigate(
|
||||||
|
params.serverKey
|
||||||
|
? sessionHref(requireServerKey(params.serverKey), id)
|
||||||
|
: legacySessionHref(sdk().directory, id),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
setPromptRef: (el) => {
|
||||||
|
inputRef = el
|
||||||
|
},
|
||||||
|
setDockRef: (el) => {
|
||||||
promptDock = el
|
promptDock = el
|
||||||
}}
|
},
|
||||||
/>
|
})
|
||||||
)
|
return (
|
||||||
|
<SessionComposerRegion
|
||||||
|
controller={controller}
|
||||||
|
promptInput={
|
||||||
|
<PromptInput
|
||||||
|
controls={inputController()}
|
||||||
|
ref={(el) => {
|
||||||
|
inputRef = el
|
||||||
|
}}
|
||||||
|
newSessionWorktree={newSessionWorktree()}
|
||||||
|
onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
|
||||||
|
onSubmit={() => {
|
||||||
|
comments.clear()
|
||||||
|
resumeScroll()
|
||||||
|
}}
|
||||||
|
edit={editingFollowup()}
|
||||||
|
onEditLoaded={clearFollowupEdit}
|
||||||
|
shouldQueue={queueEnabled}
|
||||||
|
onQueue={queueFollowup}
|
||||||
|
onAbort={() => {
|
||||||
|
const id = params.id
|
||||||
|
if (!id) return
|
||||||
|
setFollowup("paused", id, true)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const mobileTabs = (compact = false, bottom = false) => (
|
const mobileTabs = (compact = false, bottom = false) => (
|
||||||
<Tabs value={store.mobileTab} class="h-auto">
|
<Tabs value={store.mobileTab} class="h-auto">
|
||||||
|
|
@ -1775,7 +1783,9 @@ export default function Page() {
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={(params.id || !newSessionDesign()) && !mobileChanges()}>{composerRegion("dock")}</Show>
|
<Show when={(params.id || !newSessionDesign()) && !mobileChanges()}>
|
||||||
|
{(_) => composerRegion()}
|
||||||
|
</Show>
|
||||||
<Show when={!!params.id && mobileTabsBottom()}>{mobileTabs(true, true)}</Show>
|
<Show when={!!params.id && mobileTabsBottom()}>{mobileTabs(true, true)}</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export { SessionComposerRegion } from "./session-composer-region"
|
export { SessionComposerRegion } from "./session-composer-region"
|
||||||
export { createSessionComposerControls } from "./session-composer-controls"
|
export { createPromptInputController, createPromptProjectControls } from "./session-composer-controls"
|
||||||
export { createSessionComposerState } from "./session-composer-state"
|
export { createSessionComposerController } from "./session-composer-state"
|
||||||
|
export { createSessionComposerRegionController } from "./session-composer-region-controller"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { createQuery } from "@tanstack/solid-query"
|
||||||
import { useNavigate, useSearchParams } from "@solidjs/router"
|
import { useNavigate, useSearchParams } from "@solidjs/router"
|
||||||
import { type Accessor, createMemo } from "solid-js"
|
import { type Accessor, createMemo } from "solid-js"
|
||||||
import type { PromptInputControls } from "@/components/prompt-input"
|
import type { PromptInputControls } from "@/components/prompt-input"
|
||||||
|
import type { PromptProjectControls } from "@/components/prompt-project-selector"
|
||||||
import { useDirectoryPicker } from "@/components/directory-picker"
|
import { useDirectoryPicker } from "@/components/directory-picker"
|
||||||
import { useGlobal } from "@/context/global"
|
import { useGlobal } from "@/context/global"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
|
@ -17,26 +18,55 @@ import { useTabs } from "@/context/tabs"
|
||||||
import { useProviders } from "@/hooks/use-providers"
|
import { useProviders } from "@/hooks/use-providers"
|
||||||
import { pathKey } from "@/utils/path-key"
|
import { pathKey } from "@/utils/path-key"
|
||||||
|
|
||||||
export function createSessionComposerControls(input: {
|
export function createPromptInputController(input: {
|
||||||
sessionKey: Accessor<string>
|
sessionKey: Accessor<string>
|
||||||
sessionID: Accessor<string | undefined>
|
sessionID: Accessor<string | undefined>
|
||||||
queryOptions: Pick<QueryOptionsApi, "agents" | "providers">
|
queryOptions: Pick<QueryOptionsApi, "agents" | "providers">
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
|
||||||
const layout = useLayout()
|
const layout = useLayout()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const providers = useProviders()
|
const providers = useProviders()
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
|
const sync = useSync()
|
||||||
|
const sdk = useSDK()
|
||||||
|
const view = layout.view(input.sessionKey)
|
||||||
|
const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory)))
|
||||||
|
const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null))
|
||||||
|
const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory)))
|
||||||
|
|
||||||
|
return createMemo<PromptInputControls>(() => ({
|
||||||
|
agents: {
|
||||||
|
available: sync().data.agent,
|
||||||
|
options: local.agent.list().map((agent) => agent.name),
|
||||||
|
current: local.agent.current()?.name ?? "",
|
||||||
|
loading: agentsQuery.isLoading,
|
||||||
|
visible: settings.visibility.customAgents(),
|
||||||
|
select: local.agent.set,
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
selection: local.model,
|
||||||
|
paid: providers.paid().length > 0,
|
||||||
|
loading: agentsQuery.isLoading || providersQuery.isLoading || globalProvidersQuery.isLoading,
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
id: input.sessionID(),
|
||||||
|
tabs: layout.tabs(input.sessionKey),
|
||||||
|
reviewPanel: view.reviewPanel,
|
||||||
|
},
|
||||||
|
newLayoutDesigns: settings.general.newLayoutDesigns(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPromptProjectControls() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const layout = useLayout()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const serverSDK = useServerSDK()
|
const serverSDK = useServerSDK()
|
||||||
const sync = useSync()
|
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const tabs = useTabs()
|
const tabs = useTabs()
|
||||||
const global = useGlobal()
|
const global = useGlobal()
|
||||||
const pickDirectory = useDirectoryPicker()
|
const pickDirectory = useDirectoryPicker()
|
||||||
const [search] = useSearchParams<{ draftId?: string }>()
|
const [search] = useSearchParams<{ draftId?: string }>()
|
||||||
const view = layout.view(input.sessionKey)
|
|
||||||
|
|
||||||
const projectServer = () => serverSDK().server
|
const projectServer = () => serverSDK().server
|
||||||
const projectServerCtx = createMemo(() => global.ensureServerCtx(projectServer()))
|
const projectServerCtx = createMemo(() => global.ensureServerCtx(projectServer()))
|
||||||
const projects = createMemo(() => {
|
const projects = createMemo(() => {
|
||||||
|
|
@ -44,17 +74,13 @@ export function createSessionComposerControls(input: {
|
||||||
return search.draftId ? projectServerCtx().projects.list() : layout.projects.list()
|
return search.draftId ? projectServerCtx().projects.list() : layout.projects.list()
|
||||||
}
|
}
|
||||||
return server.list.flatMap((conn) => {
|
return server.list.flatMap((conn) => {
|
||||||
const server = { key: ServerConnection.key(conn), name: serverName(conn) }
|
const item = { key: ServerConnection.key(conn), name: serverName(conn) }
|
||||||
return global
|
return global
|
||||||
.ensureServerCtx(conn)
|
.ensureServerCtx(conn)
|
||||||
.projects.list()
|
.projects.list()
|
||||||
.map((project) => ({ ...project, server }))
|
.map((project) => ({ ...project, server: item }))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory)))
|
|
||||||
const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null))
|
|
||||||
const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory)))
|
|
||||||
|
|
||||||
const selectProject = (worktree: string, serverKey?: string) => {
|
const selectProject = (worktree: string, serverKey?: string) => {
|
||||||
const conn = serverKey ? server.list.find((conn) => ServerConnection.key(conn) === serverKey) : projectServer()
|
const conn = serverKey ? server.list.find((conn) => ServerConnection.key(conn) === serverKey) : projectServer()
|
||||||
if (search.draftId) {
|
if (search.draftId) {
|
||||||
|
|
@ -94,32 +120,11 @@ export function createSessionComposerControls(input: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return createMemo<PromptInputControls>(() => ({
|
return createMemo<PromptProjectControls>(() => ({
|
||||||
agents: {
|
available: projects(),
|
||||||
available: sync().data.agent,
|
directory: sdk().directory,
|
||||||
options: local.agent.list().map((agent) => agent.name),
|
server: server.list.length > 1 ? ServerConnection.key(projectServer()) : undefined,
|
||||||
current: local.agent.current()?.name ?? "",
|
select: selectProject,
|
||||||
loading: agentsQuery.isLoading,
|
add: addProject,
|
||||||
visible: settings.visibility.customAgents(),
|
|
||||||
select: local.agent.set,
|
|
||||||
},
|
|
||||||
model: {
|
|
||||||
selection: local.model,
|
|
||||||
paid: providers.paid().length > 0,
|
|
||||||
loading: agentsQuery.isLoading || providersQuery.isLoading || globalProvidersQuery.isLoading,
|
|
||||||
},
|
|
||||||
projects: {
|
|
||||||
available: projects(),
|
|
||||||
directory: sdk().directory,
|
|
||||||
server: server.list.length > 1 ? ServerConnection.key(projectServer()) : undefined,
|
|
||||||
select: selectProject,
|
|
||||||
add: addProject,
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
id: input.sessionID(),
|
|
||||||
tabs: layout.tabs(input.sessionKey),
|
|
||||||
reviewPanel: view.reviewPanel,
|
|
||||||
},
|
|
||||||
newLayoutDesigns: settings.general.newLayoutDesigns(),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
|
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||||
|
import { type Accessor, createEffect, createMemo, createResource, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import type { PromptInputState } from "@/components/prompt-input"
|
||||||
|
import { useSync } from "@/context/sync"
|
||||||
|
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
||||||
|
import type { SessionComposerController } from "./session-composer-state"
|
||||||
|
|
||||||
|
export type SessionComposerFollowupDock = {
|
||||||
|
items: { id: string; text: string }[]
|
||||||
|
sending?: string
|
||||||
|
onSend: (id: string) => void
|
||||||
|
onEdit: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionComposerRevertDock = {
|
||||||
|
items: { id: string; text: string }[]
|
||||||
|
restoring?: string
|
||||||
|
disabled?: boolean
|
||||||
|
onRestore: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSessionComposerRegionController(input: {
|
||||||
|
state: SessionComposerController
|
||||||
|
sessionKey: Accessor<string>
|
||||||
|
sessionID: Accessor<string | undefined>
|
||||||
|
prompt: PromptInputState
|
||||||
|
ready: Accessor<boolean>
|
||||||
|
centered: Accessor<boolean>
|
||||||
|
todo: {
|
||||||
|
collapsed: Accessor<boolean>
|
||||||
|
onToggle: () => void
|
||||||
|
}
|
||||||
|
followup: Accessor<SessionComposerFollowupDock | undefined>
|
||||||
|
revert: Accessor<SessionComposerRevertDock | undefined>
|
||||||
|
onResponseSubmit: () => void
|
||||||
|
openParent: () => void
|
||||||
|
setPromptRef: (el: HTMLDivElement) => void
|
||||||
|
setDockRef: (el: HTMLDivElement) => void
|
||||||
|
}) {
|
||||||
|
const sync = useSync()
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
ready: input.ready() || input.state.dock(),
|
||||||
|
height: 320,
|
||||||
|
body: undefined as HTMLDivElement | undefined,
|
||||||
|
})
|
||||||
|
let timer: number | undefined
|
||||||
|
let frame: number | undefined
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
if (timer !== undefined) window.clearTimeout(timer)
|
||||||
|
if (frame !== undefined) cancelAnimationFrame(frame)
|
||||||
|
timer = undefined
|
||||||
|
frame = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
input.sessionKey()
|
||||||
|
const ready = input.ready()
|
||||||
|
const dock = input.state.dock()
|
||||||
|
|
||||||
|
clear()
|
||||||
|
if (store.ready || (!ready && !dock)) return
|
||||||
|
if (dock) {
|
||||||
|
setStore("ready", true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = requestAnimationFrame(() => {
|
||||||
|
frame = undefined
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
setStore("ready", true)
|
||||||
|
timer = undefined
|
||||||
|
}, 140)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!input.prompt.ready()) return
|
||||||
|
setSessionHandoff(input.sessionKey(), {
|
||||||
|
prompt: input.prompt
|
||||||
|
.current()
|
||||||
|
.map((part) => {
|
||||||
|
if (part.type === "file") return `[file:${part.path}]`
|
||||||
|
if (part.type === "agent") return `@${part.name}`
|
||||||
|
if (part.type === "image") return `[image:${part.filename}]`
|
||||||
|
return part.content
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
.trim(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const el = store.body
|
||||||
|
if (!el) return
|
||||||
|
const update = () => setStore("height", el.getBoundingClientRect().height)
|
||||||
|
createResizeObserver(el, update)
|
||||||
|
update()
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(clear)
|
||||||
|
|
||||||
|
const parentID = createMemo(() => {
|
||||||
|
const id = input.sessionID()
|
||||||
|
return id ? sync().session.get(id)?.parentID : undefined
|
||||||
|
})
|
||||||
|
const open = createMemo(() => store.ready && input.state.dock() && !input.state.closing())
|
||||||
|
const progress = useSpring(
|
||||||
|
() => (open() ? 1 : 0),
|
||||||
|
{ visualDuration: 0.3, bounce: 0 },
|
||||||
|
() => `${input.sessionKey()}\0${store.ready}`,
|
||||||
|
)
|
||||||
|
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
|
||||||
|
const ready = Promise.resolve()
|
||||||
|
const [promptReady] = createResource(
|
||||||
|
() => input.prompt.ready.promise ?? ready,
|
||||||
|
(promise) => promise.then(() => true),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: input.state,
|
||||||
|
centered: input.centered,
|
||||||
|
todo: input.todo,
|
||||||
|
followup: input.followup,
|
||||||
|
revert: input.revert,
|
||||||
|
onResponseSubmit: input.onResponseSubmit,
|
||||||
|
openParent: input.openParent,
|
||||||
|
setPromptRef: input.setPromptRef,
|
||||||
|
setDockRef: input.setDockRef,
|
||||||
|
parentID,
|
||||||
|
child: () => !!parentID(),
|
||||||
|
showComposer: () => !input.state.blocked() || !!parentID(),
|
||||||
|
handoffPrompt: () => getSessionHandoff(input.sessionKey())?.prompt,
|
||||||
|
promptReady: () => input.prompt.ready() || promptReady(),
|
||||||
|
dock: () => (store.ready && input.state.dock()) || value() > 0.001,
|
||||||
|
dockProgress: value,
|
||||||
|
dockHeight: () => Math.max(78, store.height),
|
||||||
|
lift: () => (input.revert()?.items.length ? 18 : 36 * value()),
|
||||||
|
setDockBodyRef: (el: HTMLDivElement) => setStore("body", el),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionComposerRegionController = ReturnType<typeof createSessionComposerRegionController>
|
||||||
|
|
@ -1,216 +1,83 @@
|
||||||
import { Show, createEffect, createMemo, createResource, onCleanup } from "solid-js"
|
import { Show, type JSX } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
|
||||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
|
||||||
import { PromptInput, type PromptInputControls, type PromptInputProps } from "@/components/prompt-input"
|
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
import { usePrompt } from "@/context/prompt"
|
|
||||||
import { useSync } from "@/context/sync"
|
|
||||||
import { getSessionHandoff, setSessionHandoff } from "@/pages/session/handoff"
|
|
||||||
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
import { SessionPermissionDock } from "@/pages/session/composer/session-permission-dock"
|
||||||
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
|
import { SessionQuestionDock } from "@/pages/session/composer/session-question-dock"
|
||||||
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
|
import { SessionFollowupDock } from "@/pages/session/composer/session-followup-dock"
|
||||||
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
import { SessionRevertDock } from "@/pages/session/composer/session-revert-dock"
|
||||||
import type { SessionComposerState } from "@/pages/session/composer/session-composer-state"
|
|
||||||
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
import { SessionTodoDock } from "@/pages/session/composer/session-todo-dock"
|
||||||
import type { FollowupDraft } from "@/components/prompt-input/submit"
|
import type { SessionComposerRegionController } from "./session-composer-region-controller"
|
||||||
import { createResizeObserver } from "@solid-primitives/resize-observer"
|
|
||||||
import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout"
|
|
||||||
|
|
||||||
export function SessionComposerRegion(props: {
|
export function SessionComposerRegion(props: {
|
||||||
state: SessionComposerState
|
controller: SessionComposerRegionController
|
||||||
sessionKey: string
|
promptInput: JSX.Element
|
||||||
sessionID?: string
|
|
||||||
controls: PromptInputControls
|
|
||||||
promptInput: Omit<PromptInputProps, "controls" | "variant">
|
|
||||||
todo: {
|
|
||||||
collapsed: boolean
|
|
||||||
onToggle: () => void
|
|
||||||
}
|
|
||||||
ready: boolean
|
|
||||||
centered: boolean
|
|
||||||
placement?: "dock" | "inline"
|
|
||||||
openParent?: () => void
|
|
||||||
onResponseSubmit: () => void
|
|
||||||
followup?: {
|
|
||||||
queue: () => boolean
|
|
||||||
items: { id: string; text: string }[]
|
|
||||||
sending?: string
|
|
||||||
edit?: { id: string; prompt: FollowupDraft["prompt"]; context: FollowupDraft["context"] }
|
|
||||||
onQueue: (draft: FollowupDraft) => void
|
|
||||||
onAbort: () => void
|
|
||||||
onSend: (id: string) => void
|
|
||||||
onEdit: (id: string) => void
|
|
||||||
onEditLoaded: () => void
|
|
||||||
}
|
|
||||||
revert?: {
|
|
||||||
items: { id: string; text: string }[]
|
|
||||||
restoring?: string
|
|
||||||
disabled?: boolean
|
|
||||||
onRestore: (id: string) => void
|
|
||||||
}
|
|
||||||
setPromptDockRef: (el: HTMLDivElement) => void
|
|
||||||
}) {
|
}) {
|
||||||
const prompt = props.promptInput.state ?? usePrompt()
|
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const sync = useSync()
|
const controller = props.controller
|
||||||
|
const rolled = () => {
|
||||||
const handoffPrompt = createMemo(() => getSessionHandoff(props.sessionKey)?.prompt)
|
const revert = controller.revert()
|
||||||
const info = createMemo(() => (props.sessionID ? sync().session.get(props.sessionID) : undefined))
|
return revert?.items.length ? revert : undefined
|
||||||
const parentID = createMemo(() => info()?.parentID)
|
|
||||||
const child = createMemo(() => !!parentID())
|
|
||||||
const showComposer = createMemo(() => !props.state.blocked() || child())
|
|
||||||
|
|
||||||
const previewPrompt = () =>
|
|
||||||
prompt
|
|
||||||
.current()
|
|
||||||
.map((part) => {
|
|
||||||
if (part.type === "file") return `[file:${part.path}]`
|
|
||||||
if (part.type === "agent") return `@${part.name}`
|
|
||||||
if (part.type === "image") return `[image:${part.filename}]`
|
|
||||||
return part.content
|
|
||||||
})
|
|
||||||
.join("")
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!prompt.ready()) return
|
|
||||||
setSessionHandoff(props.sessionKey, { prompt: previewPrompt() })
|
|
||||||
})
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
ready: props.ready || props.state.dock(),
|
|
||||||
height: 320,
|
|
||||||
body: undefined as HTMLDivElement | undefined,
|
|
||||||
})
|
|
||||||
let timer: number | undefined
|
|
||||||
let frame: number | undefined
|
|
||||||
|
|
||||||
const clear = () => {
|
|
||||||
if (timer !== undefined) {
|
|
||||||
window.clearTimeout(timer)
|
|
||||||
timer = undefined
|
|
||||||
}
|
|
||||||
if (frame !== undefined) {
|
|
||||||
cancelAnimationFrame(frame)
|
|
||||||
frame = undefined
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
props.sessionKey
|
|
||||||
const ready = props.ready
|
|
||||||
const dock = props.state.dock()
|
|
||||||
const delay = 140
|
|
||||||
|
|
||||||
clear()
|
|
||||||
if (store.ready || (!ready && !dock)) return
|
|
||||||
if (dock) {
|
|
||||||
setStore("ready", true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
frame = requestAnimationFrame(() => {
|
|
||||||
frame = undefined
|
|
||||||
timer = window.setTimeout(() => {
|
|
||||||
setStore("ready", true)
|
|
||||||
timer = undefined
|
|
||||||
}, delay)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
onCleanup(clear)
|
|
||||||
|
|
||||||
const open = createMemo(() => store.ready && props.state.dock() && !props.state.closing())
|
|
||||||
const progress = useSpring(
|
|
||||||
() => (open() ? 1 : 0),
|
|
||||||
{ visualDuration: 0.3, bounce: 0 },
|
|
||||||
() => `${props.sessionKey}\0${store.ready}`,
|
|
||||||
)
|
|
||||||
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
|
|
||||||
const dock = createMemo(() => (store.ready && props.state.dock()) || value() > 0.001)
|
|
||||||
const rolled = createMemo(() => (props.revert?.items.length ? props.revert : undefined))
|
|
||||||
const lift = createMemo(() => (rolled() ? 18 : 36 * value()))
|
|
||||||
const full = createMemo(() => Math.max(78, store.height))
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const el = store.body
|
|
||||||
if (!el) return
|
|
||||||
const update = () => setStore("height", el.getBoundingClientRect().height)
|
|
||||||
createResizeObserver(store.body, update)
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
|
|
||||||
const ready = Promise.resolve()
|
|
||||||
const [promptReadyResource] = createResource(
|
|
||||||
() => prompt.ready.promise ?? ready,
|
|
||||||
(promise) => promise.then(() => true),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={props.setPromptDockRef}
|
ref={controller.setDockRef}
|
||||||
data-component="session-prompt-dock"
|
data-component="session-prompt-dock"
|
||||||
classList={{
|
class="w-full shrink-0 flex flex-col justify-center items-center pb-3 bg-background-stronger pointer-events-none"
|
||||||
"w-full flex flex-col justify-center items-center pointer-events-none": true,
|
|
||||||
"shrink-0 pb-3 bg-background-stronger": props.placement !== "inline",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"w-full pointer-events-auto": true,
|
"w-full px-3 pointer-events-auto": true,
|
||||||
"px-3": props.placement !== "inline",
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": controller.centered(),
|
||||||
[NEW_SESSION_CONTENT_WIDTH]: props.placement === "inline",
|
|
||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={props.state.questionRequest()} keyed>
|
<Show when={controller.state.questionRequest()} keyed>
|
||||||
{(request) => (
|
{(request) => (
|
||||||
<div>
|
<div>
|
||||||
<SessionQuestionDock request={request} onSubmit={props.onResponseSubmit} />
|
<SessionQuestionDock request={request} onSubmit={controller.onResponseSubmit} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.state.permissionRequest()} keyed>
|
<Show when={controller.state.permissionRequest()} keyed>
|
||||||
{(request) => (
|
{(request) => (
|
||||||
<div>
|
<div>
|
||||||
<SessionPermissionDock
|
<SessionPermissionDock
|
||||||
request={request}
|
request={request}
|
||||||
responding={props.state.permissionResponding()}
|
responding={controller.state.permissionResponding()}
|
||||||
onDecide={(response) => {
|
onDecide={(response) => {
|
||||||
props.onResponseSubmit()
|
controller.onResponseSubmit()
|
||||||
props.state.decide(response)
|
controller.state.decide(response)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={showComposer()}>
|
<Show when={controller.showComposer()}>
|
||||||
<Show when={dock()}>
|
<Show when={controller.dock()}>
|
||||||
<div
|
<div
|
||||||
classList={{
|
classList={{
|
||||||
"overflow-hidden": true,
|
"overflow-hidden": true,
|
||||||
"pointer-events-none": value() < 0.98,
|
"pointer-events-none": controller.dockProgress() < 0.98,
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"max-height": `${full() * value()}px`,
|
"max-height": `${controller.dockHeight() * controller.dockProgress()}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div ref={(el) => setStore("body", el)}>
|
<div ref={controller.setDockBodyRef}>
|
||||||
<SessionTodoDock
|
<SessionTodoDock
|
||||||
todos={props.state.todos()}
|
todos={controller.state.todos()}
|
||||||
collapsed={props.todo.collapsed}
|
collapsed={controller.todo.collapsed()}
|
||||||
onToggle={props.todo.onToggle}
|
onToggle={controller.todo.onToggle}
|
||||||
collapseLabel={language.t("session.todo.collapse")}
|
collapseLabel={language.t("session.todo.collapse")}
|
||||||
expandLabel={language.t("session.todo.expand")}
|
expandLabel={language.t("session.todo.expand")}
|
||||||
dockProgress={value()}
|
dockProgress={controller.dockProgress()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={prompt.ready() || promptReadyResource()}
|
when={controller.promptReady()}
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<Show when={rolled()} keyed>
|
<Show when={rolled()} keyed>
|
||||||
|
|
@ -227,9 +94,9 @@ export function SessionComposerRegion(props: {
|
||||||
</Show>
|
</Show>
|
||||||
<div
|
<div
|
||||||
class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none"
|
class="w-full min-h-32 md:min-h-40 rounded-md border border-border-weak-base bg-background-base/50 px-4 py-3 text-text-weak whitespace-pre-wrap pointer-events-none"
|
||||||
style={{ "margin-top": `${-36 * value()}px` }}
|
style={{ "margin-top": `${-36 * controller.dockProgress()}px` }}
|
||||||
>
|
>
|
||||||
{handoffPrompt() || language.t("prompt.loading")}
|
{controller.handoffPrompt() || language.t("prompt.loading")}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
@ -238,7 +105,7 @@ export function SessionComposerRegion(props: {
|
||||||
{(revert) => (
|
{(revert) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
"margin-top": `${-36 * value()}px`,
|
"margin-top": `${-36 * controller.dockProgress()}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SessionRevertDock
|
<SessionRevertDock
|
||||||
|
|
@ -255,44 +122,35 @@ export function SessionComposerRegion(props: {
|
||||||
"relative z-30": true,
|
"relative z-30": true,
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
"margin-top": `${-lift()}px`,
|
"margin-top": `${-controller.lift()}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show when={props.followup?.items.length}>
|
<Show when={controller.followup()?.items.length}>
|
||||||
<SessionFollowupDock
|
<SessionFollowupDock
|
||||||
items={props.followup!.items}
|
items={controller.followup()!.items}
|
||||||
sending={props.followup!.sending}
|
sending={controller.followup()!.sending}
|
||||||
onSend={props.followup!.onSend}
|
onSend={controller.followup()!.onSend}
|
||||||
onEdit={props.followup!.onEdit}
|
onEdit={controller.followup()!.onEdit}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={child()}
|
when={controller.child()}
|
||||||
fallback={
|
fallback={
|
||||||
<Show when={!props.state.blocked()}>
|
<Show when={!controller.state.blocked()}>
|
||||||
<PromptInput
|
{props.promptInput}
|
||||||
{...props.promptInput}
|
|
||||||
controls={props.controls}
|
|
||||||
variant={props.placement === "inline" ? "new-session" : undefined}
|
|
||||||
edit={props.followup?.edit}
|
|
||||||
onEditLoaded={props.followup?.onEditLoaded}
|
|
||||||
shouldQueue={props.followup?.queue}
|
|
||||||
onQueue={props.followup?.onQueue}
|
|
||||||
onAbort={props.followup?.onAbort}
|
|
||||||
/>
|
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={props.promptInput.ref}
|
ref={controller.setPromptRef}
|
||||||
class="w-full rounded-[12px] border border-border-weak-base bg-background-base p-3 text-16-regular text-text-weak"
|
class="w-full rounded-[12px] border border-border-weak-base bg-background-base p-3 text-16-regular text-text-weak"
|
||||||
>
|
>
|
||||||
<span>{language.t("session.child.promptDisabled")} </span>
|
<span>{language.t("session.child.promptDisabled")} </span>
|
||||||
<Show when={parentID() && props.openParent}>
|
<Show when={controller.parentID()}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="text-text-base transition-colors hover:text-text-strong"
|
class="text-text-base transition-colors hover:text-text-strong"
|
||||||
onClick={props.openParent}
|
onClick={controller.openParent}
|
||||||
>
|
>
|
||||||
{language.t("session.child.backToParent")}
|
{language.t("session.child.backToParent")}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const todoDockAtBoundary = (state: ReturnType<typeof todoState>) => state
|
||||||
|
|
||||||
const idle = { type: "idle" as const }
|
const idle = { type: "idle" as const }
|
||||||
|
|
||||||
export function createSessionComposerState(options?: { closeMs?: number | (() => number) }) {
|
export function createSessionComposerController(options?: { closeMs?: number | (() => number) }) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const sdk = useSDK()
|
const sdk = useSDK()
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
|
|
@ -201,4 +201,4 @@ export function createSessionComposerState(options?: { closeMs?: number | (() =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionComposerState = ReturnType<typeof createSessionComposerState>
|
export type SessionComposerController = ReturnType<typeof createSessionComposerController>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ import { createEffect, createMemo, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import type { Todo } from "@opencode-ai/sdk/v2"
|
import type { Todo } from "@opencode-ai/sdk/v2"
|
||||||
import { useServerSync } from "@/context/global-sync"
|
import { useServerSync } from "@/context/global-sync"
|
||||||
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
|
import { usePrompt } from "@/context/prompt"
|
||||||
|
import {
|
||||||
|
SessionComposerRegion,
|
||||||
|
createSessionComposerController,
|
||||||
|
createSessionComposerRegionController,
|
||||||
|
} from "@/pages/session/composer"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "UI/Todo Panel Motion",
|
title: "UI/Todo Panel Motion",
|
||||||
|
|
@ -60,7 +66,6 @@ const controls = {
|
||||||
paid: true,
|
paid: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
},
|
},
|
||||||
projects: { available: [], directory: "/tmp/story", select: () => {}, add: () => {} },
|
|
||||||
session: {
|
session: {
|
||||||
id: "story-session",
|
id: "story-session",
|
||||||
tabs: { active: () => undefined, all: () => [], open: () => {}, setActive: () => {} },
|
tabs: { active: () => undefined, all: () => [], open: () => {}, setActive: () => {} },
|
||||||
|
|
@ -149,6 +154,7 @@ const css = `
|
||||||
export const Playground = {
|
export const Playground = {
|
||||||
render: () => {
|
render: () => {
|
||||||
const global = useServerSync()
|
const global = useServerSync()
|
||||||
|
const prompt = usePrompt()
|
||||||
const [cfg, setCfg] = createStore({
|
const [cfg, setCfg] = createStore({
|
||||||
open: true,
|
open: true,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
|
|
@ -188,7 +194,7 @@ export const Playground = {
|
||||||
const countMask = () => cfg.countMask
|
const countMask = () => cfg.countMask
|
||||||
const countMaskHeight = () => cfg.countMaskHeight
|
const countMaskHeight = () => cfg.countMaskHeight
|
||||||
const countWidthDuration = () => cfg.countWidthDuration
|
const countWidthDuration = () => cfg.countWidthDuration
|
||||||
const state = createSessionComposerState({ closeMs: () => Math.round(dockCloseDuration() * 1000) })
|
const state = createSessionComposerController({ closeMs: () => Math.round(dockCloseDuration() * 1000) })
|
||||||
let frame
|
let frame
|
||||||
let scrollRef
|
let scrollRef
|
||||||
|
|
||||||
|
|
@ -217,7 +223,6 @@ export const Playground = {
|
||||||
|
|
||||||
const collapsed = () => cfg.collapsed
|
const collapsed = () => cfg.collapsed
|
||||||
const setCollapsed = (value: boolean) => setCfg("collapsed", value)
|
const setCollapsed = (value: boolean) => setCfg("collapsed", value)
|
||||||
|
|
||||||
const openDock = () => {
|
const openDock = () => {
|
||||||
clear()
|
clear()
|
||||||
setCfg("open", true)
|
setCfg("open", true)
|
||||||
|
|
@ -281,36 +286,32 @@ export const Playground = {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<SessionComposerRegion
|
<SessionComposerRegion
|
||||||
state={state}
|
controller={
|
||||||
sessionKey="story-session"
|
createSessionComposerRegionController({
|
||||||
sessionID="story-session"
|
state,
|
||||||
controls={controls}
|
sessionKey: () => "story-session",
|
||||||
promptInput={{
|
sessionID: () => "story-session",
|
||||||
submission: { abort: () => {}, handleSubmit: (event) => event.preventDefault() },
|
prompt,
|
||||||
ref: () => {},
|
ready: () => true,
|
||||||
newSessionWorktree: "",
|
centered: () => false,
|
||||||
onNewSessionWorktreeReset: () => {},
|
todo: { collapsed, onToggle: () => setCollapsed(!collapsed()) },
|
||||||
}}
|
followup: () => undefined,
|
||||||
todo={{ collapsed: collapsed(), onToggle: () => setCollapsed(!collapsed()) }}
|
revert: () => undefined,
|
||||||
ready
|
onResponseSubmit: pin,
|
||||||
centered={false}
|
openParent: () => {},
|
||||||
onResponseSubmit={pin}
|
setPromptRef: () => {},
|
||||||
setPromptDockRef={() => {}}
|
setDockRef: () => {},
|
||||||
dockOpenVisualDuration={dockOpenDuration()}
|
})
|
||||||
dockOpenBounce={dockOpenBounce()}
|
}
|
||||||
dockCloseVisualDuration={dockCloseDuration()}
|
promptInput={
|
||||||
dockCloseBounce={dockCloseBounce()}
|
<PromptInput
|
||||||
drawerExpandVisualDuration={drawerExpandDuration()}
|
controls={controls}
|
||||||
drawerExpandBounce={drawerExpandBounce()}
|
submission={{ abort: () => {}, handleSubmit: (event) => event.preventDefault() }}
|
||||||
drawerCollapseVisualDuration={drawerCollapseDuration()}
|
ref={() => {}}
|
||||||
drawerCollapseBounce={drawerCollapseBounce()}
|
newSessionWorktree=""
|
||||||
subtitleDuration={subtitleDuration()}
|
onNewSessionWorktreeReset={() => {}}
|
||||||
subtitleTravel={subtitleAuto() ? undefined : subtitleTravel()}
|
/>
|
||||||
subtitleEdge={subtitleAuto() ? undefined : subtitleEdge()}
|
}
|
||||||
countDuration={countDuration()}
|
|
||||||
countMask={countMask()}
|
|
||||||
countMaskHeight={countMaskHeight()}
|
|
||||||
countWidthDuration={countWidthDuration()}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue