Explorar el Código

parallel pre-load

Sebastian Herrlinger hace 1 mes
padre
commit
5c95616579
Se han modificado 2 ficheros con 81 adiciones y 57 borrados
  1. 61 49
      packages/opencode/src/cli/cmd/tui/plugin.ts
  2. 20 8
      packages/opencode/src/plugin/index.ts

+ 61 - 49
packages/opencode/src/cli/cmd/tui/plugin.ts

@@ -193,14 +193,14 @@ export namespace TuiPlugin {
           await deps
         }
 
-        const loadOne = async (item: (typeof plugins)[number], retry = false) => {
+        const prep = async (item: (typeof plugins)[number], retry = false) => {
           const spec = Config.pluginSpecifier(item)
           log.info("loading tui plugin", { path: spec, retry })
           const target = await resolvePluginTarget(spec).catch((error) => {
             log.error("failed to resolve tui plugin", { path: spec, retry, error })
             return
           })
-          if (!target) return false
+          if (!target) return
           const meta = await PluginMeta.touch(spec, target).catch((error) => {
             log.warn("failed to track tui plugin", { path: spec, retry, error })
           })
@@ -217,62 +217,74 @@ export namespace TuiPlugin {
 
           const root = pluginRoot(spec, target)
           const install = makeInstallFn(getPluginMeta(config, item), root)
-
           const mod = await import(target).catch((error) => {
             log.error("failed to load tui plugin", { path: spec, retry, error })
             return
           })
-          if (!mod) return false
-
-          for (const [name, entry] of uniqueModuleEntries(mod)) {
-            if (!entry || typeof entry !== "object") {
-              log.warn("ignoring non-object tui plugin export", {
-                path: spec,
-                name,
-                type: entry === null ? "null" : typeof entry,
-              })
-              continue
-            }
+          if (!mod) return
 
-            const slotPlugin = getTuiSlotPlugin(entry)
-            if (slotPlugin) input.slots.register(slotPlugin)
-
-            const tuiPlugin = getTuiPlugin(entry)
-            if (!tuiPlugin) continue
-            await tuiPlugin(
-              {
-                ...input,
-                api: {
-                  command: input.api.command,
-                  route: input.api.route,
-                  ui: input.api.ui,
-                  keybind: input.api.keybind,
-                  theme: Object.create(input.api.theme, {
-                    install: {
-                      value: install,
-                      configurable: true,
-                      enumerable: true,
-                    },
-                  }),
-                },
-              },
-              Config.pluginOptions(item),
-            )
+          return {
+            item,
+            spec,
+            mod,
+            install,
           }
-
-          return true
         }
 
         try {
-          for (const item of plugins) {
-            const ok = await loadOne(item)
-            if (ok) continue
-
-            const spec = Config.pluginSpecifier(item)
-            if (!spec.startsWith("file://")) continue
-
-            await wait()
-            await loadOne(item, true)
+          const loaded = await Promise.all(plugins.map((item) => prep(item)))
+
+          for (let i = 0; i < plugins.length; i++) {
+            let load = loaded[i]
+            if (!load) {
+              const item = plugins[i]
+              if (!item) continue
+              const spec = Config.pluginSpecifier(item)
+              if (!spec.startsWith("file://")) continue
+              await wait()
+              load = await prep(item, true)
+            }
+            if (!load) continue
+
+            // Keep plugin execution sequential for deterministic side effects:
+            // command registration order affects keybind/command precedence,
+            // route registration is last-wins when ids collide,
+            // and hook chains rely on stable plugin ordering.
+            for (const [name, value] of uniqueModuleEntries(load.mod)) {
+              if (!value || typeof value !== "object") {
+                log.warn("ignoring non-object tui plugin export", {
+                  path: load.spec,
+                  name,
+                  type: value === null ? "null" : typeof value,
+                })
+                continue
+              }
+
+              const slotPlugin = getTuiSlotPlugin(value)
+              if (slotPlugin) input.slots.register(slotPlugin)
+
+              const tuiPlugin = getTuiPlugin(value)
+              if (!tuiPlugin) continue
+              await tuiPlugin(
+                {
+                  ...input,
+                  api: {
+                    command: input.api.command,
+                    route: input.api.route,
+                    ui: input.api.ui,
+                    keybind: input.api.keybind,
+                    theme: Object.create(input.api.theme, {
+                      install: {
+                        value: load.install,
+                        configurable: true,
+                        enumerable: true,
+                      },
+                    }),
+                  },
+                },
+                Config.pluginOptions(load.item),
+              )
+            }
           }
         } finally {
           await PluginMeta.persist().catch((error) => {

+ 20 - 8
packages/opencode/src/plugin/index.ts

@@ -82,13 +82,13 @@ export namespace Plugin {
       return value.server
     }
 
-    for (const item of plugins) {
+    const prep = async (item: (typeof plugins)[number]) => {
       const spec = Config.pluginSpecifier(item)
       // ignore old codex plugin since it is supported first party now
-      if (spec.includes("opencode-openai-codex-auth") || spec.includes("opencode-copilot-auth")) continue
+      if (spec.includes("opencode-openai-codex-auth") || spec.includes("opencode-copilot-auth")) return
       log.info("loading plugin", { path: spec })
       const target = await resolvePlugin(spec)
-      if (!target) continue
+      if (!target) return
       const mod = await import(target).catch((err) => {
         const message = err instanceof Error ? err.message : String(err)
         log.error("failed to load plugin", { path: spec, error: message })
@@ -99,23 +99,35 @@ export namespace Plugin {
         })
         return
       })
-      if (!mod) continue
+      if (!mod) return
+      return {
+        item,
+        spec,
+        mod,
+      }
+    }
 
+    const loaded = await Promise.all(plugins.map((item) => prep(item)))
+    for (const load of loaded) {
+      if (!load) continue
+
+      // Keep plugin execution sequential so hook registration and execution
+      // order remains deterministic across plugin runs.
       // Prevent duplicate initialization when plugins export the same function
       // as both a named export and default export (e.g., `export const X` and `export default X`).
       // uniqueModuleEntries keeps only the first export for each shared value reference.
       await (async () => {
-        for (const [, entry] of uniqueModuleEntries(mod)) {
+        for (const [, entry] of uniqueModuleEntries(load.mod)) {
           const server = getServerPlugin(entry)
           if (!server) throw new TypeError("Plugin export is not a function")
-          hooks.push(await server(input, Config.pluginOptions(item)))
+          hooks.push(await server(input, Config.pluginOptions(load.item)))
         }
       })().catch((err) => {
         const message = err instanceof Error ? err.message : String(err)
-        log.error("failed to load plugin", { path: spec, error: message })
+        log.error("failed to load plugin", { path: load.spec, error: message })
         Bus.publish(Session.Event.Error, {
           error: new NamedError.Unknown({
-            message: `Failed to load plugin ${spec}: ${message}`,
+            message: `Failed to load plugin ${load.spec}: ${message}`,
           }).toObject(),
         })
       })