Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 22 additions & 19 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { useSpring } from "@opencode-ai/ui/motion-spring"
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal } from "solid-js"
import { createEffect, on, Component, Show, onCleanup, createMemo, createSignal, createResource } from "solid-js"
import { createStore } from "solid-js/store"
import { useLocal } from "@/context/local"
import { selectionFromLines, type SelectedLineRange, useFile } from "@/context/file"
Expand Down Expand Up @@ -54,7 +54,7 @@ import { PromptImageAttachments } from "./prompt-input/image-attachments"
import { PromptDragOverlay } from "./prompt-input/drag-overlay"
import { promptPlaceholder } from "./prompt-input/placeholder"
import { ImagePreview } from "@opencode-ai/ui/image-preview"
import { useQuery } from "@tanstack/solid-query"
import { useQueries, useQuery } from "@tanstack/solid-query"
import { loadAgentsQuery, loadProvidersQuery } from "@/context/global-sync/bootstrap"

interface PromptInputProps {
Expand Down Expand Up @@ -1252,16 +1252,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}

const agentsQuery = useQuery(() => loadAgentsQuery(sdk.directory))
const agentsLoading = () => agentsQuery.isLoading

const globalProvidersQuery = useQuery(() => loadProvidersQuery(null))
const providersQuery = useQuery(() => loadProvidersQuery(sdk.directory))
const [agentsQuery, globalProvidersQuery, providersQuery] = useQueries(() => ({
queries: [loadAgentsQuery(sdk.directory), loadProvidersQuery(null), loadProvidersQuery(sdk.directory)],
}))

const agentsLoading = () => agentsQuery.isLoading
const providersLoading = () => agentsLoading() || providersQuery.isLoading || globalProvidersQuery.isLoading

const [promptReady] = createResource(
() => prompt.ready().promise,
(p) => p,
)

return (
<div class="relative size-full _max-h-[320px] flex flex-col gap-0">
{(promptReady(), null)}
<PromptPopover
popover={store.popover}
setSlashPopoverRef={(el) => (slashPopoverRef = el)}
Expand Down Expand Up @@ -1358,15 +1363,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}}
style={{ "padding-bottom": space }}
/>
<Show when={!prompt.dirty()}>
<div
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
classList={{ "font-mono!": store.mode === "shell" }}
style={{ "padding-bottom": space }}
>
{placeholder()}
</div>
</Show>
<div
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
classList={{ "font-mono!": store.mode === "shell" }}
style={{ "padding-bottom": space, display: prompt.dirty() ? "none" : undefined }}
>
{placeholder()}
</div>
</div>

<div
Expand Down Expand Up @@ -1457,7 +1460,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</div>
<div class="flex items-center gap-1.5 min-w-0 flex-1 h-7">
<Show when={!agentsLoading()}>
<div data-component="prompt-agent-control">
<div data-component="prompt-agent-control" style={{ animation: "fade-in 0.3s" }}>
<TooltipKeybind
placement="top"
gutter={4}
Expand All @@ -1483,7 +1486,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Show>
<Show when={!providersLoading()}>
<Show when={store.mode !== "shell"}>
<div data-component="prompt-model-control">
<div data-component="prompt-model-control" style={{ animation: "fade-in 0.3s" }}>
<Show
when={providers.paid().length > 0}
fallback={
Expand Down Expand Up @@ -1554,7 +1557,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</TooltipKeybind>
</Show>
</div>
<div data-component="prompt-variant-control">
<div data-component="prompt-variant-control" style={{ animation: "fade-in 0.3s" }}>
<TooltipKeybind
placement="top"
gutter={4}
Expand Down
24 changes: 13 additions & 11 deletions packages/app/src/context/global-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
} from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
import { getFilename } from "@opencode-ai/shared/util/path"
import { createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
import { useLanguage } from "@/context/language"
import { Persist, persisted } from "@/utils/persist"
Expand Down Expand Up @@ -223,16 +223,18 @@ function createGlobalSync() {
limit,
permission: store.permission,
})
setStore(
"sessionTotal",
estimateRootSessionTotal({
count: nonArchived.length,
limit: x.limit,
limited: x.limited,
}),
)
setStore("session", reconcile(sessions, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
batch(() => {
setStore(
"sessionTotal",
estimateRootSessionTotal({
count: nonArchived.length,
limit: x.limit,
limited: x.limited,
}),
)
setStore("session", reconcile(sessions, { key: "id" }))
cleanupDroppedSessionCaches(store, setStore, sessions, setSessionTodo)
})
sessionMeta.set(directory, { limit })
})
.catch((err) => {
Expand Down
144 changes: 79 additions & 65 deletions packages/app/src/context/global-sync/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import type { State, VcsCache } from "./types"
import { cmp, normalizeAgentList, normalizeProviderList } from "./utils"
import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions, skipToken } from "@tanstack/solid-query"
import { loadSessionsQuery } from "../global-sync"

type GlobalStore = {
ready: boolean
Expand Down Expand Up @@ -82,6 +81,9 @@ export async function bootstrapGlobal(input: {
input.setGlobalStore("config", x.data!)
}),
),
]

const slow = [
() =>
input.queryClient.fetchQuery({
...loadProvidersQuery(null),
Expand All @@ -93,9 +95,6 @@ export async function bootstrapGlobal(input: {
}),
),
}),
]

const slow = [
() =>
retry(() =>
input.globalSDK.path.get().then((x) => {
Expand Down Expand Up @@ -183,8 +182,43 @@ function warmSessions(input: {
export const loadProvidersQuery = (directory: string | null) =>
queryOptions<null>({ queryKey: [directory, "providers"], queryFn: skipToken })

export const loadAgentsQuery = (directory: string | null) =>
queryOptions<null>({ queryKey: [directory, "agents"], queryFn: skipToken })
export const loadAgentsQuery = (
directory: string | null,
sdk?: OpencodeClient,
transform?: (x: Awaited<ReturnType<OpencodeClient["app"]["agents"]>>) => void,
) =>
queryOptions<null>({
queryKey: [directory, "agents"],
queryFn:
sdk && transform
? () =>
retry(() =>
sdk.app
.agents()
.then(transform)
.then(() => null),
)
: skipToken,
})

export const loadPathQuery = (
directory: string | null,
sdk?: OpencodeClient,
transform?: (x: Awaited<ReturnType<OpencodeClient["path"]["get"]>>) => void,
) =>
queryOptions<Path>({
queryKey: [directory, "path"],
queryFn:
sdk && transform
? () =>
retry(() =>
sdk.path.get().then(async (x) => {
transform(x)
return x.data!
}),
)
: skipToken,
})

export async function bootstrapDirectory(input: {
directory: string
Expand Down Expand Up @@ -222,45 +256,27 @@ export async function bootstrapDirectory(input: {
input.setStore("lsp", [])
if (loading) input.setStore("status", "partial")

const fast = [() => Promise.resolve(input.loadSessions(input.directory))]

const errs = errors(await runAll(fast))
if (errs.length > 0) {
console.error("Failed to bootstrap instance", errs[0])
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(errs[0], input.translate),
})
}

const rev = (providerRev.get(input.directory) ?? 0) + 1
providerRev.set(input.directory, rev)
;(async () => {
const slow = [
() => Promise.resolve(input.loadSessions(input.directory)),
() =>
input.queryClient.ensureQueryData({
...loadAgentsQuery(input.directory),
queryFn: () =>
retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", normalizeAgentList(x.data)))).then(
() => null,
),
}),
input.queryClient.ensureQueryData(
loadAgentsQuery(input.directory, input.sdk, (x) => input.setStore("agent", normalizeAgentList(x.data))),
),
() => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
() => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
() =>
seededProject
? Promise.resolve()
: retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
() =>
seededPath
? Promise.resolve()
: retry(() =>
input.sdk.path.get().then((x) => {
input.setStore("path", x.data!)
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
if (next) input.setStore("project", next)
}),
),
!seededProject &&
(() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))),
!seededPath &&
(() =>
input.queryClient.ensureQueryData(
loadPathQuery(input.directory, input.sdk, (x) => {
const next = projectID(x.data?.directory ?? input.directory, input.global.project)
if (next) input.setStore("project", next)
}),
)),
() =>
retry(() =>
input.sdk.vcs.get().then((x) => {
Expand Down Expand Up @@ -330,7 +346,28 @@ export async function bootstrapDirectory(input: {
input.setStore("mcp_ready", true)
}),
),
]
() =>
input.queryClient.ensureQueryData({
...loadProvidersQuery(input.directory),
queryFn: () =>
retry(() => input.sdk.provider.list())
.then((x) => {
if (providerRev.get(input.directory) !== rev) return
input.setStore("provider", normalizeProviderList(x.data!))
input.setStore("provider_ready", true)
})
.catch((err) => {
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(err, input.translate),
})
})
.then(() => null),
}),
].filter(Boolean) as (() => Promise<any>)[]

await waitForPaint()
const slowErrs = errors(await runAll(slow))
Expand All @@ -344,29 +381,6 @@ export async function bootstrapDirectory(input: {
})
}

if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")

const rev = (providerRev.get(input.directory) ?? 0) + 1
providerRev.set(input.directory, rev)
void input.queryClient.ensureQueryData({
...loadSessionsQuery(input.directory),
queryFn: () =>
retry(() => input.sdk.provider.list())
.then((x) => {
if (providerRev.get(input.directory) !== rev) return
input.setStore("provider", normalizeProviderList(x.data!))
input.setStore("provider_ready", true)
})
.catch((err) => {
if (providerRev.get(input.directory) !== rev) console.error("Failed to refresh provider list", err)
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(err, input.translate),
})
})
.then(() => null),
})
if (loading && slowErrs.length === 0) input.setStore("status", "complete")
})()
}
10 changes: 9 additions & 1 deletion packages/app/src/context/global-sync/child-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
type VcsCache,
} from "./types"
import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction"
import { useQuery } from "@tanstack/solid-query"
import { loadPathQuery } from "./bootstrap"

export function createChildStoreManager(input: {
owner: Owner
Expand Down Expand Up @@ -156,14 +158,20 @@ export function createChildStoreManager(input: {
createRoot((dispose) => {
const initialMeta = meta[0].value
const initialIcon = icon[0].value

const pathQuery = useQuery(() => loadPathQuery(directory))
const child = createStore<State>({
project: "",
projectMeta: initialMeta,
icon: initialIcon,
provider_ready: false,
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
get path() {
if (pathQuery.isLoading || !pathQuery.data)
return { state: "", config: "", worktree: "", directory: "", home: "" }
return pathQuery.data
},
status: "loading" as const,
agent: [],
command: [],
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/context/prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ function createPromptSession(dir: string, id: string | undefined) {

return {
ready,
current: createMemo(() => store.prompt),
current: () => store.prompt,
cursor: createMemo(() => store.cursor),
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
context: {
items: createMemo(() => store.context.items),
add(item: ContextItem) {
Expand Down Expand Up @@ -277,7 +277,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
const pick = (scope?: Scope) => (scope ? load(scope.dir, scope.id) : session())

return {
ready: () => session().ready(),
ready: () => session().ready,
current: () => session().current(),
cursor: () => session().cursor(),
dirty: () => session().dirty(),
Expand Down
9 changes: 9 additions & 0 deletions packages/app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,13 @@
width: auto;
}
}

@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
Loading
Loading