Просмотр исходного кода

Reapply "fix(app): more startup efficiency (#18985)"

This reverts commit cbe1337f2401066cf33eb9009b597eafb49123ba.
Adam 3 недель назад
Родитель
Сommit
2b0baf97bd

+ 1 - 0
packages/app/src/components/prompt-input.tsx

@@ -572,6 +572,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
       const open = recent()
       const open = recent()
       const seen = new Set(open)
       const seen = new Set(open)
       const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
       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 paths = await files.searchFilesAndDirectories(query)
       const fileOptions: AtOption[] = paths
       const fileOptions: AtOption[] = paths
         .filter((path) => !seen.has(path))
         .filter((path) => !seen.has(path))

+ 189 - 121
packages/app/src/context/global-sync/bootstrap.ts

@@ -31,6 +31,47 @@ type GlobalStore = {
   reload: undefined | "pending" | "complete"
   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: {
 export async function bootstrapGlobal(input: {
   globalSDK: OpencodeClient
   globalSDK: OpencodeClient
   requestFailedTitle: string
   requestFailedTitle: string
@@ -38,45 +79,54 @@ export async function bootstrapGlobal(input: {
   formatMoreCount: (count: number) => string
   formatMoreCount: (count: number) => string
   setGlobalStore: SetStoreFunction<GlobalStore>
   setGlobalStore: SetStoreFunction<GlobalStore>
 }) {
 }) {
-  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 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 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,
-    })
-  }
+  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,
+  })
   input.setGlobalStore("ready", true)
   input.setGlobalStore("ready", true)
 }
 }
 
 
@@ -119,95 +169,113 @@ export async function bootstrapDirectory(input: {
   }
   }
   if (loading) input.setStore("status", "partial")
   if (loading) input.setStore("status", "partial")
 
 
-  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 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 project = getFilename(input.directory)
     const project = getFilename(input.directory)
     showToast({
     showToast({
       variant: "error",
       variant: "error",
       title: input.translate("toast.project.reloadFailed.title", { project }),
       title: input.translate("toast.project.reloadFailed.title", { project }),
-      description: formatServerError(errors[0], input.translate),
+      description: formatServerError(slowErrs[0], input.translate),
     })
     })
-    return
   }
   }
 
 
-  if (loading) input.setStore("status", "complete")
+  if (loading && errs.length === 0 && slowErrs.length === 0) input.setStore("status", "complete")
 }
 }

+ 5 - 2
packages/app/src/context/settings.tsx

@@ -118,8 +118,11 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
 
 
     createEffect(() => {
     createEffect(() => {
       if (typeof document === "undefined") return
       if (typeof document === "undefined") return
-      void loadFont().then((x) => x.ensureMonoFont(store.appearance?.font))
-      document.documentElement.style.setProperty("--font-family-mono", monoFontFamily(store.appearance?.font))
+      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))
     })
     })
 
 
     return {
     return {

+ 4 - 3
packages/app/src/context/sync.tsx

@@ -180,7 +180,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
       return globalSync.child(directory)
       return globalSync.child(directory)
     }
     }
     const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
     const absolute = (path: string) => (current()[0].path.directory + "/" + path).replace("//", "/")
-    const messagePageSize = 200
+    const initialMessagePageSize = 80
+    const historyMessagePageSize = 200
     const inflight = new Map<string, Promise<void>>()
     const inflight = new Map<string, Promise<void>>()
     const inflightDiff = new Map<string, Promise<void>>()
     const inflightDiff = new Map<string, Promise<void>>()
     const inflightTodo = new Map<string, Promise<void>>()
     const inflightTodo = new Map<string, Promise<void>>()
@@ -463,7 +464,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
             const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
             const cached = store.message[sessionID] !== undefined && meta.limit[key] !== undefined
             if (cached && hasSession && !opts?.force) return
             if (cached && hasSession && !opts?.force) return
 
 
-            const limit = meta.limit[key] ?? messagePageSize
+            const limit = meta.limit[key] ?? initialMessagePageSize
             const sessionReq =
             const sessionReq =
               hasSession && !opts?.force
               hasSession && !opts?.force
                 ? Promise.resolve()
                 ? Promise.resolve()
@@ -560,7 +561,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
             const [, setStore] = globalSync.child(directory)
             const [, setStore] = globalSync.child(directory)
             touch(directory, setStore, sessionID)
             touch(directory, setStore, sessionID)
             const key = keyFor(directory, sessionID)
             const key = keyFor(directory, sessionID)
-            const step = count ?? messagePageSize
+            const step = count ?? historyMessagePageSize
             if (meta.loading[key]) return
             if (meta.loading[key]) return
             if (meta.complete[key]) return
             if (meta.complete[key]) return
             const before = meta.cursor[key]
             const before = meta.cursor[key]

+ 8 - 0
packages/app/src/pages/home.tsx

@@ -113,6 +113,14 @@ export default function Home() {
             </ul>
             </ul>
           </div>
           </div>
         </Match>
         </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}>
         <Match when={true}>
           <div class="mt-30 mx-auto flex flex-col items-center gap-3">
           <div class="mt-30 mx-auto flex flex-col items-center gap-3">
             <Icon name="folder-add-left" size="large" />
             <Icon name="folder-add-left" size="large" />

+ 4 - 3
packages/app/src/pages/session.tsx

@@ -1184,8 +1184,6 @@ export default function Page() {
     on(
     on(
       () => sdk.directory,
       () => sdk.directory,
       () => {
       () => {
-        void file.tree.list("")
-
         const tab = activeFileTab()
         const tab = activeFileTab()
         if (!tab) return
         if (!tab) return
         const path = file.pathFromTab(tab)
         const path = file.pathFromTab(tab)
@@ -1640,6 +1638,9 @@ export default function Page() {
     sessionID: () => params.id,
     sessionID: () => params.id,
     messagesReady,
     messagesReady,
     visibleUserMessages,
     visibleUserMessages,
+    historyMore,
+    historyLoading,
+    loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
     turnStart: historyWindow.turnStart,
     turnStart: historyWindow.turnStart,
     currentMessageId: () => store.messageId,
     currentMessageId: () => store.messageId,
     pendingMessage: () => ui.pendingMessage,
     pendingMessage: () => ui.pendingMessage,
@@ -1711,7 +1712,7 @@ export default function Page() {
           <div class="flex-1 min-h-0 overflow-hidden">
           <div class="flex-1 min-h-0 overflow-hidden">
             <Switch>
             <Switch>
               <Match when={params.id}>
               <Match when={params.id}>
-                <Show when={lastUserMessage()}>
+                <Show when={messagesReady()}>
                   <MessageTimeline
                   <MessageTimeline
                     mobileChanges={mobileChanges()}
                     mobileChanges={mobileChanges()}
                     mobileFallback={reviewContent({
                     mobileFallback={reviewContent({

+ 18 - 0
packages/app/src/pages/session/use-session-hash-scroll.ts

@@ -8,6 +8,9 @@ export const useSessionHashScroll = (input: {
   sessionID: () => string | undefined
   sessionID: () => string | undefined
   messagesReady: () => boolean
   messagesReady: () => boolean
   visibleUserMessages: () => UserMessage[]
   visibleUserMessages: () => UserMessage[]
+  historyMore: () => boolean
+  historyLoading: () => boolean
+  loadMore: (sessionID: string) => Promise<void>
   turnStart: () => number
   turnStart: () => number
   currentMessageId: () => string | undefined
   currentMessageId: () => string | undefined
   pendingMessage: () => string | undefined
   pendingMessage: () => string | undefined
@@ -181,6 +184,21 @@ export const useSessionHashScroll = (input: {
     queue(() => scrollToMessage(msg, "auto"))
     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(() => {
   onMount(() => {
     if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
     if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
       window.history.scrollRestoration = "manual"
       window.history.scrollRestoration = "manual"

+ 12 - 0
packages/app/vite.js

@@ -1,7 +1,10 @@
+import { readFileSync } from "node:fs"
 import solidPlugin from "vite-plugin-solid"
 import solidPlugin from "vite-plugin-solid"
 import tailwindcss from "@tailwindcss/vite"
 import tailwindcss from "@tailwindcss/vite"
 import { fileURLToPath } from "url"
 import { fileURLToPath } from "url"
 
 
+const theme = fileURLToPath(new URL("./public/oc-theme-preload.js", import.meta.url))
+
 /**
 /**
  * @type {import("vite").PluginOption}
  * @type {import("vite").PluginOption}
  */
  */
@@ -21,6 +24,15 @@ 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(),
   tailwindcss(),
   solidPlugin(),
   solidPlugin(),
 ]
 ]