chore: refactor packages/app files (#13236)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Frank <frank@anoma.ly>
This commit is contained in:
@@ -207,6 +207,18 @@ export default function Layout(props: ParentProps) {
|
||||
const setEditor = editor.setEditor
|
||||
const InlineEditor = editor.InlineEditor
|
||||
|
||||
const clearSidebarHoverState = () => {
|
||||
if (layout.sidebar.opened()) return
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
}
|
||||
|
||||
const navigateWithSidebarReset = (href: string) => {
|
||||
clearSidebarHoverState()
|
||||
navigate(href)
|
||||
layout.mobileSidebar.hide()
|
||||
}
|
||||
|
||||
function cycleTheme(direction = 1) {
|
||||
const ids = availableThemeEntries().map(([id]) => id)
|
||||
if (ids.length === 0) return
|
||||
@@ -252,166 +264,167 @@ export default function Layout(props: ParentProps) {
|
||||
setLocale(next)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
||||
const useUpdatePolling = () =>
|
||||
onMount(() => {
|
||||
if (!platform.checkUpdate || !platform.update || !platform.restart) return
|
||||
|
||||
let toastId: number | undefined
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
let toastId: number | undefined
|
||||
let interval: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
async function pollUpdate() {
|
||||
const { updateAvailable, version } = await platform.checkUpdate!()
|
||||
if (updateAvailable && toastId === undefined) {
|
||||
toastId = showToast({
|
||||
persistent: true,
|
||||
icon: "download",
|
||||
title: language.t("toast.update.title"),
|
||||
description: language.t("toast.update.description", { version: version ?? "" }),
|
||||
actions: [
|
||||
{
|
||||
label: language.t("toast.update.action.installRestart"),
|
||||
onClick: async () => {
|
||||
await platform.update!()
|
||||
await platform.restart!()
|
||||
const pollUpdate = () =>
|
||||
platform.checkUpdate!().then(({ updateAvailable, version }) => {
|
||||
if (!updateAvailable) return
|
||||
if (toastId !== undefined) return
|
||||
toastId = showToast({
|
||||
persistent: true,
|
||||
icon: "download",
|
||||
title: language.t("toast.update.title"),
|
||||
description: language.t("toast.update.description", { version: version ?? "" }),
|
||||
actions: [
|
||||
{
|
||||
label: language.t("toast.update.action.installRestart"),
|
||||
onClick: async () => {
|
||||
await platform.update!()
|
||||
await platform.restart!()
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: language.t("toast.update.action.notYet"),
|
||||
onClick: "dismiss",
|
||||
},
|
||||
],
|
||||
{
|
||||
label: language.t("toast.update.action.notYet"),
|
||||
onClick: "dismiss",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!settings.ready()) return
|
||||
createEffect(() => {
|
||||
if (!settings.ready()) return
|
||||
|
||||
if (!settings.updates.startup()) {
|
||||
if (!settings.updates.startup()) {
|
||||
if (interval === undefined) return
|
||||
clearInterval(interval)
|
||||
interval = undefined
|
||||
return
|
||||
}
|
||||
|
||||
if (interval !== undefined) return
|
||||
void pollUpdate()
|
||||
interval = setInterval(pollUpdate, 10 * 60 * 1000)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (interval === undefined) return
|
||||
clearInterval(interval)
|
||||
interval = undefined
|
||||
return
|
||||
}
|
||||
|
||||
if (interval !== undefined) return
|
||||
void pollUpdate()
|
||||
interval = setInterval(pollUpdate, 10 * 60 * 1000)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (interval === undefined) return
|
||||
clearInterval(interval)
|
||||
})
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
const toastBySession = new Map<string, number>()
|
||||
const alertedAtBySession = new Map<string, number>()
|
||||
const cooldownMs = 5000
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
if (e.details?.type === "worktree.ready") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.ready(e.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.details?.type === "worktree.failed") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed"))
|
||||
return
|
||||
}
|
||||
|
||||
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
|
||||
const title =
|
||||
e.details.type === "permission.asked"
|
||||
? language.t("notification.permission.title")
|
||||
: language.t("notification.question.title")
|
||||
const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const)
|
||||
const directory = e.name
|
||||
const props = e.details.properties
|
||||
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
|
||||
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const session = store.session.find((s) => s.id === props.sessionID)
|
||||
const sessionKey = `${directory}:${props.sessionID}`
|
||||
|
||||
const sessionTitle = session?.title ?? language.t("command.session.new")
|
||||
const projectName = getFilename(directory)
|
||||
const description =
|
||||
e.details.type === "permission.asked"
|
||||
? language.t("notification.permission.description", { sessionTitle, projectName })
|
||||
: language.t("notification.question.description", { sessionTitle, projectName })
|
||||
const href = `/${base64Encode(directory)}/session/${props.sessionID}`
|
||||
|
||||
const now = Date.now()
|
||||
const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0
|
||||
if (now - lastAlerted < cooldownMs) return
|
||||
alertedAtBySession.set(sessionKey, now)
|
||||
|
||||
if (e.details.type === "permission.asked") {
|
||||
playSound(soundSrc(settings.sounds.permissions()))
|
||||
if (settings.notifications.permissions()) {
|
||||
void platform.notify(title, description, href)
|
||||
}
|
||||
}
|
||||
|
||||
if (e.details.type === "question.asked") {
|
||||
if (settings.notifications.agent()) {
|
||||
void platform.notify(title, description, href)
|
||||
}
|
||||
}
|
||||
|
||||
const currentSession = params.id
|
||||
if (directory === currentDir() && props.sessionID === currentSession) return
|
||||
if (directory === currentDir() && session?.parentID === currentSession) return
|
||||
|
||||
const existingToastId = toastBySession.get(sessionKey)
|
||||
if (existingToastId !== undefined) toaster.dismiss(existingToastId)
|
||||
|
||||
const toastId = showToast({
|
||||
persistent: true,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
actions: [
|
||||
{
|
||||
label: language.t("notification.action.goToSession"),
|
||||
onClick: () => navigate(href),
|
||||
},
|
||||
{
|
||||
label: language.t("common.dismiss"),
|
||||
onClick: "dismiss",
|
||||
},
|
||||
],
|
||||
})
|
||||
toastBySession.set(sessionKey, toastId)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
createEffect(() => {
|
||||
const currentSession = params.id
|
||||
if (!currentDir() || !currentSession) return
|
||||
const sessionKey = `${currentDir()}:${currentSession}`
|
||||
const toastId = toastBySession.get(sessionKey)
|
||||
if (toastId !== undefined) {
|
||||
const useSDKNotificationToasts = () =>
|
||||
onMount(() => {
|
||||
const toastBySession = new Map<string, number>()
|
||||
const alertedAtBySession = new Map<string, number>()
|
||||
const cooldownMs = 5000
|
||||
|
||||
const dismissSessionAlert = (sessionKey: string) => {
|
||||
const toastId = toastBySession.get(sessionKey)
|
||||
if (toastId === undefined) return
|
||||
toaster.dismiss(toastId)
|
||||
toastBySession.delete(sessionKey)
|
||||
alertedAtBySession.delete(sessionKey)
|
||||
}
|
||||
const [store] = globalSync.child(currentDir(), { bootstrap: false })
|
||||
const childSessions = store.session.filter((s) => s.parentID === currentSession)
|
||||
for (const child of childSessions) {
|
||||
const childKey = `${currentDir()}:${child.id}`
|
||||
const childToastId = toastBySession.get(childKey)
|
||||
if (childToastId !== undefined) {
|
||||
toaster.dismiss(childToastId)
|
||||
toastBySession.delete(childKey)
|
||||
alertedAtBySession.delete(childKey)
|
||||
|
||||
const unsub = globalSDK.event.listen((e) => {
|
||||
if (e.details?.type === "worktree.ready") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.ready(e.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (e.details?.type === "worktree.failed") {
|
||||
setBusy(e.name, false)
|
||||
WorktreeState.failed(e.name, e.details.properties?.message ?? language.t("common.requestFailed"))
|
||||
return
|
||||
}
|
||||
|
||||
if (e.details?.type !== "permission.asked" && e.details?.type !== "question.asked") return
|
||||
const title =
|
||||
e.details.type === "permission.asked"
|
||||
? language.t("notification.permission.title")
|
||||
: language.t("notification.question.title")
|
||||
const icon = e.details.type === "permission.asked" ? ("checklist" as const) : ("bubble-5" as const)
|
||||
const directory = e.name
|
||||
const props = e.details.properties
|
||||
if (e.details.type === "permission.asked" && permission.autoResponds(e.details.properties, directory)) return
|
||||
|
||||
const [store] = globalSync.child(directory, { bootstrap: false })
|
||||
const session = store.session.find((s) => s.id === props.sessionID)
|
||||
const sessionKey = `${directory}:${props.sessionID}`
|
||||
|
||||
const sessionTitle = session?.title ?? language.t("command.session.new")
|
||||
const projectName = getFilename(directory)
|
||||
const description =
|
||||
e.details.type === "permission.asked"
|
||||
? language.t("notification.permission.description", { sessionTitle, projectName })
|
||||
: language.t("notification.question.description", { sessionTitle, projectName })
|
||||
const href = `/${base64Encode(directory)}/session/${props.sessionID}`
|
||||
|
||||
const now = Date.now()
|
||||
const lastAlerted = alertedAtBySession.get(sessionKey) ?? 0
|
||||
if (now - lastAlerted < cooldownMs) return
|
||||
alertedAtBySession.set(sessionKey, now)
|
||||
|
||||
if (e.details.type === "permission.asked") {
|
||||
playSound(soundSrc(settings.sounds.permissions()))
|
||||
if (settings.notifications.permissions()) {
|
||||
void platform.notify(title, description, href)
|
||||
}
|
||||
}
|
||||
|
||||
if (e.details.type === "question.asked") {
|
||||
if (settings.notifications.agent()) {
|
||||
void platform.notify(title, description, href)
|
||||
}
|
||||
}
|
||||
|
||||
const currentSession = params.id
|
||||
if (directory === currentDir() && props.sessionID === currentSession) return
|
||||
if (directory === currentDir() && session?.parentID === currentSession) return
|
||||
|
||||
dismissSessionAlert(sessionKey)
|
||||
|
||||
const toastId = showToast({
|
||||
persistent: true,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
actions: [
|
||||
{
|
||||
label: language.t("notification.action.goToSession"),
|
||||
onClick: () => navigate(href),
|
||||
},
|
||||
{
|
||||
label: language.t("common.dismiss"),
|
||||
onClick: "dismiss",
|
||||
},
|
||||
],
|
||||
})
|
||||
toastBySession.set(sessionKey, toastId)
|
||||
})
|
||||
onCleanup(unsub)
|
||||
|
||||
createEffect(() => {
|
||||
const currentSession = params.id
|
||||
if (!currentDir() || !currentSession) return
|
||||
const sessionKey = `${currentDir()}:${currentSession}`
|
||||
dismissSessionAlert(sessionKey)
|
||||
const [store] = globalSync.child(currentDir(), { bootstrap: false })
|
||||
const childSessions = store.session.filter((s) => s.parentID === currentSession)
|
||||
for (const child of childSessions) {
|
||||
dismissSessionAlert(`${currentDir()}:${child.id}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
useUpdatePolling()
|
||||
useSDKNotificationToasts()
|
||||
|
||||
function scrollToSession(sessionId: string, sessionKey: string) {
|
||||
if (!scrollContainerRef) return
|
||||
@@ -641,6 +654,21 @@ export default function Layout(props: ParentProps) {
|
||||
return created
|
||||
}
|
||||
|
||||
const mergeByID = <T extends { id: string }>(current: T[], incoming: T[]) => {
|
||||
if (current.length === 0) {
|
||||
return incoming.slice().sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
}
|
||||
|
||||
const map = new Map<string, T>()
|
||||
for (const item of current) {
|
||||
map.set(item.id, item)
|
||||
}
|
||||
for (const item of incoming) {
|
||||
map.set(item.id, item)
|
||||
}
|
||||
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
}
|
||||
|
||||
async function prefetchMessages(directory: string, sessionID: string, token: number) {
|
||||
const [store, setStore] = globalSync.child(directory, { bootstrap: false })
|
||||
|
||||
@@ -649,51 +677,24 @@ export default function Layout(props: ParentProps) {
|
||||
if (prefetchToken.value !== token) return
|
||||
|
||||
const items = (messages.data ?? []).filter((x) => !!x?.info?.id)
|
||||
const next = items
|
||||
.map((x) => x.info)
|
||||
.filter((m) => !!m?.id)
|
||||
.slice()
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
const next = items.map((x) => x.info).filter((m): m is Message => !!m?.id)
|
||||
const sorted = mergeByID([], next)
|
||||
|
||||
const current = store.message[sessionID] ?? []
|
||||
const merged = (() => {
|
||||
if (current.length === 0) return next
|
||||
|
||||
const map = new Map<string, Message>()
|
||||
for (const item of current) {
|
||||
if (!item?.id) continue
|
||||
map.set(item.id, item)
|
||||
}
|
||||
for (const item of next) {
|
||||
map.set(item.id, item)
|
||||
}
|
||||
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
})()
|
||||
const merged = mergeByID(
|
||||
current.filter((item): item is Message => !!item?.id),
|
||||
sorted,
|
||||
)
|
||||
|
||||
batch(() => {
|
||||
setStore("message", sessionID, reconcile(merged, { key: "id" }))
|
||||
|
||||
for (const message of items) {
|
||||
const currentParts = store.part[message.info.id] ?? []
|
||||
const mergedParts = (() => {
|
||||
if (currentParts.length === 0) {
|
||||
return message.parts
|
||||
.filter((p) => !!p?.id)
|
||||
.slice()
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
}
|
||||
|
||||
const map = new Map<string, (typeof currentParts)[number]>()
|
||||
for (const item of currentParts) {
|
||||
if (!item?.id) continue
|
||||
map.set(item.id, item)
|
||||
}
|
||||
for (const item of message.parts) {
|
||||
if (!item?.id) continue
|
||||
map.set(item.id, item)
|
||||
}
|
||||
return [...map.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
})()
|
||||
const mergedParts = mergeByID(
|
||||
currentParts.filter((item): item is (typeof currentParts)[number] & { id: string } => !!item?.id),
|
||||
message.parts.filter((item): item is (typeof message.parts)[number] & { id: string } => !!item?.id),
|
||||
)
|
||||
|
||||
setStore("part", message.info.id, reconcile(mergedParts, { key: "id" }))
|
||||
}
|
||||
@@ -1073,24 +1074,14 @@ export default function Layout(props: ParentProps) {
|
||||
|
||||
function navigateToProject(directory: string | undefined) {
|
||||
if (!directory) return
|
||||
if (!layout.sidebar.opened()) {
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
}
|
||||
server.projects.touch(directory)
|
||||
const lastSession = store.lastSession[directory]
|
||||
navigate(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
|
||||
layout.mobileSidebar.hide()
|
||||
navigateWithSidebarReset(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`)
|
||||
}
|
||||
|
||||
function navigateToSession(session: Session | undefined) {
|
||||
if (!session) return
|
||||
if (!layout.sidebar.opened()) {
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
}
|
||||
navigate(`/${base64Encode(session.directory)}/session/${session.id}`)
|
||||
layout.mobileSidebar.hide()
|
||||
navigateWithSidebarReset(`/${base64Encode(session.directory)}/session/${session.id}`)
|
||||
}
|
||||
|
||||
function openProject(directory: string, navigate = true) {
|
||||
@@ -1555,10 +1546,7 @@ export default function Layout(props: ParentProps) {
|
||||
}
|
||||
|
||||
const createWorkspace = async (project: LocalProject) => {
|
||||
if (!layout.sidebar.opened()) {
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
}
|
||||
clearSidebarHoverState()
|
||||
const created = await globalSDK.client.worktree
|
||||
.create({ directory: project.worktree })
|
||||
.then((x) => x.data)
|
||||
@@ -1595,8 +1583,7 @@ export default function Layout(props: ParentProps) {
|
||||
})
|
||||
|
||||
globalSync.child(created.directory)
|
||||
navigate(`/${base64Encode(created.directory)}/session`)
|
||||
layout.mobileSidebar.hide()
|
||||
navigateWithSidebarReset(`/${base64Encode(created.directory)}/session`)
|
||||
}
|
||||
|
||||
const workspaceSidebarCtx: WorkspaceSidebarContext = {
|
||||
@@ -1772,14 +1759,7 @@ export default function Layout(props: ParentProps) {
|
||||
size="large"
|
||||
icon="plus-small"
|
||||
class="w-full"
|
||||
onClick={() => {
|
||||
if (!layout.sidebar.opened()) {
|
||||
setState("hoverSession", undefined)
|
||||
setState("hoverProject", undefined)
|
||||
}
|
||||
navigate(`/${base64Encode(p().worktree)}/session`)
|
||||
layout.mobileSidebar.hide()
|
||||
}}
|
||||
onClick={() => navigateWithSidebarReset(`/${base64Encode(p().worktree)}/session`)}
|
||||
>
|
||||
{language.t("command.session.new")}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user