Files
opencode/packages/app/src/pages/session/helpers.ts
2026-03-12 11:32:05 -05:00

192 lines
5.1 KiB
TypeScript

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}`)
const terminal = wrapper?.querySelector('[data-component="terminal"]')
if (!(terminal instanceof HTMLElement)) return false
const textarea = terminal.querySelector("textarea")
if (textarea instanceof HTMLTextAreaElement) {
textarea.focus()
return true
}
terminal.focus()
terminal.dispatchEvent(
typeof PointerEvent === "function"
? new PointerEvent("pointerdown", { bubbles: true, cancelable: true })
: new MouseEvent("pointerdown", { bubbles: true, cancelable: true }),
)
return true
}
export const createOpenReviewFile = (input: {
showAllFiles: () => void
tabForPath: (path: string) => string
openTab: (tab: string) => void
setActive: (tab: string) => void
loadFile: (path: string) => any | Promise<void>
}) => {
return (path: string) => {
batch(() => {
input.showAllFiles()
const maybePromise = input.loadFile(path)
const open = () => {
const tab = input.tabForPath(path)
input.openTab(tab)
input.setActive(tab)
}
if (maybePromise instanceof Promise) maybePromise.then(open)
else open()
})
}
}
export const createOpenSessionFileTab = (input: {
normalizeTab: (tab: string) => string
openTab: (tab: string) => void
pathFromTab: (tab: string) => string | undefined
loadFile: (path: string) => void
openReviewPanel: () => void
setActive: (tab: string) => void
}) => {
return (value: string) => {
const next = input.normalizeTab(value)
input.openTab(next)
const path = input.pathFromTab(next)
if (!path) return
input.loadFile(path)
input.openReviewPanel()
input.setActive(next)
}
}
export const getTabReorderIndex = (tabs: readonly string[], from: string, to: string) => {
const fromIndex = tabs.indexOf(from)
const toIndex = tabs.indexOf(to)
if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return undefined
return toIndex
}
export const createSizing = () => {
const [state, setState] = createStore({ active: false })
let t: number | undefined
const stop = () => {
if (t !== undefined) {
clearTimeout(t)
t = undefined
}
setState("active", false)
}
const start = () => {
if (t !== undefined) {
clearTimeout(t)
t = undefined
}
setState("active", true)
}
onMount(() => {
window.addEventListener("pointerup", stop)
window.addEventListener("pointercancel", stop)
window.addEventListener("blur", stop)
onCleanup(() => {
window.removeEventListener("pointerup", stop)
window.removeEventListener("pointercancel", stop)
window.removeEventListener("blur", stop)
})
})
onCleanup(() => {
if (t !== undefined) clearTimeout(t)
})
return {
active: () => state.active,
start,
touch() {
start()
t = window.setTimeout(stop, 120)
},
}
}
export type Sizing = ReturnType<typeof createSizing>