feat(app): session timeline/turn rework (#13196)
Co-authored-by: David Hill <iamdavidhill@gmail.com>
This commit is contained in:
@@ -4,10 +4,10 @@ import { Icon } from "@opencode-ai/ui/icon"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
|
||||
import { InlineInput } from "@opencode-ai/ui/inline-input"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { SessionTurn } from "@opencode-ai/ui/session-turn"
|
||||
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture"
|
||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||
|
||||
const boundaryTarget = (root: HTMLElement, target: EventTarget | null) => {
|
||||
const current = target instanceof Element ? target : undefined
|
||||
@@ -88,8 +88,6 @@ export function MessageTimeline(props: {
|
||||
onUnregisterMessage: (id: string) => void
|
||||
onFirstTurnMount?: () => void
|
||||
lastUserMessageID?: string
|
||||
expanded: Record<string, boolean>
|
||||
onToggleExpanded: (id: string) => void
|
||||
}) {
|
||||
let touchGesture: number | undefined
|
||||
|
||||
@@ -100,7 +98,7 @@ export function MessageTimeline(props: {
|
||||
>
|
||||
<div class="relative w-full h-full min-w-0">
|
||||
<div
|
||||
class="absolute left-1/2 -translate-x-1/2 bottom-[calc(var(--prompt-height,8rem)+32px)] z-[60] pointer-events-none transition-all duration-200 ease-out"
|
||||
class="absolute left-1/2 -translate-x-1/2 bottom-6 z-[60] pointer-events-none transition-all duration-200 ease-out"
|
||||
classList={{
|
||||
"opacity-100 translate-y-0 scale-100": props.scroll.overflow && !props.scroll.bottom,
|
||||
"opacity-0 translate-y-2 scale-95 pointer-events-none": !props.scroll.overflow || props.scroll.bottom,
|
||||
@@ -164,14 +162,15 @@ export function MessageTimeline(props: {
|
||||
<Show when={props.showHeader}>
|
||||
<div
|
||||
classList={{
|
||||
"sticky top-0 z-30 bg-background-stronger": true,
|
||||
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
||||
"w-full": true,
|
||||
"px-4 md:px-6": true,
|
||||
"pb-4": true,
|
||||
"pl-2 pr-4 md:pl-4 md:pr-6": true,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
}}
|
||||
>
|
||||
<div class="h-10 w-full flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1 min-w-0 flex-1">
|
||||
<div class="h-12 w-full flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1 min-w-0 flex-1 pr-3">
|
||||
<Show when={props.parentID}>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
@@ -185,7 +184,10 @@ export function MessageTimeline(props: {
|
||||
<Show
|
||||
when={props.titleState.editing}
|
||||
fallback={
|
||||
<h1 class="text-16-medium text-text-strong truncate min-w-0" onDblClick={props.openTitleEditor}>
|
||||
<h1
|
||||
class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2"
|
||||
onDblClick={props.openTitleEditor}
|
||||
>
|
||||
{props.title}
|
||||
</h1>
|
||||
}
|
||||
@@ -194,7 +196,8 @@ export function MessageTimeline(props: {
|
||||
ref={props.titleRef}
|
||||
value={props.titleState.draft}
|
||||
disabled={props.titleState.saving}
|
||||
class="text-16-medium text-text-strong grow-1 min-w-0"
|
||||
class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]"
|
||||
style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }}
|
||||
onInput={(event) => props.onTitleDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation()
|
||||
@@ -215,19 +218,24 @@ export function MessageTimeline(props: {
|
||||
</div>
|
||||
<Show when={props.sessionID}>
|
||||
{(id) => (
|
||||
<div class="shrink-0 flex items-center">
|
||||
<DropdownMenu open={props.titleState.menuOpen} onOpenChange={props.onTitleMenuOpen}>
|
||||
<Tooltip value={props.t("common.moreOptions")} placement="top">
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||
aria-label={props.t("common.moreOptions")}
|
||||
/>
|
||||
</Tooltip>
|
||||
<div class="shrink-0 flex items-center gap-3">
|
||||
<SessionContextUsage placement="bottom" />
|
||||
<DropdownMenu
|
||||
gutter={4}
|
||||
placement="bottom-end"
|
||||
open={props.titleState.menuOpen}
|
||||
onOpenChange={props.onTitleMenuOpen}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="size-6 rounded-md data-[expanded]:bg-surface-base-active"
|
||||
aria-label={props.t("common.moreOptions")}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
style={{ "min-width": "104px" }}
|
||||
onCloseAutoFocus={(event) => {
|
||||
if (!props.titleState.pendingRename) return
|
||||
event.preventDefault()
|
||||
@@ -263,7 +271,7 @@ export function MessageTimeline(props: {
|
||||
<div
|
||||
ref={props.setContentRef}
|
||||
role="log"
|
||||
class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
|
||||
class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]"
|
||||
classList={{
|
||||
"w-full": true,
|
||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||
@@ -316,8 +324,6 @@ export function MessageTimeline(props: {
|
||||
sessionID={props.sessionID}
|
||||
messageID={message.id}
|
||||
lastUserMessageID={props.lastUserMessageID}
|
||||
stepsExpanded={props.expanded[message.id] ?? false}
|
||||
onStepsExpandedToggle={() => props.onToggleExpanded(message.id)}
|
||||
classes={{
|
||||
root: "min-w-0 w-full relative",
|
||||
content: "flex flex-col justify-between !overflow-visible",
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { For, Show } from "solid-js"
|
||||
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
||||
import type { QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { BasicTool } from "@opencode-ai/ui/basic-tool"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { QuestionDock } from "@/components/question-dock"
|
||||
import { questionSubtitle } from "@/pages/session/session-prompt-helpers"
|
||||
import { SessionTodoDock } from "@/components/session-todo-dock"
|
||||
|
||||
export function SessionPromptDock(props: {
|
||||
centered: boolean
|
||||
questionRequest: () => QuestionRequest | undefined
|
||||
permissionRequest: () => { patterns: string[]; permission: string } | undefined
|
||||
blocked: boolean
|
||||
todos: Todo[]
|
||||
promptReady: boolean
|
||||
handoffPrompt?: string
|
||||
t: (key: string, vars?: Record<string, string | number | boolean>) => string
|
||||
@@ -22,10 +23,88 @@ export function SessionPromptDock(props: {
|
||||
onSubmit: () => void
|
||||
setPromptDockRef: (el: HTMLDivElement) => void
|
||||
}) {
|
||||
const done = createMemo(
|
||||
() =>
|
||||
props.todos.length > 0 && props.todos.every((todo) => todo.status === "completed" || todo.status === "cancelled"),
|
||||
)
|
||||
|
||||
const [dock, setDock] = createSignal(props.todos.length > 0)
|
||||
const [closing, setClosing] = createSignal(false)
|
||||
const [opening, setOpening] = createSignal(false)
|
||||
let timer: number | undefined
|
||||
let raf: number | undefined
|
||||
|
||||
const scheduleClose = () => {
|
||||
if (timer) window.clearTimeout(timer)
|
||||
timer = window.setTimeout(() => {
|
||||
setDock(false)
|
||||
setClosing(false)
|
||||
timer = undefined
|
||||
}, 400)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => [props.todos.length, done()] as const,
|
||||
([count, complete], prev) => {
|
||||
if (raf) cancelAnimationFrame(raf)
|
||||
raf = undefined
|
||||
|
||||
if (count === 0) {
|
||||
if (timer) window.clearTimeout(timer)
|
||||
timer = undefined
|
||||
setDock(false)
|
||||
setClosing(false)
|
||||
setOpening(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!complete) {
|
||||
if (timer) window.clearTimeout(timer)
|
||||
timer = undefined
|
||||
const wasHidden = !dock() || closing()
|
||||
setDock(true)
|
||||
setClosing(false)
|
||||
if (wasHidden) {
|
||||
setOpening(true)
|
||||
raf = requestAnimationFrame(() => {
|
||||
setOpening(false)
|
||||
raf = undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
setOpening(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (prev && prev[1]) {
|
||||
if (closing() && !timer) scheduleClose()
|
||||
return
|
||||
}
|
||||
|
||||
setDock(true)
|
||||
setOpening(false)
|
||||
setClosing(true)
|
||||
scheduleClose()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
onCleanup(() => {
|
||||
if (!timer) return
|
||||
window.clearTimeout(timer)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (!raf) return
|
||||
cancelAnimationFrame(raf)
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={props.setPromptDockRef}
|
||||
class="absolute inset-x-0 bottom-0 pt-12 pb-4 flex flex-col justify-center items-center z-50 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none"
|
||||
data-component="session-prompt-dock"
|
||||
class="shrink-0 w-full pb-4 flex flex-col justify-center items-center bg-background-stronger pointer-events-none"
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
@@ -35,18 +114,8 @@ export function SessionPromptDock(props: {
|
||||
>
|
||||
<Show when={props.questionRequest()} keyed>
|
||||
{(req) => {
|
||||
const subtitle = questionSubtitle(req.questions.length, (key) => props.t(key))
|
||||
return (
|
||||
<div data-component="tool-part-wrapper" data-question="true" class="mb-3">
|
||||
<BasicTool
|
||||
icon="bubble-5"
|
||||
locked
|
||||
defaultOpen
|
||||
trigger={{
|
||||
title: props.t("ui.tool.questions"),
|
||||
subtitle,
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<QuestionDock request={req} />
|
||||
</div>
|
||||
)
|
||||
@@ -122,12 +191,39 @@ export function SessionPromptDock(props: {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PromptInput
|
||||
ref={props.inputRef}
|
||||
newSessionWorktree={props.newSessionWorktree}
|
||||
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
||||
onSubmit={props.onSubmit}
|
||||
/>
|
||||
<Show when={dock()}>
|
||||
<div
|
||||
classList={{
|
||||
"transition-[max-height,opacity,transform] duration-[400ms] ease-out overflow-hidden": true,
|
||||
"max-h-[320px]": !closing(),
|
||||
"max-h-0 pointer-events-none": closing(),
|
||||
"opacity-0 translate-y-9": closing() || opening(),
|
||||
"opacity-100 translate-y-0": !closing() && !opening(),
|
||||
}}
|
||||
>
|
||||
<SessionTodoDock
|
||||
todos={props.todos}
|
||||
title={props.t("session.todo.title")}
|
||||
collapseLabel={props.t("session.todo.collapse")}
|
||||
expandLabel={props.t("session.todo.expand")}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
classList={{
|
||||
"relative z-10": true,
|
||||
"transition-[margin] duration-[400ms] ease-out": true,
|
||||
"-mt-9": dock() && !closing(),
|
||||
"mt-0": !dock() || closing(),
|
||||
}}
|
||||
>
|
||||
<PromptInput
|
||||
ref={props.inputRef}
|
||||
newSessionWorktree={props.newSessionWorktree}
|
||||
onNewSessionWorktreeReset={props.onNewSessionWorktreeReset}
|
||||
onSubmit={props.onSubmit}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -22,11 +22,29 @@ import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { canAddSelectionContext } from "@/pages/session/session-command-helpers"
|
||||
|
||||
export type SessionCommandContext = {
|
||||
activeMessage: () => UserMessage | undefined
|
||||
command: ReturnType<typeof useCommand>
|
||||
dialog: ReturnType<typeof useDialog>
|
||||
file: ReturnType<typeof useFile>
|
||||
language: ReturnType<typeof useLanguage>
|
||||
local: ReturnType<typeof useLocal>
|
||||
permission: ReturnType<typeof usePermission>
|
||||
prompt: ReturnType<typeof usePrompt>
|
||||
sdk: ReturnType<typeof useSDK>
|
||||
sync: ReturnType<typeof useSync>
|
||||
terminal: ReturnType<typeof useTerminal>
|
||||
layout: ReturnType<typeof useLayout>
|
||||
params: ReturnType<typeof useParams>
|
||||
navigate: ReturnType<typeof useNavigate>
|
||||
tabs: () => ReturnType<ReturnType<typeof useLayout>["tabs"]>
|
||||
view: () => ReturnType<ReturnType<typeof useLayout>["view"]>
|
||||
info: () => { revert?: { messageID?: string }; share?: { url?: string } } | undefined
|
||||
status: () => { type: string }
|
||||
userMessages: () => UserMessage[]
|
||||
visibleUserMessages: () => UserMessage[]
|
||||
showAllFiles: () => void
|
||||
navigateMessageByOffset: (offset: number) => void
|
||||
setExpanded: (id: string, fn: (open: boolean | undefined) => boolean) => void
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
addSelectionToContext: (path: string, selection: FileSelection) => void
|
||||
focusInput: () => void
|
||||
}
|
||||
|
||||
@@ -37,88 +55,45 @@ const withCategory = (category: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
const command = useCommand()
|
||||
const dialog = useDialog()
|
||||
const file = useFile()
|
||||
const language = useLanguage()
|
||||
const local = useLocal()
|
||||
const permission = usePermission()
|
||||
const prompt = usePrompt()
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const terminal = useTerminal()
|
||||
const layout = useLayout()
|
||||
const params = useParams()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
|
||||
const tabs = createMemo(() => layout.tabs(sessionKey))
|
||||
const view = createMemo(() => layout.view(sessionKey))
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const idle = { type: "idle" as const }
|
||||
const status = createMemo(() => sync.data.session_status[params.id ?? ""] ?? idle)
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[])
|
||||
const visibleUserMessages = createMemo(() => {
|
||||
const revert = info()?.revert?.messageID
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
})
|
||||
|
||||
const selectionPreview = (path: string, selection: FileSelection) => {
|
||||
const content = file.get(path)?.content?.content
|
||||
if (!content) return undefined
|
||||
const start = Math.max(1, Math.min(selection.startLine, selection.endLine))
|
||||
const end = Math.max(selection.startLine, selection.endLine)
|
||||
const lines = content.split("\n").slice(start - 1, end)
|
||||
if (lines.length === 0) return undefined
|
||||
return lines.slice(0, 2).join("\n")
|
||||
}
|
||||
|
||||
const addSelectionToContext = (path: string, selection: FileSelection) => {
|
||||
const preview = selectionPreview(path, selection)
|
||||
prompt.context.add({ type: "file", path, selection, preview })
|
||||
}
|
||||
|
||||
const sessionCommand = withCategory(language.t("command.category.session"))
|
||||
const fileCommand = withCategory(language.t("command.category.file"))
|
||||
const contextCommand = withCategory(language.t("command.category.context"))
|
||||
const viewCommand = withCategory(language.t("command.category.view"))
|
||||
const terminalCommand = withCategory(language.t("command.category.terminal"))
|
||||
const modelCommand = withCategory(language.t("command.category.model"))
|
||||
const mcpCommand = withCategory(language.t("command.category.mcp"))
|
||||
const agentCommand = withCategory(language.t("command.category.agent"))
|
||||
const permissionsCommand = withCategory(language.t("command.category.permissions"))
|
||||
export const useSessionCommands = (input: SessionCommandContext) => {
|
||||
const sessionCommand = withCategory(input.language.t("command.category.session"))
|
||||
const fileCommand = withCategory(input.language.t("command.category.file"))
|
||||
const contextCommand = withCategory(input.language.t("command.category.context"))
|
||||
const viewCommand = withCategory(input.language.t("command.category.view"))
|
||||
const terminalCommand = withCategory(input.language.t("command.category.terminal"))
|
||||
const modelCommand = withCategory(input.language.t("command.category.model"))
|
||||
const mcpCommand = withCategory(input.language.t("command.category.mcp"))
|
||||
const agentCommand = withCategory(input.language.t("command.category.agent"))
|
||||
const permissionsCommand = withCategory(input.language.t("command.category.permissions"))
|
||||
|
||||
const sessionCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "session.new",
|
||||
title: language.t("command.session.new"),
|
||||
title: input.language.t("command.session.new"),
|
||||
keybind: "mod+shift+s",
|
||||
slash: "new",
|
||||
onSelect: () => navigate(`/${params.dir}/session`),
|
||||
onSelect: () => input.navigate(`/${input.params.dir}/session`),
|
||||
}),
|
||||
])
|
||||
|
||||
const fileCommands = createMemo(() => [
|
||||
fileCommand({
|
||||
id: "file.open",
|
||||
title: language.t("command.file.open"),
|
||||
description: language.t("palette.search.placeholder"),
|
||||
title: input.language.t("command.file.open"),
|
||||
description: input.language.t("palette.search.placeholder"),
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={args.showAllFiles} />),
|
||||
onSelect: () => input.dialog.show(() => <DialogSelectFile onOpenFile={input.showAllFiles} />),
|
||||
}),
|
||||
fileCommand({
|
||||
id: "tab.close",
|
||||
title: language.t("command.tab.close"),
|
||||
title: input.language.t("command.tab.close"),
|
||||
keybind: "mod+w",
|
||||
disabled: !tabs().active(),
|
||||
disabled: !input.tabs().active(),
|
||||
onSelect: () => {
|
||||
const active = tabs().active()
|
||||
const active = input.tabs().active()
|
||||
if (!active) return
|
||||
tabs().close(active)
|
||||
input.tabs().close(active)
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -126,30 +101,30 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
const contextCommands = createMemo(() => [
|
||||
contextCommand({
|
||||
id: "context.addSelection",
|
||||
title: language.t("command.context.addSelection"),
|
||||
description: language.t("command.context.addSelection.description"),
|
||||
title: input.language.t("command.context.addSelection"),
|
||||
description: input.language.t("command.context.addSelection.description"),
|
||||
keybind: "mod+shift+l",
|
||||
disabled: !canAddSelectionContext({
|
||||
active: tabs().active(),
|
||||
pathFromTab: file.pathFromTab,
|
||||
selectedLines: file.selectedLines,
|
||||
active: input.tabs().active(),
|
||||
pathFromTab: input.file.pathFromTab,
|
||||
selectedLines: input.file.selectedLines,
|
||||
}),
|
||||
onSelect: () => {
|
||||
const active = tabs().active()
|
||||
const active = input.tabs().active()
|
||||
if (!active) return
|
||||
const path = file.pathFromTab(active)
|
||||
const path = input.file.pathFromTab(active)
|
||||
if (!path) return
|
||||
|
||||
const range = file.selectedLines(path) as SelectedLineRange | null | undefined
|
||||
const range = input.file.selectedLines(path) as SelectedLineRange | null | undefined
|
||||
if (!range) {
|
||||
showToast({
|
||||
title: language.t("toast.context.noLineSelection.title"),
|
||||
description: language.t("toast.context.noLineSelection.description"),
|
||||
title: input.language.t("toast.context.noLineSelection.title"),
|
||||
description: input.language.t("toast.context.noLineSelection.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addSelectionToContext(path, selectionFromLines(range))
|
||||
input.addSelectionToContext(path, selectionFromLines(range))
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -157,50 +132,37 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
const viewCommands = createMemo(() => [
|
||||
viewCommand({
|
||||
id: "terminal.toggle",
|
||||
title: language.t("command.terminal.toggle"),
|
||||
title: input.language.t("command.terminal.toggle"),
|
||||
keybind: "ctrl+`",
|
||||
slash: "terminal",
|
||||
onSelect: () => view().terminal.toggle(),
|
||||
onSelect: () => input.view().terminal.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "review.toggle",
|
||||
title: language.t("command.review.toggle"),
|
||||
title: input.language.t("command.review.toggle"),
|
||||
keybind: "mod+shift+r",
|
||||
onSelect: () => view().reviewPanel.toggle(),
|
||||
onSelect: () => input.view().reviewPanel.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "fileTree.toggle",
|
||||
title: language.t("command.fileTree.toggle"),
|
||||
title: input.language.t("command.fileTree.toggle"),
|
||||
keybind: "mod+\\",
|
||||
onSelect: () => layout.fileTree.toggle(),
|
||||
onSelect: () => input.layout.fileTree.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "input.focus",
|
||||
title: language.t("command.input.focus"),
|
||||
title: input.language.t("command.input.focus"),
|
||||
keybind: "ctrl+l",
|
||||
onSelect: () => args.focusInput(),
|
||||
onSelect: () => input.focusInput(),
|
||||
}),
|
||||
terminalCommand({
|
||||
id: "terminal.new",
|
||||
title: language.t("command.terminal.new"),
|
||||
description: language.t("command.terminal.new.description"),
|
||||
title: input.language.t("command.terminal.new"),
|
||||
description: input.language.t("command.terminal.new.description"),
|
||||
keybind: "ctrl+alt+t",
|
||||
onSelect: () => {
|
||||
if (terminal.all().length > 0) terminal.new()
|
||||
view().terminal.open()
|
||||
},
|
||||
}),
|
||||
viewCommand({
|
||||
id: "steps.toggle",
|
||||
title: language.t("command.steps.toggle"),
|
||||
description: language.t("command.steps.toggle.description"),
|
||||
keybind: "mod+e",
|
||||
slash: "steps",
|
||||
disabled: !params.id,
|
||||
onSelect: () => {
|
||||
const msg = args.activeMessage()
|
||||
if (!msg) return
|
||||
args.setExpanded(msg.id, (open: boolean | undefined) => !open)
|
||||
if (input.terminal.all().length > 0) input.terminal.new()
|
||||
input.view().terminal.open()
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -208,61 +170,61 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
const messageCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "message.previous",
|
||||
title: language.t("command.message.previous"),
|
||||
description: language.t("command.message.previous.description"),
|
||||
title: input.language.t("command.message.previous"),
|
||||
description: input.language.t("command.message.previous.description"),
|
||||
keybind: "mod+arrowup",
|
||||
disabled: !params.id,
|
||||
onSelect: () => args.navigateMessageByOffset(-1),
|
||||
disabled: !input.params.id,
|
||||
onSelect: () => input.navigateMessageByOffset(-1),
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "message.next",
|
||||
title: language.t("command.message.next"),
|
||||
description: language.t("command.message.next.description"),
|
||||
title: input.language.t("command.message.next"),
|
||||
description: input.language.t("command.message.next.description"),
|
||||
keybind: "mod+arrowdown",
|
||||
disabled: !params.id,
|
||||
onSelect: () => args.navigateMessageByOffset(1),
|
||||
disabled: !input.params.id,
|
||||
onSelect: () => input.navigateMessageByOffset(1),
|
||||
}),
|
||||
])
|
||||
|
||||
const agentCommands = createMemo(() => [
|
||||
modelCommand({
|
||||
id: "model.choose",
|
||||
title: language.t("command.model.choose"),
|
||||
description: language.t("command.model.choose.description"),
|
||||
title: input.language.t("command.model.choose"),
|
||||
description: input.language.t("command.model.choose.description"),
|
||||
keybind: "mod+'",
|
||||
slash: "model",
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||
onSelect: () => input.dialog.show(() => <DialogSelectModel />),
|
||||
}),
|
||||
mcpCommand({
|
||||
id: "mcp.toggle",
|
||||
title: language.t("command.mcp.toggle"),
|
||||
description: language.t("command.mcp.toggle.description"),
|
||||
title: input.language.t("command.mcp.toggle"),
|
||||
description: input.language.t("command.mcp.toggle.description"),
|
||||
keybind: "mod+;",
|
||||
slash: "mcp",
|
||||
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
||||
onSelect: () => input.dialog.show(() => <DialogSelectMcp />),
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle",
|
||||
title: language.t("command.agent.cycle"),
|
||||
description: language.t("command.agent.cycle.description"),
|
||||
title: input.language.t("command.agent.cycle"),
|
||||
description: input.language.t("command.agent.cycle.description"),
|
||||
keybind: "mod+.",
|
||||
slash: "agent",
|
||||
onSelect: () => local.agent.move(1),
|
||||
onSelect: () => input.local.agent.move(1),
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle.reverse",
|
||||
title: language.t("command.agent.cycle.reverse"),
|
||||
description: language.t("command.agent.cycle.reverse.description"),
|
||||
title: input.language.t("command.agent.cycle.reverse"),
|
||||
description: input.language.t("command.agent.cycle.reverse.description"),
|
||||
keybind: "shift+mod+.",
|
||||
onSelect: () => local.agent.move(-1),
|
||||
onSelect: () => input.local.agent.move(-1),
|
||||
}),
|
||||
modelCommand({
|
||||
id: "model.variant.cycle",
|
||||
title: language.t("command.model.variant.cycle"),
|
||||
description: language.t("command.model.variant.cycle.description"),
|
||||
title: input.language.t("command.model.variant.cycle"),
|
||||
description: input.language.t("command.model.variant.cycle.description"),
|
||||
keybind: "shift+mod+d",
|
||||
onSelect: () => {
|
||||
local.model.variant.cycle()
|
||||
input.local.model.variant.cycle()
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -271,22 +233,22 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
permissionsCommand({
|
||||
id: "permissions.autoaccept",
|
||||
title:
|
||||
params.id && permission.isAutoAccepting(params.id, sdk.directory)
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable"),
|
||||
input.params.id && input.permission.isAutoAccepting(input.params.id, input.sdk.directory)
|
||||
? input.language.t("command.permissions.autoaccept.disable")
|
||||
: input.language.t("command.permissions.autoaccept.enable"),
|
||||
keybind: "mod+shift+a",
|
||||
disabled: !params.id || !permission.permissionsEnabled(),
|
||||
disabled: !input.params.id || !input.permission.permissionsEnabled(),
|
||||
onSelect: () => {
|
||||
const sessionID = params.id
|
||||
const sessionID = input.params.id
|
||||
if (!sessionID) return
|
||||
permission.toggleAutoAccept(sessionID, sdk.directory)
|
||||
input.permission.toggleAutoAccept(sessionID, input.sdk.directory)
|
||||
showToast({
|
||||
title: permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
? language.t("toast.permissions.autoaccept.on.title")
|
||||
: language.t("toast.permissions.autoaccept.off.title"),
|
||||
description: permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
? language.t("toast.permissions.autoaccept.on.description")
|
||||
: language.t("toast.permissions.autoaccept.off.description"),
|
||||
title: input.permission.isAutoAccepting(sessionID, input.sdk.directory)
|
||||
? input.language.t("toast.permissions.autoaccept.on.title")
|
||||
: input.language.t("toast.permissions.autoaccept.off.title"),
|
||||
description: input.permission.isAutoAccepting(sessionID, input.sdk.directory)
|
||||
? input.language.t("toast.permissions.autoaccept.on.description")
|
||||
: input.language.t("toast.permissions.autoaccept.off.description"),
|
||||
})
|
||||
},
|
||||
}),
|
||||
@@ -295,71 +257,71 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
const sessionActionCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "session.undo",
|
||||
title: language.t("command.session.undo"),
|
||||
description: language.t("command.session.undo.description"),
|
||||
title: input.language.t("command.session.undo"),
|
||||
description: input.language.t("command.session.undo.description"),
|
||||
slash: "undo",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
const sessionID = input.params.id
|
||||
if (!sessionID) return
|
||||
if (status()?.type !== "idle") {
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
if (input.status()?.type !== "idle") {
|
||||
await input.sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
}
|
||||
const revert = info()?.revert?.messageID
|
||||
const message = findLast(userMessages(), (x) => !revert || x.id < revert)
|
||||
const revert = input.info()?.revert?.messageID
|
||||
const message = findLast(input.userMessages(), (x) => !revert || x.id < revert)
|
||||
if (!message) return
|
||||
await sdk.client.session.revert({ sessionID, messageID: message.id })
|
||||
const parts = sync.data.part[message.id]
|
||||
await input.sdk.client.session.revert({ sessionID, messageID: message.id })
|
||||
const parts = input.sync.data.part[message.id]
|
||||
if (parts) {
|
||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||
prompt.set(restored)
|
||||
const restored = extractPromptFromParts(parts, { directory: input.sdk.directory })
|
||||
input.prompt.set(restored)
|
||||
}
|
||||
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
||||
args.setActiveMessage(priorMessage)
|
||||
const priorMessage = findLast(input.userMessages(), (x) => x.id < message.id)
|
||||
input.setActiveMessage(priorMessage)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.redo",
|
||||
title: language.t("command.session.redo"),
|
||||
description: language.t("command.session.redo.description"),
|
||||
title: input.language.t("command.session.redo"),
|
||||
description: input.language.t("command.session.redo.description"),
|
||||
slash: "redo",
|
||||
disabled: !params.id || !info()?.revert?.messageID,
|
||||
disabled: !input.params.id || !input.info()?.revert?.messageID,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
const sessionID = input.params.id
|
||||
if (!sessionID) return
|
||||
const revertMessageID = info()?.revert?.messageID
|
||||
const revertMessageID = input.info()?.revert?.messageID
|
||||
if (!revertMessageID) return
|
||||
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
|
||||
const nextMessage = input.userMessages().find((x) => x.id > revertMessageID)
|
||||
if (!nextMessage) {
|
||||
await sdk.client.session.unrevert({ sessionID })
|
||||
prompt.reset()
|
||||
const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID)
|
||||
args.setActiveMessage(lastMsg)
|
||||
await input.sdk.client.session.unrevert({ sessionID })
|
||||
input.prompt.reset()
|
||||
const lastMsg = findLast(input.userMessages(), (x) => x.id >= revertMessageID)
|
||||
input.setActiveMessage(lastMsg)
|
||||
return
|
||||
}
|
||||
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
||||
args.setActiveMessage(priorMsg)
|
||||
await input.sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||
const priorMsg = findLast(input.userMessages(), (x) => x.id < nextMessage.id)
|
||||
input.setActiveMessage(priorMsg)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.compact",
|
||||
title: language.t("command.session.compact"),
|
||||
description: language.t("command.session.compact.description"),
|
||||
title: input.language.t("command.session.compact"),
|
||||
description: input.language.t("command.session.compact.description"),
|
||||
slash: "compact",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
const sessionID = input.params.id
|
||||
if (!sessionID) return
|
||||
const model = local.model.current()
|
||||
const model = input.local.model.current()
|
||||
if (!model) {
|
||||
showToast({
|
||||
title: language.t("toast.model.none.title"),
|
||||
description: language.t("toast.model.none.description"),
|
||||
title: input.language.t("toast.model.none.title"),
|
||||
description: input.language.t("toast.model.none.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
await sdk.client.session.summarize({
|
||||
await input.sdk.client.session.summarize({
|
||||
sessionID,
|
||||
modelID: model.id,
|
||||
providerID: model.provider.id,
|
||||
@@ -368,27 +330,29 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.fork",
|
||||
title: language.t("command.session.fork"),
|
||||
description: language.t("command.session.fork.description"),
|
||||
title: input.language.t("command.session.fork"),
|
||||
description: input.language.t("command.session.fork.description"),
|
||||
slash: "fork",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: () => dialog.show(() => <DialogFork />),
|
||||
disabled: !input.params.id || input.visibleUserMessages().length === 0,
|
||||
onSelect: () => input.dialog.show(() => <DialogFork />),
|
||||
}),
|
||||
])
|
||||
|
||||
const shareCommands = createMemo(() => {
|
||||
if (sync.data.config.share === "disabled") return []
|
||||
if (input.sync.data.config.share === "disabled") return []
|
||||
return [
|
||||
sessionCommand({
|
||||
id: "session.share",
|
||||
title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"),
|
||||
description: info()?.share?.url
|
||||
? language.t("toast.session.share.success.description")
|
||||
: language.t("command.session.share.description"),
|
||||
title: input.info()?.share?.url
|
||||
? input.language.t("session.share.copy.copyLink")
|
||||
: input.language.t("command.session.share"),
|
||||
description: input.info()?.share?.url
|
||||
? input.language.t("toast.session.share.success.description")
|
||||
: input.language.t("command.session.share.description"),
|
||||
slash: "share",
|
||||
disabled: !params.id,
|
||||
disabled: !input.params.id,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
if (!input.params.id) return
|
||||
|
||||
const write = (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
@@ -418,7 +382,7 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
const ok = await write(url)
|
||||
if (!ok) {
|
||||
showToast({
|
||||
title: language.t("toast.session.share.copyFailed.title"),
|
||||
title: input.language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
@@ -426,27 +390,27 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
|
||||
showToast({
|
||||
title: existing
|
||||
? language.t("session.share.copy.copied")
|
||||
: language.t("toast.session.share.success.title"),
|
||||
description: language.t("toast.session.share.success.description"),
|
||||
? input.language.t("session.share.copy.copied")
|
||||
: input.language.t("toast.session.share.success.title"),
|
||||
description: input.language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
})
|
||||
}
|
||||
|
||||
const existing = info()?.share?.url
|
||||
const existing = input.info()?.share?.url
|
||||
if (existing) {
|
||||
await copy(existing, true)
|
||||
return
|
||||
}
|
||||
|
||||
const url = await sdk.client.session
|
||||
.share({ sessionID: params.id })
|
||||
const url = await input.sdk.client.session
|
||||
.share({ sessionID: input.params.id })
|
||||
.then((res) => res.data?.share?.url)
|
||||
.catch(() => undefined)
|
||||
if (!url) {
|
||||
showToast({
|
||||
title: language.t("toast.session.share.failed.title"),
|
||||
description: language.t("toast.session.share.failed.description"),
|
||||
title: input.language.t("toast.session.share.failed.title"),
|
||||
description: input.language.t("toast.session.share.failed.description"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
@@ -457,25 +421,25 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.unshare",
|
||||
title: language.t("command.session.unshare"),
|
||||
description: language.t("command.session.unshare.description"),
|
||||
title: input.language.t("command.session.unshare"),
|
||||
description: input.language.t("command.session.unshare.description"),
|
||||
slash: "unshare",
|
||||
disabled: !params.id || !info()?.share?.url,
|
||||
disabled: !input.params.id || !input.info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
await sdk.client.session
|
||||
.unshare({ sessionID: params.id })
|
||||
if (!input.params.id) return
|
||||
await input.sdk.client.session
|
||||
.unshare({ sessionID: input.params.id })
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: language.t("toast.session.unshare.success.title"),
|
||||
description: language.t("toast.session.unshare.success.description"),
|
||||
title: input.language.t("toast.session.unshare.success.title"),
|
||||
description: input.language.t("toast.session.unshare.success.description"),
|
||||
variant: "success",
|
||||
}),
|
||||
)
|
||||
.catch(() =>
|
||||
showToast({
|
||||
title: language.t("toast.session.unshare.failed.title"),
|
||||
description: language.t("toast.session.unshare.failed.description"),
|
||||
title: input.language.t("toast.session.unshare.failed.title"),
|
||||
description: input.language.t("toast.session.unshare.failed.description"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
@@ -484,7 +448,7 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
]
|
||||
})
|
||||
|
||||
command.register("session", () =>
|
||||
input.command.register("session", () =>
|
||||
[
|
||||
sessionCommands(),
|
||||
fileCommands(),
|
||||
@@ -495,6 +459,6 @@ export const useSessionCommands = (args: SessionCommandContext) => {
|
||||
permissionCommands(),
|
||||
sessionActionCommands(),
|
||||
shareCommands(),
|
||||
].flatMap((section) => section),
|
||||
].flatMap((x) => x),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user