mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-11 19:28:33 +00:00
chore: cleanup (#17197)
This commit is contained in:
@@ -19,6 +19,7 @@ import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange
|
||||
import { createStore } from "solid-js/store"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
@@ -36,12 +37,11 @@ import { useSDK } from "@/context/sdk"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
|
||||
import { createOpenReviewFile, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||
import { createOpenReviewFile, createSessionTabs, createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||
import { MessageTimeline } from "@/pages/session/message-timeline"
|
||||
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { resetSessionModel, syncSessionModel } from "@/pages/session/session-model-helpers"
|
||||
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
|
||||
import { SessionSidePanel } from "@/pages/session/session-side-panel"
|
||||
import { TerminalPanel } from "@/pages/session/terminal-panel"
|
||||
import { useSessionCommands } from "@/pages/session/use-session-commands"
|
||||
@@ -373,18 +373,22 @@ export default function Page() {
|
||||
if (!view().reviewPanel.opened()) view().reviewPanel.open()
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const active = tabs().active()
|
||||
if (!active) return
|
||||
|
||||
const path = file.pathFromTab(active)
|
||||
if (path) file.load(path)
|
||||
})
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const diffs = createMemo(() => (params.id ? (sync.data.session_diff[params.id] ?? []) : []))
|
||||
const reviewCount = createMemo(() => Math.max(info()?.summary?.files ?? 0, diffs().length))
|
||||
const hasReview = createMemo(() => reviewCount() > 0)
|
||||
const reviewTab = createMemo(() => isDesktop())
|
||||
const tabState = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: file.pathFromTab,
|
||||
normalizeTab,
|
||||
review: reviewTab,
|
||||
hasReview,
|
||||
})
|
||||
const contextOpen = tabState.contextOpen
|
||||
const openedTabs = tabState.openedTabs
|
||||
const activeTab = tabState.activeTab
|
||||
const activeFileTab = tabState.activeFileTab
|
||||
const revertMessageID = createMemo(() => info()?.revert?.messageID)
|
||||
const messages = createMemo(() => (params.id ? (sync.data.message[params.id] ?? []) : []))
|
||||
const messagesReady = createMemo(() => {
|
||||
@@ -421,6 +425,14 @@ export default function Page() {
|
||||
)
|
||||
const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
|
||||
|
||||
createEffect(() => {
|
||||
const tab = activeFileTab()
|
||||
if (!tab) return
|
||||
|
||||
const path = file.pathFromTab(tab)
|
||||
if (path) file.load(path)
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => lastUserMessage()?.id,
|
||||
@@ -806,15 +818,7 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
|
||||
const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
|
||||
const openedTabs = createMemo(() =>
|
||||
tabs()
|
||||
.all()
|
||||
.filter((tab) => tab !== "context" && tab !== "review"),
|
||||
)
|
||||
|
||||
const mobileChanges = createMemo(() => !isDesktop() && store.mobileTab === "changes")
|
||||
const reviewTab = createMemo(() => isDesktop())
|
||||
|
||||
const fileTreeTab = () => layout.fileTree.tab()
|
||||
const setFileTreeTab = (value: "changes" | "all") => layout.fileTree.setTab(value)
|
||||
@@ -850,6 +854,7 @@ export default function Page() {
|
||||
navigateMessageByOffset,
|
||||
setActiveMessage,
|
||||
focusInput,
|
||||
review: reviewTab,
|
||||
})
|
||||
|
||||
const openReviewFile = createOpenReviewFile({
|
||||
@@ -964,11 +969,10 @@ export default function Page() {
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => tabs().active(),
|
||||
activeFileTab,
|
||||
(active) => {
|
||||
if (!active) return
|
||||
if (fileTreeTab() !== "changes") return
|
||||
if (!file.pathFromTab(active)) return
|
||||
showAllFiles()
|
||||
},
|
||||
{ defer: true },
|
||||
@@ -1011,8 +1015,7 @@ export default function Page() {
|
||||
|
||||
const focusReviewDiff = (path: string) => {
|
||||
openReviewPanel()
|
||||
const current = view().review.open() ?? []
|
||||
if (!current.includes(path)) view().review.setOpen([...current, path])
|
||||
view().review.openPath(path)
|
||||
setTree({ activeDiff: path, pendingDiff: path })
|
||||
}
|
||||
|
||||
@@ -1057,29 +1060,6 @@ export default function Page() {
|
||||
requestAnimationFrame(() => attempt(0))
|
||||
})
|
||||
|
||||
const activeTab = createMemo(() => {
|
||||
const active = tabs().active()
|
||||
if (active === "context") return "context"
|
||||
if (active === "review" && reviewTab()) return "review"
|
||||
if (active && file.pathFromTab(active)) return normalizeTab(active)
|
||||
|
||||
const first = openedTabs()[0]
|
||||
if (first) return first
|
||||
if (contextOpen()) return "context"
|
||||
if (reviewTab() && hasReview()) return "review"
|
||||
return "empty"
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!layout.ready()) return
|
||||
if (tabs().active()) return
|
||||
if (openedTabs().length === 0 && !contextOpen() && !(reviewTab() && hasReview())) return
|
||||
|
||||
const next = activeTab()
|
||||
if (next === "empty") return
|
||||
tabs().setActive(next)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
@@ -1146,9 +1126,9 @@ export default function Page() {
|
||||
() => {
|
||||
void file.tree.list("")
|
||||
|
||||
const active = tabs().active()
|
||||
if (!active) return
|
||||
const path = file.pathFromTab(active)
|
||||
const tab = activeFileTab()
|
||||
if (!tab) return
|
||||
const path = file.pathFromTab(tab)
|
||||
if (!path) return
|
||||
void file.load(path, { force: true })
|
||||
},
|
||||
@@ -1400,14 +1380,30 @@ export default function Page() {
|
||||
<div class="relative bg-background-base size-full overflow-hidden flex flex-col">
|
||||
<SessionHeader />
|
||||
<div class="flex-1 min-h-0 flex flex-col md:flex-row">
|
||||
<SessionMobileTabs
|
||||
open={!isDesktop() && !!params.id}
|
||||
mobileTab={store.mobileTab}
|
||||
hasReview={hasReview()}
|
||||
reviewCount={reviewCount()}
|
||||
onSession={() => setStore("mobileTab", "session")}
|
||||
onChanges={() => setStore("mobileTab", "changes")}
|
||||
/>
|
||||
<Show when={!isDesktop() && !!params.id}>
|
||||
<Tabs value={store.mobileTab} class="h-auto">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger
|
||||
value="session"
|
||||
class="!w-1/2 !max-w-none"
|
||||
classes={{ button: "w-full" }}
|
||||
onClick={() => setStore("mobileTab", "session")}
|
||||
>
|
||||
{language.t("session.tab.session")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="changes"
|
||||
class="!w-1/2 !max-w-none !border-r-0"
|
||||
classes={{ button: "w-full" }}
|
||||
onClick={() => setStore("mobileTab", "changes")}
|
||||
>
|
||||
{hasReview()
|
||||
? language.t("session.review.filesChanged", { count: reviewCount() })
|
||||
: language.t("session.review.change.other")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
</Show>
|
||||
|
||||
{/* Session panel */}
|
||||
<div
|
||||
@@ -1467,23 +1463,7 @@ export default function Page() {
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<NewSessionView
|
||||
worktree={newSessionWorktree()}
|
||||
onWorktreeChange={(value) => {
|
||||
if (value === "create") {
|
||||
setStore("newSessionWorktree", value)
|
||||
return
|
||||
}
|
||||
|
||||
setStore("newSessionWorktree", "main")
|
||||
|
||||
const target = value === "main" ? sync.project?.worktree : value
|
||||
if (!target) return
|
||||
if (target === sdk.directory) return
|
||||
layout.projects.open(target)
|
||||
navigate(`/${base64Encode(target)}/session`)
|
||||
}}
|
||||
/>
|
||||
<NewSessionView worktree={newSessionWorktree()} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||
import { Show, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { PromptInput } from "@/components/prompt-input"
|
||||
import { useLanguage } from "@/context/language"
|
||||
@@ -50,7 +51,11 @@ export function SessionComposerRegion(props: {
|
||||
setSessionHandoff(sessionKey(), { prompt: previewPrompt() })
|
||||
})
|
||||
|
||||
const [ready, setReady] = createSignal(false)
|
||||
const [store, setStore] = createStore({
|
||||
ready: false,
|
||||
height: 320,
|
||||
body: undefined as HTMLDivElement | undefined,
|
||||
})
|
||||
let timer: number | undefined
|
||||
let frame: number | undefined
|
||||
|
||||
@@ -67,17 +72,17 @@ export function SessionComposerRegion(props: {
|
||||
|
||||
createEffect(() => {
|
||||
sessionKey()
|
||||
const active = props.ready
|
||||
const ready = props.ready
|
||||
const delay = 140
|
||||
|
||||
clear()
|
||||
setReady(false)
|
||||
if (!active) return
|
||||
setStore("ready", false)
|
||||
if (!ready) return
|
||||
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = undefined
|
||||
timer = window.setTimeout(() => {
|
||||
setReady(true)
|
||||
setStore("ready", true)
|
||||
timer = undefined
|
||||
}, delay)
|
||||
})
|
||||
@@ -85,21 +90,19 @@ export function SessionComposerRegion(props: {
|
||||
|
||||
onCleanup(clear)
|
||||
|
||||
const open = createMemo(() => ready() && props.state.dock() && !props.state.closing())
|
||||
const open = createMemo(() => store.ready && props.state.dock() && !props.state.closing())
|
||||
const progress = useSpring(() => (open() ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
|
||||
const value = createMemo(() => Math.max(0, Math.min(1, progress())))
|
||||
const [height, setHeight] = createSignal(320)
|
||||
const dock = createMemo(() => (ready() && props.state.dock()) || value() > 0.001)
|
||||
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, height()))
|
||||
const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
|
||||
const full = createMemo(() => Math.max(78, store.height))
|
||||
|
||||
createEffect(() => {
|
||||
const el = contentRef()
|
||||
const el = store.body
|
||||
if (!el) return
|
||||
const update = () => {
|
||||
setHeight(el.getBoundingClientRect().height)
|
||||
setStore("height", el.getBoundingClientRect().height)
|
||||
}
|
||||
update()
|
||||
const observer = new ResizeObserver(update)
|
||||
@@ -174,7 +177,7 @@ export function SessionComposerRegion(props: {
|
||||
"max-height": `${full() * value()}px`,
|
||||
}}
|
||||
>
|
||||
<div ref={setContentRef}>
|
||||
<div ref={(el) => setStore("body", el)}>
|
||||
<SessionTodoDock
|
||||
todos={props.state.todos()}
|
||||
title={language.t("session.todo.title")}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { useSpring } from "@opencode-ai/ui/motion-spring"
|
||||
import { TextReveal } from "@opencode-ai/ui/text-reveal"
|
||||
import { TextStrikethrough } from "@opencode-ai/ui/text-strikethrough"
|
||||
import { Index, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
||||
import { Index, createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
function dot(status: Todo["status"]) {
|
||||
if (status !== "in_progress") return undefined
|
||||
@@ -40,8 +41,12 @@ export function SessionTodoDock(props: {
|
||||
expandLabel: string
|
||||
dockProgress: number
|
||||
}) {
|
||||
const [collapsed, setCollapsed] = createSignal(false)
|
||||
const toggle = () => setCollapsed((value) => !value)
|
||||
const [store, setStore] = createStore({
|
||||
collapsed: false,
|
||||
height: 320,
|
||||
})
|
||||
|
||||
const toggle = () => setStore("collapsed", (value) => !value)
|
||||
|
||||
const total = createMemo(() => props.todos.length)
|
||||
const done = createMemo(() => props.todos.filter((todo) => todo.status === "completed").length)
|
||||
@@ -56,22 +61,21 @@ export function SessionTodoDock(props: {
|
||||
)
|
||||
|
||||
const preview = createMemo(() => active()?.content ?? "")
|
||||
const collapse = useSpring(() => (collapsed() ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
|
||||
const collapse = useSpring(() => (store.collapsed ? 1 : 0), { visualDuration: 0.3, bounce: 0 })
|
||||
const dock = createMemo(() => Math.max(0, Math.min(1, props.dockProgress)))
|
||||
const shut = createMemo(() => 1 - dock())
|
||||
const value = createMemo(() => Math.max(0, Math.min(1, collapse())))
|
||||
const hide = createMemo(() => Math.max(value(), shut()))
|
||||
const off = createMemo(() => hide() > 0.98)
|
||||
const turn = createMemo(() => Math.max(0, Math.min(1, value())))
|
||||
const [height, setHeight] = createSignal(320)
|
||||
const full = createMemo(() => Math.max(78, height()))
|
||||
const full = createMemo(() => Math.max(78, store.height))
|
||||
let contentRef: HTMLDivElement | undefined
|
||||
|
||||
createEffect(() => {
|
||||
const el = contentRef
|
||||
if (!el) return
|
||||
const update = () => {
|
||||
setHeight(el.getBoundingClientRect().height)
|
||||
setStore("height", el.getBoundingClientRect().height)
|
||||
}
|
||||
update()
|
||||
const observer = new ResizeObserver(update)
|
||||
@@ -127,7 +131,7 @@ export function SessionTodoDock(props: {
|
||||
>
|
||||
<TextReveal
|
||||
class="text-14-regular text-text-base cursor-default"
|
||||
text={collapsed() ? preview() : undefined}
|
||||
text={store.collapsed ? preview() : undefined}
|
||||
duration={600}
|
||||
travel={25}
|
||||
edge={17}
|
||||
@@ -140,7 +144,7 @@ export function SessionTodoDock(props: {
|
||||
<div class="ml-auto">
|
||||
<IconButton
|
||||
data-action="session-todo-toggle-button"
|
||||
data-collapsed={collapsed() ? "true" : "false"}
|
||||
data-collapsed={store.collapsed ? "true" : "false"}
|
||||
icon="chevron-down"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
@@ -153,14 +157,14 @@ export function SessionTodoDock(props: {
|
||||
event.stopPropagation()
|
||||
toggle()
|
||||
}}
|
||||
aria-label={collapsed() ? props.expandLabel : props.collapseLabel}
|
||||
aria-label={store.collapsed ? props.expandLabel : props.collapseLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-slot="session-todo-list"
|
||||
aria-hidden={collapsed() || off()}
|
||||
aria-hidden={store.collapsed || off()}
|
||||
classList={{
|
||||
"pointer-events-none": hide() > 0.1,
|
||||
}}
|
||||
@@ -169,7 +173,7 @@ export function SessionTodoDock(props: {
|
||||
opacity: `${Math.max(0, Math.min(1, 1 - hide()))}`,
|
||||
}}
|
||||
>
|
||||
<TodoList todos={props.todos} open={!collapsed()} />
|
||||
<TodoList todos={props.todos} open={!store.collapsed} />
|
||||
</div>
|
||||
</div>
|
||||
</DockTray>
|
||||
@@ -177,8 +181,10 @@ export function SessionTodoDock(props: {
|
||||
}
|
||||
|
||||
function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||
const [stuck, setStuck] = createSignal(false)
|
||||
const [scrolling, setScrolling] = createSignal(false)
|
||||
const [store, setStore] = createStore({
|
||||
stuck: false,
|
||||
scrolling: false,
|
||||
})
|
||||
let scrollRef!: HTMLDivElement
|
||||
let timer: number | undefined
|
||||
|
||||
@@ -186,7 +192,7 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||
|
||||
const ensure = () => {
|
||||
if (!props.open) return
|
||||
if (scrolling()) return
|
||||
if (store.scrolling) return
|
||||
if (!scrollRef || scrollRef.offsetParent === null) return
|
||||
|
||||
const el = scrollRef.querySelector("[data-in-progress]")
|
||||
@@ -207,7 +213,7 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||
scrollRef.scrollTop = bottom - (scrollRef.clientHeight - bottomFade)
|
||||
}
|
||||
|
||||
setStuck(scrollRef.scrollTop > 0)
|
||||
setStore("stuck", scrollRef.scrollTop > 0)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
@@ -229,11 +235,11 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||
ref={scrollRef}
|
||||
style={{ "overflow-anchor": "none" }}
|
||||
onScroll={(e) => {
|
||||
setStuck(e.currentTarget.scrollTop > 0)
|
||||
setScrolling(true)
|
||||
setStore("stuck", e.currentTarget.scrollTop > 0)
|
||||
setStore("scrolling", true)
|
||||
if (timer) window.clearTimeout(timer)
|
||||
timer = window.setTimeout(() => {
|
||||
setScrolling(false)
|
||||
setStore("scrolling", false)
|
||||
if (inProgress() < 0) return
|
||||
requestAnimationFrame(ensure)
|
||||
}, 250)
|
||||
@@ -278,7 +284,7 @@ function TodoList(props: { todos: Todo[]; open: boolean }) {
|
||||
class="pointer-events-none absolute top-0 left-0 right-0 h-4 transition-opacity duration-150"
|
||||
style={{
|
||||
background: "linear-gradient(to bottom, var(--background-base), transparent)",
|
||||
opacity: stuck() ? 1 : 0,
|
||||
opacity: store.stuck ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useLanguage } from "@/context/language"
|
||||
import { usePrompt } from "@/context/prompt"
|
||||
import { getSessionHandoff } from "@/pages/session/handoff"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
|
||||
function FileCommentMenu(props: {
|
||||
moreLabel: string
|
||||
@@ -58,6 +59,11 @@ export function FileTabContent(props: { tab: string }) {
|
||||
const prompt = usePrompt()
|
||||
const fileComponent = useFileComponent()
|
||||
const { sessionKey, tabs, view } = useSessionLayout()
|
||||
const activeFileTab = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: file.pathFromTab,
|
||||
normalizeTab: (tab) => (tab.startsWith("file://") ? file.tab(tab) : tab),
|
||||
}).activeFileTab
|
||||
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let scrollFrame: number | undefined
|
||||
@@ -228,7 +234,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (tabs().active() !== props.tab) return
|
||||
if (activeFileTab() !== props.tab) return
|
||||
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return
|
||||
if (event.key.toLowerCase() !== "f") return
|
||||
|
||||
@@ -256,7 +262,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
const p = path()
|
||||
if (!focus || !p) return
|
||||
if (focus.file !== p) return
|
||||
if (tabs().active() !== props.tab) return
|
||||
if (activeFileTab() !== props.tab) return
|
||||
|
||||
const target = fileComments().find((comment) => comment.id === focus.id)
|
||||
if (!target) return
|
||||
@@ -376,7 +382,7 @@ export function FileTabContent(props: { tab: string }) {
|
||||
createEffect(() => {
|
||||
const loaded = !!state()?.loaded
|
||||
const ready = file.ready()
|
||||
const active = tabs().active() === props.tab
|
||||
const active = activeFileTab() === props.tab
|
||||
const restore = (loaded && !prev.loaded) || (ready && !prev.ready) || (active && loaded && !prev.active)
|
||||
prev = { loaded, ready, active }
|
||||
if (!restore) return
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createOpenReviewFile, createOpenSessionFileTab, focusTerminalById, getTabReorderIndex } from "./helpers"
|
||||
import { createMemo, createRoot } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import {
|
||||
createOpenReviewFile,
|
||||
createOpenSessionFileTab,
|
||||
createSessionTabs,
|
||||
focusTerminalById,
|
||||
getTabReorderIndex,
|
||||
} from "./helpers"
|
||||
|
||||
describe("createOpenReviewFile", () => {
|
||||
test("opens and loads selected review file", () => {
|
||||
@@ -87,3 +95,66 @@ describe("getTabReorderIndex", () => {
|
||||
expect(getTabReorderIndex(["a", "b", "c"], "a", "missing")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("createSessionTabs", () => {
|
||||
test("normalizes the effective file tab", () => {
|
||||
createRoot((dispose) => {
|
||||
const [state] = createStore({
|
||||
active: undefined as string | undefined,
|
||||
all: ["file://src/a.ts", "context"],
|
||||
})
|
||||
const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all }))
|
||||
const result = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: (tab) => (tab.startsWith("file://") ? tab.slice("file://".length) : undefined),
|
||||
normalizeTab: (tab) => (tab.startsWith("file://") ? `norm:${tab.slice("file://".length)}` : tab),
|
||||
})
|
||||
|
||||
expect(result.activeTab()).toBe("norm:src/a.ts")
|
||||
expect(result.activeFileTab()).toBe("norm:src/a.ts")
|
||||
expect(result.closableTab()).toBe("norm:src/a.ts")
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
test("prefers context and review fallbacks when no file tab is active", () => {
|
||||
createRoot((dispose) => {
|
||||
const [state] = createStore({
|
||||
active: undefined as string | undefined,
|
||||
all: ["context"],
|
||||
})
|
||||
const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all }))
|
||||
const result = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: () => undefined,
|
||||
normalizeTab: (tab) => tab,
|
||||
review: () => true,
|
||||
hasReview: () => true,
|
||||
})
|
||||
|
||||
expect(result.activeTab()).toBe("context")
|
||||
expect(result.closableTab()).toBe("context")
|
||||
dispose()
|
||||
})
|
||||
|
||||
createRoot((dispose) => {
|
||||
const [state] = createStore({
|
||||
active: undefined as string | undefined,
|
||||
all: [],
|
||||
})
|
||||
const tabs = createMemo(() => ({ active: () => state.active, all: () => state.all }))
|
||||
const result = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: () => undefined,
|
||||
normalizeTab: (tab) => tab,
|
||||
review: () => true,
|
||||
hasReview: () => true,
|
||||
})
|
||||
|
||||
expect(result.activeTab()).toBe("review")
|
||||
expect(result.activeFileTab()).toBeUndefined()
|
||||
expect(result.closableTab()).toBeUndefined()
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,77 @@
|
||||
import { batch, onCleanup, onMount } from "solid-js"
|
||||
import { batch, createMemo, onCleanup, onMount, type Accessor } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { same } from "@/utils/same"
|
||||
|
||||
const emptyTabs: string[] = []
|
||||
|
||||
type Tabs = {
|
||||
active: Accessor<string | undefined>
|
||||
all: Accessor<string[]>
|
||||
}
|
||||
|
||||
type TabsInput = {
|
||||
tabs: Accessor<Tabs>
|
||||
pathFromTab: (tab: string) => string | undefined
|
||||
normalizeTab: (tab: string) => string
|
||||
review?: Accessor<boolean>
|
||||
hasReview?: Accessor<boolean>
|
||||
}
|
||||
|
||||
export const getSessionKey = (dir: string | undefined, id: string | undefined) => `${dir ?? ""}${id ? `/${id}` : ""}`
|
||||
|
||||
export const createSessionTabs = (input: TabsInput) => {
|
||||
const review = input.review ?? (() => false)
|
||||
const hasReview = input.hasReview ?? (() => false)
|
||||
const contextOpen = createMemo(() => input.tabs().active() === "context" || input.tabs().all().includes("context"))
|
||||
const openedTabs = createMemo(
|
||||
() => {
|
||||
const seen = new Set<string>()
|
||||
return input
|
||||
.tabs()
|
||||
.all()
|
||||
.flatMap((tab) => {
|
||||
if (tab === "context" || tab === "review") return []
|
||||
const value = input.pathFromTab(tab) ? input.normalizeTab(tab) : tab
|
||||
if (seen.has(value)) return []
|
||||
seen.add(value)
|
||||
return [value]
|
||||
})
|
||||
},
|
||||
emptyTabs,
|
||||
{ equals: same },
|
||||
)
|
||||
const activeTab = createMemo(() => {
|
||||
const active = input.tabs().active()
|
||||
if (active === "context") return active
|
||||
if (active === "review" && review()) return active
|
||||
if (active && input.pathFromTab(active)) return input.normalizeTab(active)
|
||||
|
||||
const first = openedTabs()[0]
|
||||
if (first) return first
|
||||
if (contextOpen()) return "context"
|
||||
if (review() && hasReview()) return "review"
|
||||
return "empty"
|
||||
})
|
||||
const activeFileTab = createMemo(() => {
|
||||
const active = activeTab()
|
||||
if (!openedTabs().includes(active)) return
|
||||
return active
|
||||
})
|
||||
const closableTab = createMemo(() => {
|
||||
const active = activeTab()
|
||||
if (active === "context") return active
|
||||
if (!openedTabs().includes(active)) return
|
||||
return active
|
||||
})
|
||||
|
||||
return {
|
||||
contextOpen,
|
||||
openedTabs,
|
||||
activeTab,
|
||||
activeFileTab,
|
||||
closableTab,
|
||||
}
|
||||
}
|
||||
|
||||
export const focusTerminalById = (id: string) => {
|
||||
const wrapper = document.getElementById(`terminal-wrapper-${id}`)
|
||||
|
||||
@@ -37,14 +37,6 @@ export interface SessionReviewTabProps {
|
||||
}
|
||||
}
|
||||
|
||||
export function StickyAddButton(props: { children: JSX.Element }) {
|
||||
return (
|
||||
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SessionReviewTab(props: SessionReviewTabProps) {
|
||||
let scroll: HTMLDivElement | undefined
|
||||
let restoreFrame: number | undefined
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Show } from "solid-js"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
export function SessionMobileTabs(props: {
|
||||
open: boolean
|
||||
mobileTab: "session" | "changes"
|
||||
hasReview: boolean
|
||||
reviewCount: number
|
||||
onSession: () => void
|
||||
onChanges: () => void
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<Show when={props.open}>
|
||||
<Tabs value={props.mobileTab} class="h-auto">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger
|
||||
value="session"
|
||||
class="!w-1/2 !max-w-none"
|
||||
classes={{ button: "w-full" }}
|
||||
onClick={props.onSession}
|
||||
>
|
||||
{language.t("session.tab.session")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="changes"
|
||||
class="!w-1/2 !max-w-none !border-r-0"
|
||||
classes={{ button: "w-full" }}
|
||||
onClick={props.onChanges}
|
||||
>
|
||||
{props.hasReview
|
||||
? language.t("session.review.filesChanged", { count: props.reviewCount })
|
||||
: language.t("session.review.change.other")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
@@ -22,8 +22,7 @@ import { useLayout } from "@/context/layout"
|
||||
import { useSync } from "@/context/sync"
|
||||
import { createFileTabListSync } from "@/pages/session/file-tab-scroll"
|
||||
import { FileTabContent } from "@/pages/session/file-tabs"
|
||||
import { createOpenSessionFileTab, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
||||
import { StickyAddButton } from "@/pages/session/review-tab"
|
||||
import { createOpenSessionFileTab, createSessionTabs, getTabReorderIndex, type Sizing } from "@/pages/session/helpers"
|
||||
import { setSessionHandoff } from "@/pages/session/handoff"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
|
||||
@@ -132,31 +131,17 @@ export function SessionSidePanel(props: {
|
||||
setActive: tabs().setActive,
|
||||
})
|
||||
|
||||
const contextOpen = createMemo(() => tabs().active() === "context" || tabs().all().includes("context"))
|
||||
const openedTabs = createMemo(() =>
|
||||
tabs()
|
||||
.all()
|
||||
.filter((tab) => tab !== "context" && tab !== "review"),
|
||||
)
|
||||
|
||||
const activeTab = createMemo(() => {
|
||||
const active = tabs().active()
|
||||
if (active === "context") return "context"
|
||||
if (active === "review" && reviewTab()) return "review"
|
||||
if (active && file.pathFromTab(active)) return normalizeTab(active)
|
||||
|
||||
const first = openedTabs()[0]
|
||||
if (first) return first
|
||||
if (contextOpen()) return "context"
|
||||
if (reviewTab() && hasReview()) return "review"
|
||||
return "empty"
|
||||
})
|
||||
|
||||
const activeFileTab = createMemo(() => {
|
||||
const active = activeTab()
|
||||
if (!openedTabs().includes(active)) return
|
||||
return active
|
||||
const tabState = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: file.pathFromTab,
|
||||
normalizeTab,
|
||||
review: reviewTab,
|
||||
hasReview,
|
||||
})
|
||||
const contextOpen = tabState.contextOpen
|
||||
const openedTabs = tabState.openedTabs
|
||||
const activeTab = tabState.activeTab
|
||||
const activeFileTab = tabState.activeFileTab
|
||||
|
||||
const fileTreeTab = () => layout.fileTree.tab()
|
||||
|
||||
@@ -297,7 +282,7 @@ export function SessionSidePanel(props: {
|
||||
<SortableProvider ids={openedTabs()}>
|
||||
<For each={openedTabs()}>{(tab) => <SortableTab tab={tab} onTabClose={tabs().close} />}</For>
|
||||
</SortableProvider>
|
||||
<StickyAddButton>
|
||||
<div class="bg-background-stronger h-full shrink-0 sticky right-0 z-10 flex items-center justify-center pr-3">
|
||||
<TooltipKeybind
|
||||
title={language.t("command.file.open")}
|
||||
keybind={command.keybind("file.open")}
|
||||
@@ -314,7 +299,7 @@ export function SessionSidePanel(props: {
|
||||
aria-label={language.t("command.file.open")}
|
||||
/>
|
||||
</TooltipKeybind>
|
||||
</StickyAddButton>
|
||||
</div>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
||||
@@ -354,10 +339,10 @@ export function SessionSidePanel(props: {
|
||||
<DragOverlay>
|
||||
<Show when={store.activeDraggable} keyed>
|
||||
{(tab) => {
|
||||
const path = createMemo(() => file.pathFromTab(tab))
|
||||
const path = file.pathFromTab(tab)
|
||||
return (
|
||||
<div data-component="tabs-drag-preview">
|
||||
<Show when={path()}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||
<Show when={path}>{(p) => <FileVisual active path={p()} />}</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, Show, createEffect, createMemo, on, onCleanup } from "solid-js"
|
||||
import { For, Show, createEffect, createMemo, on, onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
|
||||
@@ -13,7 +13,7 @@ import { Terminal } from "@/components/terminal"
|
||||
import { useCommand } from "@/context/command"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import { useLayout } from "@/context/layout"
|
||||
import { useTerminal, type LocalPTY } from "@/context/terminal"
|
||||
import { useTerminal } from "@/context/terminal"
|
||||
import { terminalTabLabel } from "@/pages/session/terminal-label"
|
||||
import { createSizing, focusTerminalById } from "@/pages/session/helpers"
|
||||
import { getTerminalHandoff, setTerminalHandoff } from "@/pages/session/handoff"
|
||||
@@ -41,7 +41,7 @@ export function TerminalPanel() {
|
||||
const max = () => store.view * 0.6
|
||||
const pane = () => Math.min(height(), max())
|
||||
|
||||
createEffect(() => {
|
||||
onMount(() => {
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
const sync = () => setStore("view", window.visualViewport?.height ?? window.innerHeight)
|
||||
@@ -144,9 +144,8 @@ export function TerminalPanel() {
|
||||
return getTerminalHandoff(dir) ?? []
|
||||
})
|
||||
|
||||
const all = createMemo(() => terminal.all())
|
||||
const all = terminal.all
|
||||
const ids = createMemo(() => all().map((pty) => pty.id))
|
||||
const byId = createMemo(() => new Map(all().map((pty) => [pty.id, { ...pty }])))
|
||||
|
||||
const handleTerminalDragStart = (event: unknown) => {
|
||||
const id = getDraggableId(event)
|
||||
@@ -159,8 +158,8 @@ export function TerminalPanel() {
|
||||
if (!draggable || !droppable) return
|
||||
|
||||
const terminals = terminal.all()
|
||||
const fromIndex = terminals.findIndex((t: LocalPTY) => t.id === draggable.id.toString())
|
||||
const toIndex = terminals.findIndex((t: LocalPTY) => t.id === droppable.id.toString())
|
||||
const fromIndex = terminals.findIndex((t) => t.id === draggable.id.toString())
|
||||
const toIndex = terminals.findIndex((t) => t.id === droppable.id.toString())
|
||||
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||
terminal.move(draggable.id.toString(), toIndex)
|
||||
}
|
||||
@@ -253,13 +252,7 @@ export function TerminalPanel() {
|
||||
>
|
||||
<Tabs.List class="h-10 border-b border-border-weaker-base">
|
||||
<SortableProvider ids={ids()}>
|
||||
<For each={ids()}>
|
||||
{(id) => (
|
||||
<Show when={byId().get(id)}>
|
||||
{(pty) => <SortableTerminalTab terminal={pty()} onClose={close} />}
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
<For each={all()}>{(pty) => <SortableTerminalTab terminal={pty} onClose={close} />}</For>
|
||||
</SortableProvider>
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<TooltipKeybind
|
||||
@@ -281,7 +274,7 @@ export function TerminalPanel() {
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
<Show when={terminal.active()} keyed>
|
||||
{(id) => (
|
||||
<Show when={byId().get(id)}>
|
||||
<Show when={all().find((pty) => pty.id === id)}>
|
||||
{(pty) => (
|
||||
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
|
||||
<Terminal
|
||||
@@ -299,9 +292,9 @@ export function TerminalPanel() {
|
||||
</div>
|
||||
</div>
|
||||
<DragOverlay>
|
||||
<Show when={store.activeDraggable}>
|
||||
{(draggedId) => (
|
||||
<Show when={byId().get(draggedId())}>
|
||||
<Show when={store.activeDraggable} keyed>
|
||||
{(id) => (
|
||||
<Show when={all().find((pty) => pty.id === id)}>
|
||||
{(t) => (
|
||||
<div class="relative p-1 h-10 flex items-center bg-background-stronger text-14-regular">
|
||||
{terminalTabLabel({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { useCommand, type CommandOption } from "@/context/command"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
@@ -18,6 +17,7 @@ import { DialogSelectMcp } from "@/components/dialog-select-mcp"
|
||||
import { DialogFork } from "@/components/dialog-fork"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { findLast } from "@opencode-ai/util/array"
|
||||
import { createSessionTabs } from "@/pages/session/helpers"
|
||||
import { extractPromptFromParts } from "@/utils/prompt"
|
||||
import { UserMessage } from "@opencode-ai/sdk/v2"
|
||||
import { useSessionLayout } from "@/pages/session/session-layout"
|
||||
@@ -26,6 +26,7 @@ export type SessionCommandContext = {
|
||||
navigateMessageByOffset: (offset: number) => void
|
||||
setActiveMessage: (message: UserMessage | undefined) => void
|
||||
focusInput: () => void
|
||||
review?: () => boolean
|
||||
}
|
||||
|
||||
const withCategory = (category: string) => {
|
||||
@@ -50,17 +51,43 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const navigate = useNavigate()
|
||||
const { params, tabs, view } = useSessionLayout()
|
||||
|
||||
const info = createMemo(() => (params.id ? sync.session.get(params.id) : undefined))
|
||||
const info = () => {
|
||||
const id = params.id
|
||||
if (!id) return
|
||||
return sync.session.get(id)
|
||||
}
|
||||
const hasReview = () => {
|
||||
const id = params.id
|
||||
if (!id) return false
|
||||
return Math.max(info()?.summary?.files ?? 0, (sync.data.session_diff[id] ?? []).length) > 0
|
||||
}
|
||||
const normalizeTab = (tab: string) => {
|
||||
if (!tab.startsWith("file://")) return tab
|
||||
return file.tab(tab)
|
||||
}
|
||||
const tabState = createSessionTabs({
|
||||
tabs,
|
||||
pathFromTab: file.pathFromTab,
|
||||
normalizeTab,
|
||||
review: actions.review,
|
||||
hasReview,
|
||||
})
|
||||
const activeFileTab = tabState.activeFileTab
|
||||
const closableTab = tabState.closableTab
|
||||
|
||||
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 status = () => sync.data.session_status[params.id ?? ""] ?? idle
|
||||
const messages = () => {
|
||||
const id = params.id
|
||||
if (!id) return []
|
||||
return sync.data.message[id] ?? []
|
||||
}
|
||||
const userMessages = () => messages().filter((m) => m.role === "user") as UserMessage[]
|
||||
const visibleUserMessages = () => {
|
||||
const revert = info()?.revert?.messageID
|
||||
if (!revert) return userMessages()
|
||||
return userMessages().filter((m) => m.id < revert)
|
||||
})
|
||||
}
|
||||
|
||||
const showAllFiles = () => {
|
||||
if (layout.fileTree.tab() !== "changes") return
|
||||
@@ -79,9 +106,9 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
}
|
||||
|
||||
const canAddSelectionContext = () => {
|
||||
const active = tabs().active()
|
||||
if (!active) return false
|
||||
const path = file.pathFromTab(active)
|
||||
const tab = activeFileTab()
|
||||
if (!tab) return false
|
||||
const path = file.pathFromTab(tab)
|
||||
if (!path) return false
|
||||
return file.selectedLines(path) != null
|
||||
}
|
||||
@@ -100,404 +127,369 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
|
||||
const agentCommand = withCategory(language.t("command.category.agent"))
|
||||
const permissionsCommand = withCategory(language.t("command.category.permissions"))
|
||||
|
||||
const sessionCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "session.new",
|
||||
title: language.t("command.session.new"),
|
||||
keybind: "mod+shift+s",
|
||||
slash: "new",
|
||||
onSelect: () => navigate(`/${params.dir}/session`),
|
||||
}),
|
||||
])
|
||||
|
||||
const fileCommands = createMemo(() => [
|
||||
fileCommand({
|
||||
id: "file.open",
|
||||
title: language.t("command.file.open"),
|
||||
description: language.t("palette.search.placeholder"),
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
||||
}),
|
||||
fileCommand({
|
||||
id: "tab.close",
|
||||
title: language.t("command.tab.close"),
|
||||
keybind: "mod+w",
|
||||
disabled: !tabs().active(),
|
||||
onSelect: () => {
|
||||
const active = tabs().active()
|
||||
if (!active) return
|
||||
tabs().close(active)
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const contextCommands = createMemo(() => [
|
||||
contextCommand({
|
||||
id: "context.addSelection",
|
||||
title: language.t("command.context.addSelection"),
|
||||
description: language.t("command.context.addSelection.description"),
|
||||
keybind: "mod+shift+l",
|
||||
disabled: !canAddSelectionContext(),
|
||||
onSelect: () => {
|
||||
const active = tabs().active()
|
||||
if (!active) return
|
||||
const path = file.pathFromTab(active)
|
||||
if (!path) return
|
||||
|
||||
const range = file.selectedLines(path) as SelectedLineRange | null | undefined
|
||||
if (!range) {
|
||||
showToast({
|
||||
title: language.t("toast.context.noLineSelection.title"),
|
||||
description: language.t("toast.context.noLineSelection.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
addSelectionToContext(path, selectionFromLines(range))
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const viewCommands = createMemo(() => [
|
||||
viewCommand({
|
||||
id: "terminal.toggle",
|
||||
title: language.t("command.terminal.toggle"),
|
||||
keybind: "ctrl+`",
|
||||
slash: "terminal",
|
||||
onSelect: () => view().terminal.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "review.toggle",
|
||||
title: language.t("command.review.toggle"),
|
||||
keybind: "mod+shift+r",
|
||||
onSelect: () => view().reviewPanel.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "fileTree.toggle",
|
||||
title: language.t("command.fileTree.toggle"),
|
||||
keybind: "mod+\\",
|
||||
onSelect: () => layout.fileTree.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "input.focus",
|
||||
title: language.t("command.input.focus"),
|
||||
keybind: "ctrl+l",
|
||||
onSelect: () => focusInput(),
|
||||
}),
|
||||
terminalCommand({
|
||||
id: "terminal.new",
|
||||
title: language.t("command.terminal.new"),
|
||||
description: language.t("command.terminal.new.description"),
|
||||
keybind: "ctrl+alt+t",
|
||||
onSelect: () => {
|
||||
if (terminal.all().length > 0) terminal.new()
|
||||
view().terminal.open()
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const messageCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "message.previous",
|
||||
title: language.t("command.message.previous"),
|
||||
description: language.t("command.message.previous.description"),
|
||||
keybind: "mod+arrowup",
|
||||
disabled: !params.id,
|
||||
onSelect: () => navigateMessageByOffset(-1),
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "message.next",
|
||||
title: language.t("command.message.next"),
|
||||
description: language.t("command.message.next.description"),
|
||||
keybind: "mod+arrowdown",
|
||||
disabled: !params.id,
|
||||
onSelect: () => navigateMessageByOffset(1),
|
||||
}),
|
||||
])
|
||||
|
||||
const agentCommands = createMemo(() => [
|
||||
modelCommand({
|
||||
id: "model.choose",
|
||||
title: language.t("command.model.choose"),
|
||||
description: language.t("command.model.choose.description"),
|
||||
keybind: "mod+'",
|
||||
slash: "model",
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||
}),
|
||||
mcpCommand({
|
||||
id: "mcp.toggle",
|
||||
title: language.t("command.mcp.toggle"),
|
||||
description: language.t("command.mcp.toggle.description"),
|
||||
keybind: "mod+;",
|
||||
slash: "mcp",
|
||||
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle",
|
||||
title: language.t("command.agent.cycle"),
|
||||
description: language.t("command.agent.cycle.description"),
|
||||
keybind: "mod+.",
|
||||
slash: "agent",
|
||||
onSelect: () => local.agent.move(1),
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle.reverse",
|
||||
title: language.t("command.agent.cycle.reverse"),
|
||||
description: language.t("command.agent.cycle.reverse.description"),
|
||||
keybind: "shift+mod+.",
|
||||
onSelect: () => local.agent.move(-1),
|
||||
}),
|
||||
modelCommand({
|
||||
id: "model.variant.cycle",
|
||||
title: language.t("command.model.variant.cycle"),
|
||||
description: language.t("command.model.variant.cycle.description"),
|
||||
keybind: "shift+mod+d",
|
||||
onSelect: () => {
|
||||
local.model.variant.cycle()
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const isAutoAcceptActive = () => {
|
||||
const sessionID = params.id
|
||||
if (sessionID) return permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
return permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
}
|
||||
command.register("session", () => {
|
||||
const share =
|
||||
sync.data.config.share === "disabled"
|
||||
? []
|
||||
: [
|
||||
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"),
|
||||
slash: "share",
|
||||
disabled: !params.id,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
|
||||
const permissionCommands = createMemo(() => [
|
||||
permissionsCommand({
|
||||
id: "permissions.autoaccept",
|
||||
title: isAutoAcceptActive()
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable"),
|
||||
keybind: "mod+shift+a",
|
||||
disabled: false,
|
||||
onSelect: () => {
|
||||
const sessionID = params.id
|
||||
if (sessionID) {
|
||||
permission.toggleAutoAccept(sessionID, sdk.directory)
|
||||
} else {
|
||||
permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||
}
|
||||
const active = sessionID
|
||||
? permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
: permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
showToast({
|
||||
title: active
|
||||
? language.t("toast.permissions.autoaccept.on.title")
|
||||
: language.t("toast.permissions.autoaccept.off.title"),
|
||||
description: active
|
||||
? language.t("toast.permissions.autoaccept.on.description")
|
||||
: language.t("toast.permissions.autoaccept.off.description"),
|
||||
})
|
||||
},
|
||||
}),
|
||||
])
|
||||
const write = (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
if (body) {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = value
|
||||
textarea.setAttribute("readonly", "")
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.opacity = "0"
|
||||
textarea.style.pointerEvents = "none"
|
||||
body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const copied = document.execCommand("copy")
|
||||
body.removeChild(textarea)
|
||||
if (copied) return Promise.resolve(true)
|
||||
}
|
||||
|
||||
const sessionActionCommands = createMemo(() => [
|
||||
sessionCommand({
|
||||
id: "session.undo",
|
||||
title: language.t("command.session.undo"),
|
||||
description: language.t("command.session.undo.description"),
|
||||
slash: "undo",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
if (status()?.type !== "idle") {
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
}
|
||||
const revert = info()?.revert?.messageID
|
||||
const message = findLast(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]
|
||||
if (parts) {
|
||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||
prompt.set(restored)
|
||||
}
|
||||
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
||||
setActiveMessage(priorMessage)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.redo",
|
||||
title: language.t("command.session.redo"),
|
||||
description: language.t("command.session.redo.description"),
|
||||
slash: "redo",
|
||||
disabled: !params.id || !info()?.revert?.messageID,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const revertMessageID = info()?.revert?.messageID
|
||||
if (!revertMessageID) return
|
||||
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
|
||||
if (!nextMessage) {
|
||||
await sdk.client.session.unrevert({ sessionID })
|
||||
prompt.reset()
|
||||
const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID)
|
||||
setActiveMessage(lastMsg)
|
||||
return
|
||||
}
|
||||
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
||||
setActiveMessage(priorMsg)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.compact",
|
||||
title: language.t("command.session.compact"),
|
||||
description: language.t("command.session.compact.description"),
|
||||
slash: "compact",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const model = local.model.current()
|
||||
if (!model) {
|
||||
showToast({
|
||||
title: language.t("toast.model.none.title"),
|
||||
description: language.t("toast.model.none.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
await sdk.client.session.summarize({
|
||||
sessionID,
|
||||
modelID: model.id,
|
||||
providerID: model.provider.id,
|
||||
})
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.fork",
|
||||
title: language.t("command.session.fork"),
|
||||
description: language.t("command.session.fork.description"),
|
||||
slash: "fork",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: () => dialog.show(() => <DialogFork />),
|
||||
}),
|
||||
])
|
||||
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
||||
if (!clipboard?.writeText) return Promise.resolve(false)
|
||||
return clipboard.writeText(value).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
}
|
||||
|
||||
const copy = async (url: string, existing: boolean) => {
|
||||
const ok = await write(url)
|
||||
if (!ok) {
|
||||
showToast({
|
||||
title: language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
showToast({
|
||||
title: existing
|
||||
? language.t("session.share.copy.copied")
|
||||
: language.t("toast.session.share.success.title"),
|
||||
description: language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
})
|
||||
}
|
||||
|
||||
const existing = info()?.share?.url
|
||||
if (existing) {
|
||||
await copy(existing, true)
|
||||
return
|
||||
}
|
||||
|
||||
const url = await sdk.client.session
|
||||
.share({ sessionID: 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"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await copy(url, false)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.unshare",
|
||||
title: language.t("command.session.unshare"),
|
||||
description: language.t("command.session.unshare.description"),
|
||||
slash: "unshare",
|
||||
disabled: !params.id || !info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
await sdk.client.session
|
||||
.unshare({ sessionID: params.id })
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: language.t("toast.session.unshare.success.title"),
|
||||
description: 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"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const shareCommands = createMemo(() => {
|
||||
if (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"),
|
||||
slash: "share",
|
||||
disabled: !params.id,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
|
||||
const write = (value: string) => {
|
||||
const body = typeof document === "undefined" ? undefined : document.body
|
||||
if (body) {
|
||||
const textarea = document.createElement("textarea")
|
||||
textarea.value = value
|
||||
textarea.setAttribute("readonly", "")
|
||||
textarea.style.position = "fixed"
|
||||
textarea.style.opacity = "0"
|
||||
textarea.style.pointerEvents = "none"
|
||||
body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const copied = document.execCommand("copy")
|
||||
body.removeChild(textarea)
|
||||
if (copied) return Promise.resolve(true)
|
||||
}
|
||||
|
||||
const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
|
||||
if (!clipboard?.writeText) return Promise.resolve(false)
|
||||
return clipboard.writeText(value).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
}
|
||||
|
||||
const copy = async (url: string, existing: boolean) => {
|
||||
const ok = await write(url)
|
||||
if (!ok) {
|
||||
showToast({
|
||||
title: language.t("toast.session.share.copyFailed.title"),
|
||||
variant: "error",
|
||||
})
|
||||
return
|
||||
}
|
||||
id: "session.new",
|
||||
title: language.t("command.session.new"),
|
||||
keybind: "mod+shift+s",
|
||||
slash: "new",
|
||||
onSelect: () => navigate(`/${params.dir}/session`),
|
||||
}),
|
||||
fileCommand({
|
||||
id: "file.open",
|
||||
title: language.t("command.file.open"),
|
||||
description: language.t("palette.search.placeholder"),
|
||||
keybind: "mod+p",
|
||||
slash: "open",
|
||||
onSelect: () => dialog.show(() => <DialogSelectFile onOpenFile={showAllFiles} />),
|
||||
}),
|
||||
fileCommand({
|
||||
id: "tab.close",
|
||||
title: language.t("command.tab.close"),
|
||||
keybind: "mod+w",
|
||||
disabled: !closableTab(),
|
||||
onSelect: () => {
|
||||
const tab = closableTab()
|
||||
if (!tab) return
|
||||
tabs().close(tab)
|
||||
},
|
||||
}),
|
||||
contextCommand({
|
||||
id: "context.addSelection",
|
||||
title: language.t("command.context.addSelection"),
|
||||
description: language.t("command.context.addSelection.description"),
|
||||
keybind: "mod+shift+l",
|
||||
disabled: !canAddSelectionContext(),
|
||||
onSelect: () => {
|
||||
const tab = activeFileTab()
|
||||
if (!tab) return
|
||||
const path = file.pathFromTab(tab)
|
||||
if (!path) return
|
||||
|
||||
const range = file.selectedLines(path) as SelectedLineRange | null | undefined
|
||||
if (!range) {
|
||||
showToast({
|
||||
title: existing
|
||||
? language.t("session.share.copy.copied")
|
||||
: language.t("toast.session.share.success.title"),
|
||||
description: language.t("toast.session.share.success.description"),
|
||||
variant: "success",
|
||||
})
|
||||
}
|
||||
|
||||
const existing = info()?.share?.url
|
||||
if (existing) {
|
||||
await copy(existing, true)
|
||||
return
|
||||
}
|
||||
|
||||
const url = await sdk.client.session
|
||||
.share({ sessionID: 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"),
|
||||
variant: "error",
|
||||
title: language.t("toast.context.noLineSelection.title"),
|
||||
description: language.t("toast.context.noLineSelection.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await copy(url, false)
|
||||
addSelectionToContext(path, selectionFromLines(range))
|
||||
},
|
||||
}),
|
||||
viewCommand({
|
||||
id: "terminal.toggle",
|
||||
title: language.t("command.terminal.toggle"),
|
||||
keybind: "ctrl+`",
|
||||
slash: "terminal",
|
||||
onSelect: () => view().terminal.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "review.toggle",
|
||||
title: language.t("command.review.toggle"),
|
||||
keybind: "mod+shift+r",
|
||||
onSelect: () => view().reviewPanel.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "fileTree.toggle",
|
||||
title: language.t("command.fileTree.toggle"),
|
||||
keybind: "mod+\\",
|
||||
onSelect: () => layout.fileTree.toggle(),
|
||||
}),
|
||||
viewCommand({
|
||||
id: "input.focus",
|
||||
title: language.t("command.input.focus"),
|
||||
keybind: "ctrl+l",
|
||||
onSelect: focusInput,
|
||||
}),
|
||||
terminalCommand({
|
||||
id: "terminal.new",
|
||||
title: language.t("command.terminal.new"),
|
||||
description: language.t("command.terminal.new.description"),
|
||||
keybind: "ctrl+alt+t",
|
||||
onSelect: () => {
|
||||
if (terminal.all().length > 0) terminal.new()
|
||||
view().terminal.open()
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.unshare",
|
||||
title: language.t("command.session.unshare"),
|
||||
description: language.t("command.session.unshare.description"),
|
||||
slash: "unshare",
|
||||
disabled: !params.id || !info()?.share?.url,
|
||||
onSelect: async () => {
|
||||
if (!params.id) return
|
||||
await sdk.client.session
|
||||
.unshare({ sessionID: params.id })
|
||||
.then(() =>
|
||||
showToast({
|
||||
title: language.t("toast.session.unshare.success.title"),
|
||||
description: 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"),
|
||||
variant: "error",
|
||||
}),
|
||||
)
|
||||
id: "message.previous",
|
||||
title: language.t("command.message.previous"),
|
||||
description: language.t("command.message.previous.description"),
|
||||
keybind: "mod+arrowup",
|
||||
disabled: !params.id,
|
||||
onSelect: () => navigateMessageByOffset(-1),
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "message.next",
|
||||
title: language.t("command.message.next"),
|
||||
description: language.t("command.message.next.description"),
|
||||
keybind: "mod+arrowdown",
|
||||
disabled: !params.id,
|
||||
onSelect: () => navigateMessageByOffset(1),
|
||||
}),
|
||||
modelCommand({
|
||||
id: "model.choose",
|
||||
title: language.t("command.model.choose"),
|
||||
description: language.t("command.model.choose.description"),
|
||||
keybind: "mod+'",
|
||||
slash: "model",
|
||||
onSelect: () => dialog.show(() => <DialogSelectModel />),
|
||||
}),
|
||||
mcpCommand({
|
||||
id: "mcp.toggle",
|
||||
title: language.t("command.mcp.toggle"),
|
||||
description: language.t("command.mcp.toggle.description"),
|
||||
keybind: "mod+;",
|
||||
slash: "mcp",
|
||||
onSelect: () => dialog.show(() => <DialogSelectMcp />),
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle",
|
||||
title: language.t("command.agent.cycle"),
|
||||
description: language.t("command.agent.cycle.description"),
|
||||
keybind: "mod+.",
|
||||
slash: "agent",
|
||||
onSelect: () => local.agent.move(1),
|
||||
}),
|
||||
agentCommand({
|
||||
id: "agent.cycle.reverse",
|
||||
title: language.t("command.agent.cycle.reverse"),
|
||||
description: language.t("command.agent.cycle.reverse.description"),
|
||||
keybind: "shift+mod+.",
|
||||
onSelect: () => local.agent.move(-1),
|
||||
}),
|
||||
modelCommand({
|
||||
id: "model.variant.cycle",
|
||||
title: language.t("command.model.variant.cycle"),
|
||||
description: language.t("command.model.variant.cycle.description"),
|
||||
keybind: "shift+mod+d",
|
||||
onSelect: () => local.model.variant.cycle(),
|
||||
}),
|
||||
permissionsCommand({
|
||||
id: "permissions.autoaccept",
|
||||
title: isAutoAcceptActive()
|
||||
? language.t("command.permissions.autoaccept.disable")
|
||||
: language.t("command.permissions.autoaccept.enable"),
|
||||
keybind: "mod+shift+a",
|
||||
disabled: false,
|
||||
onSelect: () => {
|
||||
const sessionID = params.id
|
||||
if (sessionID) permission.toggleAutoAccept(sessionID, sdk.directory)
|
||||
else permission.toggleAutoAcceptDirectory(sdk.directory)
|
||||
|
||||
const active = sessionID
|
||||
? permission.isAutoAccepting(sessionID, sdk.directory)
|
||||
: permission.isAutoAcceptingDirectory(sdk.directory)
|
||||
showToast({
|
||||
title: active
|
||||
? language.t("toast.permissions.autoaccept.on.title")
|
||||
: language.t("toast.permissions.autoaccept.off.title"),
|
||||
description: active
|
||||
? language.t("toast.permissions.autoaccept.on.description")
|
||||
: language.t("toast.permissions.autoaccept.off.description"),
|
||||
})
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.undo",
|
||||
title: language.t("command.session.undo"),
|
||||
description: language.t("command.session.undo.description"),
|
||||
slash: "undo",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
if (status().type !== "idle") {
|
||||
await sdk.client.session.abort({ sessionID }).catch(() => {})
|
||||
}
|
||||
const revert = info()?.revert?.messageID
|
||||
const message = findLast(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]
|
||||
if (parts) {
|
||||
const restored = extractPromptFromParts(parts, { directory: sdk.directory })
|
||||
prompt.set(restored)
|
||||
}
|
||||
const priorMessage = findLast(userMessages(), (x) => x.id < message.id)
|
||||
setActiveMessage(priorMessage)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.redo",
|
||||
title: language.t("command.session.redo"),
|
||||
description: language.t("command.session.redo.description"),
|
||||
slash: "redo",
|
||||
disabled: !params.id || !info()?.revert?.messageID,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const revertMessageID = info()?.revert?.messageID
|
||||
if (!revertMessageID) return
|
||||
const nextMessage = userMessages().find((x) => x.id > revertMessageID)
|
||||
if (!nextMessage) {
|
||||
await sdk.client.session.unrevert({ sessionID })
|
||||
prompt.reset()
|
||||
const lastMsg = findLast(userMessages(), (x) => x.id >= revertMessageID)
|
||||
setActiveMessage(lastMsg)
|
||||
return
|
||||
}
|
||||
await sdk.client.session.revert({ sessionID, messageID: nextMessage.id })
|
||||
const priorMsg = findLast(userMessages(), (x) => x.id < nextMessage.id)
|
||||
setActiveMessage(priorMsg)
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.compact",
|
||||
title: language.t("command.session.compact"),
|
||||
description: language.t("command.session.compact.description"),
|
||||
slash: "compact",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: async () => {
|
||||
const sessionID = params.id
|
||||
if (!sessionID) return
|
||||
const model = local.model.current()
|
||||
if (!model) {
|
||||
showToast({
|
||||
title: language.t("toast.model.none.title"),
|
||||
description: language.t("toast.model.none.description"),
|
||||
})
|
||||
return
|
||||
}
|
||||
await sdk.client.session.summarize({
|
||||
sessionID,
|
||||
modelID: model.id,
|
||||
providerID: model.provider.id,
|
||||
})
|
||||
},
|
||||
}),
|
||||
sessionCommand({
|
||||
id: "session.fork",
|
||||
title: language.t("command.session.fork"),
|
||||
description: language.t("command.session.fork.description"),
|
||||
slash: "fork",
|
||||
disabled: !params.id || visibleUserMessages().length === 0,
|
||||
onSelect: () => dialog.show(() => <DialogFork />),
|
||||
}),
|
||||
...share,
|
||||
]
|
||||
})
|
||||
|
||||
command.register("session", () =>
|
||||
[
|
||||
sessionCommands(),
|
||||
fileCommands(),
|
||||
contextCommands(),
|
||||
viewCommands(),
|
||||
messageCommands(),
|
||||
agentCommands(),
|
||||
permissionCommands(),
|
||||
sessionActionCommands(),
|
||||
shareCommands(),
|
||||
].flatMap((x) => x),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user