summaryrefslogtreecommitdiffhomepage
path: root/packages/app
diff options
context:
space:
mode:
Diffstat (limited to 'packages/app')
-rw-r--r--packages/app/src/components/prompt-input.tsx1
-rw-r--r--packages/app/src/context/global-sync/bootstrap.ts310
-rw-r--r--packages/app/src/context/settings.tsx7
-rw-r--r--packages/app/src/context/sync.tsx7
-rw-r--r--packages/app/src/pages/home.tsx8
-rw-r--r--packages/app/src/pages/session.tsx7
-rw-r--r--packages/app/src/pages/session/use-session-hash-scroll.ts18
-rw-r--r--packages/app/vite.js12
8 files changed, 129 insertions, 241 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index ee98e68cd..f523671ec 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -572,7 +572,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const open = recent()
const seen = new Set(open)
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
- if (!query.trim()) return [...agents, ...pinned]
const paths = await files.searchFilesAndDirectories(query)
const fileOptions: AtOption[] = paths
.filter((path) => !seen.has(path))
diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts
index 47be3abcb..c795ab471 100644
--- a/packages/app/src/context/global-sync/bootstrap.ts
+++ b/packages/app/src/context/global-sync/bootstrap.ts
@@ -31,47 +31,6 @@ type GlobalStore = {
reload: undefined | "pending" | "complete"
}
-function waitForPaint() {
- return new Promise<void>((resolve) => {
- let done = false
- const finish = () => {
- if (done) return
- done = true
- resolve()
- }
- const timer = setTimeout(finish, 50)
- if (typeof requestAnimationFrame !== "function") return
- requestAnimationFrame(() => {
- clearTimeout(timer)
- finish()
- })
- })
-}
-
-function errors(list: PromiseSettledResult<unknown>[]) {
- return list.filter((item): item is PromiseRejectedResult => item.status === "rejected").map((item) => item.reason)
-}
-
-function runAll(list: Array<() => Promise<unknown>>) {
- return Promise.allSettled(list.map((item) => item()))
-}
-
-function showErrors(input: {
- errors: unknown[]
- title: string
- translate: (key: string, vars?: Record<string, string | number>) => string
- formatMoreCount: (count: number) => string
-}) {
- if (input.errors.length === 0) return
- const message = formatServerError(input.errors[0], input.translate)
- const more = input.errors.length > 1 ? input.formatMoreCount(input.errors.length - 1) : ""
- showToast({
- variant: "error",
- title: input.title,
- description: message + more,
- })
-}
-
export async function bootstrapGlobal(input: {
globalSDK: OpencodeClient
requestFailedTitle: string
@@ -79,54 +38,45 @@ export async function bootstrapGlobal(input: {
formatMoreCount: (count: number) => string
setGlobalStore: SetStoreFunction<GlobalStore>
}) {
- const fast = [
- () =>
- retry(() =>
- input.globalSDK.path.get().then((x) => {
- input.setGlobalStore("path", x.data!)
- }),
- ),
- () =>
- retry(() =>
- input.globalSDK.global.config.get().then((x) => {
- input.setGlobalStore("config", x.data!)
- }),
- ),
- () =>
- retry(() =>
- input.globalSDK.provider.list().then((x) => {
- input.setGlobalStore("provider", normalizeProviderList(x.data!))
- }),
- ),
+ const tasks = [
+ retry(() =>
+ input.globalSDK.path.get().then((x) => {
+ input.setGlobalStore("path", x.data!)
+ }),
+ ),
+ retry(() =>
+ input.globalSDK.global.config.get().then((x) => {
+ input.setGlobalStore("config", x.data!)
+ }),
+ ),
+ retry(() =>
+ input.globalSDK.project.list().then((x) => {
+ const projects = (x.data ?? [])
+ .filter((p) => !!p?.id)
+ .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
+ .slice()
+ .sort((a, b) => cmp(a.id, b.id))
+ input.setGlobalStore("project", projects)
+ }),
+ ),
+ retry(() =>
+ input.globalSDK.provider.list().then((x) => {
+ input.setGlobalStore("provider", normalizeProviderList(x.data!))
+ }),
+ ),
]
- const slow = [
- () =>
- retry(() =>
- input.globalSDK.project.list().then((x) => {
- const projects = (x.data ?? [])
- .filter((p) => !!p?.id)
- .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test"))
- .slice()
- .sort((a, b) => cmp(a.id, b.id))
- input.setGlobalStore("project", projects)
- }),
- ),
- ]
-
- showErrors({
- errors: errors(await runAll(fast)),
- title: input.requestFailedTitle,
- translate: input.translate,
- formatMoreCount: input.formatMoreCount,
- })
- await waitForPaint()
- showErrors({
- errors: errors(await runAll(slow)),
- title: input.requestFailedTitle,
- translate: input.translate,
- formatMoreCount: input.formatMoreCount,
- })
+ const results = await Promise.allSettled(tasks)
+ const errors = results.filter((r): r is PromiseRejectedResult => r.status === "rejected").map((r) => r.reason)
+ if (errors.length) {
+ const message = formatServerError(errors[0], input.translate)
+ const more = errors.length > 1 ? input.formatMoreCount(errors.length - 1) : ""
+ showToast({
+ variant: "error",
+ title: input.requestFailedTitle,
+ description: message + more,
+ })
+ }
input.setGlobalStore("ready", true)
}
@@ -169,113 +119,95 @@ export async function bootstrapDirectory(input: {
}
if (loading) input.setStore("status", "partial")
- const fast = [
- () =>
- seededProject
- ? Promise.resolve()
- : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
- () => retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))),
- () => retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
- () =>
- 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)
- }),
- ),
- () => retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
- () =>
- retry(() =>
- input.sdk.vcs.get().then((x) => {
- const next = x.data ?? input.store.vcs
- input.setStore("vcs", next)
- if (next?.branch) input.vcsCache.setStore("value", next)
- }),
- ),
- () => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
- () =>
- retry(() =>
- input.sdk.permission.list().then((x) => {
- const grouped = groupBySession(
- (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
- )
- batch(() => {
- for (const sessionID of Object.keys(input.store.permission)) {
- if (grouped[sessionID]) continue
- input.setStore("permission", sessionID, [])
- }
- for (const [sessionID, permissions] of Object.entries(grouped)) {
- input.setStore(
- "permission",
- sessionID,
- reconcile(
- permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
- { key: "id" },
- ),
- )
- }
- })
- }),
- ),
- () =>
- retry(() =>
- input.sdk.question.list().then((x) => {
- const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
- batch(() => {
- for (const sessionID of Object.keys(input.store.question)) {
- if (grouped[sessionID]) continue
- input.setStore("question", sessionID, [])
- }
- for (const [sessionID, questions] of Object.entries(grouped)) {
- input.setStore(
- "question",
- sessionID,
- reconcile(
- questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
- { key: "id" },
- ),
- )
- }
- })
- }),
- ),
- ]
-
- const slow = [
- () =>
- retry(() =>
- input.sdk.provider.list().then((x) => {
- input.setStore("provider", normalizeProviderList(x.data!))
- }),
- ),
- () => Promise.resolve(input.loadSessions(input.directory)),
- () => retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
- () => retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
- ]
-
- 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),
- })
- }
-
- await waitForPaint()
- const slowErrs = errors(await runAll(slow))
- if (slowErrs.length > 0) {
- console.error("Failed to finish bootstrap instance", slowErrs[0])
+ const results = await Promise.allSettled([
+ seededProject
+ ? Promise.resolve()
+ : retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id)),
+ retry(() =>
+ input.sdk.provider.list().then((x) => {
+ input.setStore("provider", normalizeProviderList(x.data!))
+ }),
+ ),
+ retry(() => input.sdk.app.agents().then((x) => input.setStore("agent", x.data ?? []))),
+ retry(() => input.sdk.config.get().then((x) => input.setStore("config", x.data!))),
+ 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)
+ }),
+ ),
+ retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? []))),
+ retry(() => input.sdk.session.status().then((x) => input.setStore("session_status", x.data!))),
+ input.loadSessions(input.directory),
+ retry(() => input.sdk.mcp.status().then((x) => input.setStore("mcp", x.data!))),
+ retry(() => input.sdk.lsp.status().then((x) => input.setStore("lsp", x.data!))),
+ retry(() =>
+ input.sdk.vcs.get().then((x) => {
+ const next = x.data ?? input.store.vcs
+ input.setStore("vcs", next)
+ if (next?.branch) input.vcsCache.setStore("value", next)
+ }),
+ ),
+ retry(() =>
+ input.sdk.permission.list().then((x) => {
+ const grouped = groupBySession(
+ (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID),
+ )
+ batch(() => {
+ for (const sessionID of Object.keys(input.store.permission)) {
+ if (grouped[sessionID]) continue
+ input.setStore("permission", sessionID, [])
+ }
+ for (const [sessionID, permissions] of Object.entries(grouped)) {
+ input.setStore(
+ "permission",
+ sessionID,
+ reconcile(
+ permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
+ { key: "id" },
+ ),
+ )
+ }
+ })
+ }),
+ ),
+ retry(() =>
+ input.sdk.question.list().then((x) => {
+ const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID))
+ batch(() => {
+ for (const sessionID of Object.keys(input.store.question)) {
+ if (grouped[sessionID]) continue
+ input.setStore("question", sessionID, [])
+ }
+ for (const [sessionID, questions] of Object.entries(grouped)) {
+ input.setStore(
+ "question",
+ sessionID,
+ reconcile(
+ questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
+ { key: "id" },
+ ),
+ )
+ }
+ })
+ }),
+ ),
+ ])
+
+ const errors = results
+ .filter((item): item is PromiseRejectedResult => item.status === "rejected")
+ .map((item) => item.reason)
+ if (errors.length > 0) {
+ console.error("Failed to bootstrap instance", errors[0])
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
- description: formatServerError(slowErrs[0], input.translate),
+ description: formatServerError(errors[0], input.translate),
})
+ return
}
- if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
+ if (loading) input.setStore("status", "complete")
}
diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
index eddd752eb..247d36dd3 100644
--- a/packages/app/src/context/settings.tsx
+++ b/packages/app/src/context/settings.tsx
@@ -118,11 +118,8 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
createEffect(() => {
if (typeof document === "undefined") return
- const id = store.appearance?.font ?? defaultSettings.appearance.font
- if (id !== defaultSettings.appearance.font) {
- void loadFont().then((x) => x.ensureMonoFont(id))
- }
- document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(id))
+ void loadFont().then((x) => x.ensureMonoFont(store.appearance?.font))
+ document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
})
return {
diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx
index bbf4fc5ec..66b889e2a 100644
--- a/packages/app/src/context/sync.tsx
+++ b/packages/app/src/context/sync.tsx
@@ -180,8 +180,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
return globalSync.child(directory)
}
const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
- const initialMessagePageSize = 80
- const historyMessagePageSize = 200
+ const messagePageSize = 200
const inflight = new Map<string, Promise<void>>()
const inflightDiff = new Map<string, Promise<void>>()
const inflightTodo = new Map<string, Promise<void>>()
@@ -464,7 +463,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
if (cached && hasSession && !opts?.force) return
- const limit = meta.limit[key] ?? initialMessagePageSize
+ const limit = meta.limit[key] ?? messagePageSize
const sessionReq =
hasSession && !opts?.force
? Promise.resolve()
@@ -561,7 +560,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
const [, setStore] = globalSync.child(directory)
touch(directory, setStore, sessionID)
const key = keyFor(directory, sessionID)
- const step = count ?? historyMessagePageSize
+ const step = count ?? messagePageSize
if (meta.loading[key]) return
if (meta.complete[key]) return
const before = meta.cursor[key]
diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx
index 4c795b968..ba3a2b942 100644
--- a/packages/app/src/pages/home.tsx
+++ b/packages/app/src/pages/home.tsx
@@ -113,14 +113,6 @@ export default function Home() {
</ul>
</div>
</Match>
- <Match when={!sync.ready}>
- <div class="mt-30 mx-auto flex flex-col items-center gap-3">
- <div class="text-12-regular text-text-weak">{language.t("common.loading")}</div>
- <Button class="px-3" onClick={chooseProject}>
- {language.t("command.project.open")}
- </Button>
- </div>
- </Match>
<Match when={true}>
<div class="mt-30 mx-auto flex flex-col items-center gap-3">
<Icon name="folder-add-left" size="large" />
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 2d3e31355..7a3b476e8 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -1184,6 +1184,8 @@ export default function Page() {
on(
() => sdk.directory,
() => {
+ void file.tree.list("")
+
const tab = activeFileTab()
if (!tab) return
const path = file.pathFromTab(tab)
@@ -1638,9 +1640,6 @@ export default function Page() {
sessionID: () => params.id,
messagesReady,
visibleUserMessages,
- historyMore,
- historyLoading,
- loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
turnStart: historyWindow.turnStart,
currentMessageId: () => store.messageId,
pendingMessage: () => ui.pendingMessage,
@@ -1712,7 +1711,7 @@ export default function Page() {
<div class="flex-1 min-h-0 overflow-hidden">
<Switch>
<Match when={params.id}>
- <Show when={messagesReady()}>
+ <Show when={lastUserMessage()}>
<MessageTimeline
mobileChanges={mobileChanges()}
mobileFallback={reviewContent({
diff --git a/packages/app/src/pages/session/use-session-hash-scroll.ts b/packages/app/src/pages/session/use-session-hash-scroll.ts
index c582749d1..5fadb1f22 100644
--- a/packages/app/src/pages/session/use-session-hash-scroll.ts
+++ b/packages/app/src/pages/session/use-session-hash-scroll.ts
@@ -8,9 +8,6 @@ export const useSessionHashScroll = (input: {
sessionID: () => string | undefined
messagesReady: () => boolean
visibleUserMessages: () => UserMessage[]
- historyMore: () => boolean
- historyLoading: () => boolean
- loadMore: (sessionID: string) => Promise<void>
turnStart: () => number
currentMessageId: () => string | undefined
pendingMessage: () => string | undefined
@@ -184,21 +181,6 @@ export const useSessionHashScroll = (input: {
queue(() => scrollToMessage(msg, "auto"))
})
- createEffect(() => {
- const sessionID = input.sessionID()
- if (!sessionID || !input.messagesReady()) return
-
- visibleUserMessages()
-
- let targetId = input.pendingMessage()
- if (!targetId && !clearing) targetId = messageIdFromHash(location.hash)
- if (!targetId) return
- if (messageById().has(targetId)) return
- if (!input.historyMore() || input.historyLoading()) return
-
- void input.loadMore(sessionID)
- })
-
onMount(() => {
if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
window.history.scrollRestoration = "manual"
diff --git a/packages/app/vite.js b/packages/app/vite.js
index f65a68a1c..6b8fd6137 100644
--- a/packages/app/vite.js
+++ b/packages/app/vite.js
@@ -1,10 +1,7 @@
-import { readFileSync } from "node:fs"
import solidPlugin from "vite-plugin-solid"
import tailwindcss from "@tailwindcss/vite"
import { fileURLToPath } from "url"
-const theme = fileURLToPath(new URL("./public/oc-theme-preload.js", import.meta.url))
-
/**
* @type {import("vite").PluginOption}
*/
@@ -24,15 +21,6 @@ export default [
}
},
},
- {
- name: "opencode-desktop:theme-preload",
- transformIndexHtml(html) {
- return html.replace(
- '<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>',
- `<script id="oc-theme-preload-script">${readFileSync(theme, "utf8")}</script>`,
- )
- },
- },
tailwindcss(),
solidPlugin(),
]