openAgent
gitlabThe open source UNIVERSAL agent.
clone
https://gitlab.com/bitzzdev/openAgent.gitAIagentautomationopen-sourceuniversal-agent
import { useDialog } from "../ui/dialog"import { DialogSelect } from "../ui/dialog-select"import { useRoute } from "../context/route"import { useSync } from "../context/sync"import { createMemo, createResource, createSignal, onCleanup, onMount } from "solid-js"import path from "path"import { Locale } from "../util/locale"import { useProject } from "../context/project"import { useTheme } from "../context/theme"import { useSDK } from "../context/sdk"import { useLocal } from "../context/local"import { DialogSessionRename } from "./dialog-session-rename"import { createDebouncedSignal } from "../util/signal"import { useToast } from "../ui/toast"import { openWorkspaceSelect, type WorkspaceSelection, warpWorkspaceSession } from "./dialog-workspace-create"import { Spinner } from "./spinner"import { errorMessage } from "../util/error"import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"import { useCommandShortcut } from "../keymap"import { useEvent } from "../context/event"type SessionListFilter = { scope?: "project"; path?: string }export function createDialogSessionListQuery(input: { search?: string; filter: SessionListFilter }) {const search = input.search?.trim()return {roots: true,limit: search ? 30 : 100,...(search ? { search } : {}),...input.filter,}}export function loadDialogSessionList<T>(input: {search?: stringfilter: SessionListFilterlist: (query: ReturnType<typeof createDialogSessionListQuery>) => Promise<{ data?: T[] }>}) {return input.list(createDialogSessionListQuery(input)).then((result) => result.data,() => undefined,)}export function DialogSessionList() {const dialog = useDialog()const route = useRoute()const sync = useSync()const project = useProject()const { theme } = useTheme()const sdk = useSDK()const event = useEvent()const local = useLocal()const toast = useToast()const [toDelete, setToDelete] = createSignal<string>()const [deleted, setDeleted] = createSignal(new Set<string>())const [search, setSearch] = createDebouncedSignal("", 150)const deleteHint = useCommandShortcut("session.delete")const quickSwitch1 = useCommandShortcut("session.quick_switch.1")const quickSwitch9 = useCommandShortcut("session.quick_switch.9")const [browseResults, { refetch: refetchBrowse }] = createResource(() => sync.session.query(),(filter) => loadDialogSessionList({ filter, list: (query) => sdk.client.session.list(query) }),)const [searchResults, { refetch }] = createResource(() => ({ query: search(), filter: sync.session.query() }),(input) => {if (!input.query) return undefinedreturn loadDialogSessionList({search: input.query,filter: input.filter,list: (query) => sdk.client.session.list(query),})},)const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))const sessions = createMemo(() => {const result = searchResults() ?? browseResults() ?? sync.data.sessionconst synced = new Map(sync.data.session.map((session) => [session.id, session]))const ids = new Set(result.map((session) => session.id))const extra = [currentSessionID(), ...local.session.pinned()].flatMap((id) => {if (!id || ids.has(id)) return []const session = synced.get(id)if (session) ids.add(id)return session ? [session] : []})const query = search().trim().toLowerCase()return [...result.map((session) => synced.get(session.id) ?? session), ...extra].filter((session) => !deleted().has(session.id)).filter((session) => !query || session.title.toLowerCase().includes(query))})onCleanup(event.on("session.deleted", (event) => {setDeleted((current) => new Set(current).add(event.properties.info.id))}),)function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {const workspace = project.workspace.get(session.workspaceID!)const list = () => dialog.replace(() => <DialogSessionList />)const warp = async (selection: WorkspaceSelection) => {const workspaceID = await (async () => {if (selection.type === "none") return nullif (selection.type === "existing") return selection.workspaceIDlet resulttry {result = await sdk.client.experimental.workspace.create({ type: selection.workspaceType, branch: null })} catch (err) {toast.show({title: "Failed to create workspace",message: errorMessage(err),variant: "error",})return}const workspace = result?.dataif (!workspace) {toast.show({title: "Failed to create workspace",message: errorMessage(result?.error ?? "no response"),variant: "error",})return}await project.workspace.sync()return workspace.id})()if (workspaceID === undefined) returnawait warpWorkspaceSession({dialog,sdk,sync,project,toast,sourceWorkspaceID: session.workspaceID,workspaceID,sessionID: session.id,copyChanges: false,done: list,})}dialog.replace(() => (<DialogSessionDeleteFailedsession={session.title}workspace={workspace?.name ?? session.workspaceID!}onDone={list}onDelete={async () => {const current = currentSessionID()const info = current ? sync.data.session.find((item) => item.id === current) : undefinedconst result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! })if (result.error) {toast.show({variant: "error",title: "Failed to delete workspace",message: errorMessage(result.error),})return false}await project.workspace.sync()await sync.session.refresh()await refetchBrowse()if (search()) await refetch()if (info?.workspaceID === session.workspaceID) {route.navigate({ type: "home" })}return true}}onRestore={() => {void openWorkspaceSelect({dialog,sdk,sync,project,toast,onSelect: (selection) => {void warp(selection)},})return false}}/>))}function orderByRecency(sessionsList: NonNullable<ReturnType<typeof sessions>>) {return sessionsList.filter((x) => x.parentID === undefined).toSorted((a, b) => b.time.updated - a.time.updated).map((x) => x.id)}const browseOrder = createMemo(() => orderByRecency(browseResults() ?? sync.data.session))const quickSwitchHint = createMemo(() => {const first = quickSwitch1()const last = quickSwitch9()if (!first || !last) return undefinedreturn quickSwitchRange(first, last)})const quickSwitchFooterHints = createMemo(() => {const hint = quickSwitchHint()return hint && local.session.slots().length > 0 ? [{ title: "switch", label: hint }] : []})const options = createMemo(() => {const today = new Date().toDateString()const sessionMap = new Map(sessions().filter((x) => x.parentID === undefined).map((x) => [x.id, x]),)const searchResult = searchResults()const order = searchResult ? orderByRecency(sessions()) : browseOrder()const current = currentSessionID()const displayOrder = current && sessionMap.has(current) && !order.includes(current) ? [...order, current] : orderconst pinned = local.session.pinned().filter((id) => sessionMap.has(id))const pinnedSet = new Set(pinned)const slotByID = new Map<string, number>(local.session.slots().map((id, i) => [id, i + 1]))function buildOption(id: string, category: string) {const x = sessionMap.get(id)if (!x) return undefinedconst directory = x.path? x.directory.endsWith(x.path)? x.directory.slice(0, -x.path.length).replace(/\/$/, ""): undefined: x.directoryconst footer =directory && directory !== project.data.project.mainDir ? Locale.truncate(path.basename(directory), 20) : ""const isDeleting = toDelete() === x.idconst status = sync.data.session_status?.[x.id]const isWorking = status?.type === "busy" || status?.type === "retry"const slot = slotByID.get(x.id)const gutter = isWorking? () => <Spinner />: slot !== undefined? () => <text fg={theme.accent}>{slot}</text>: undefinedreturn {title: isDeleting ? `Press ${deleteHint()} again to confirm` : x.title,bg: isDeleting ? theme.error : undefined,value: x.id,category,footer,gutter,}}const remaining = displayOrder.filter((id) => !pinnedSet.has(id)).map((id) => {const x = sessionMap.get(id)if (!x) return undefinedconst label = new Date(x.time.updated).toDateString()return buildOption(id, label === today ? "Today" : label)}).filter((x) => x !== undefined)return [...pinned.map((id) => buildOption(id, "Pinned")).filter((x) => x !== undefined), ...remaining]})onMount(() => {dialog.setSize("large")})return (<DialogSelecttitle="Sessions"options={options()}skipFilter={true}preserveSelection={true}current={currentSessionID()}onFilter={setSearch}onMove={() => {setToDelete(undefined)}}onSelect={(option) => {route.navigate({type: "session",sessionID: option.value,})dialog.clear()}}actions={[{command: "session.pin.toggle",title: "pin/unpin",onTrigger: (option: { value: string }) => {local.session.togglePin(option.value)},},{command: "session.delete",title: "delete",onTrigger: async (option) => {if (toDelete() === option.value) {const session = sessions().find((item) => item.id === option.value)const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefinedtry {const result = await sdk.client.session.delete({sessionID: option.value,})if (result.error) {if (session?.workspaceID) {recover(session)} else {toast.show({variant: "error",title: "Failed to delete session",message: errorMessage(result.error),})}setToDelete(undefined)return}} catch (err) {if (session?.workspaceID) {recover(session)} else {toast.show({variant: "error",title: "Failed to delete session",message: errorMessage(err),})}setToDelete(undefined)return}if (status && status !== "connected") {await sync.session.refresh()}await refetchBrowse()if (search()) await refetch()setToDelete(undefined)return}setToDelete(option.value)},},{command: "session.rename",title: "rename",onTrigger: async (option) => {dialog.replace(() => <DialogSessionRename session={option.value} />)},},]}footerHints={quickSwitchFooterHints()}/>)}function quickSwitchRange(first: string, last: string) {const prefix = first.slice(0, -1)if (first.endsWith("1") && last === `${prefix}9`) return `${prefix}1-9`return `${first} through ${last}`}