Dax Raad 3 месяцев назад
Родитель
Сommit
4bb7ea9127

+ 10 - 10
bun.lock

@@ -185,8 +185,8 @@
         "@opencode-ai/plugin": "workspace:*",
         "@opencode-ai/script": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
-        "@opentui/core": "0.1.39",
-        "@opentui/solid": "0.1.39",
+        "@opentui/core": "0.0.0-20251108-0c7899b1",
+        "@opentui/solid": "0.0.0-20251108-0c7899b1",
         "@parcel/watcher": "2.5.1",
         "@pierre/precision-diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
@@ -962,21 +962,21 @@
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
-    "@opentui/core": ["@opentui/core@0.1.39", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.39", "@opentui/core-darwin-x64": "0.1.39", "@opentui/core-linux-arm64": "0.1.39", "@opentui/core-linux-x64": "0.1.39", "@opentui/core-win32-arm64": "0.1.39", "@opentui/core-win32-x64": "0.1.39", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-5gPyg3X/8Nr80RfNEJFiMM8Tj01VFfvFwEMCMQrDiOhmSfFXSH2grF/KPl2bnd2Qa13maXWFEl6W3aATObnrnQ=="],
+    "@opentui/core": ["@opentui/core@0.0.0-20251108-0c7899b1", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-darwin-x64": "0.0.0-20251108-0c7899b1", "@opentui/core-linux-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-linux-x64": "0.0.0-20251108-0c7899b1", "@opentui/core-win32-arm64": "0.0.0-20251108-0c7899b1", "@opentui/core-win32-x64": "0.0.0-20251108-0c7899b1", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-uJ7wbVw2v5NnL6g3v72SjPLUwMl2wqOejUEo8t4NeBA8nsboSxggqkrqOYf6OOmCADoAqyFDY7akZMsz6HMZtg=="],
 
-    "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.39", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tDUdNdzGeylkDWTiDIy/CalM/9nIeDwMZGN0Q6FLqABnAplwBhdIH2w/gInAcMaTyagm7Qk88p398Wbnxa9uyg=="],
+    "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251108-0c7899b1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DS9CmFmZZjwe6PIhz6zhZAsDx11DtyMFDxn8V3On2b8G892aBG6rHYtBBnsM28/1GGEJBTeDQ/jUXPVd6FNJ/g=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.39", "", { "os": "darwin", "cpu": "x64" }, "sha512-dWXXNUpdi3ndd+6WotQezsO7g54MLSc/6DmYcl0p7fZrQFct8fX0c9ny/S0xAusNHgBGVS5j5FWE75Mx79301Q=="],
+    "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251108-0c7899b1", "", { "os": "darwin", "cpu": "x64" }, "sha512-K4XwdmT6FTShn7EG8AKliPzO5H59R0XUlZi9+kfRVW59IIJtna5wxbu69SkA28dFoWj5i4yDumwoBI+tI7T6vg=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.39", "", { "os": "linux", "cpu": "arm64" }, "sha512-ookQbxLjsg51iwGb6/KTxCfiVRtE9lSE2OVFLLYork8iVzxg81jX29Uoxe1knZ8FjOJ0+VqTzex2IqQH6mjJlw=="],
+    "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251108-0c7899b1", "", { "os": "linux", "cpu": "arm64" }, "sha512-3JUmxZeSvxV5yU7NEXSecy5Z1/LcVUMy1oWyusZgp96X0CTYAXMrolZt9IJDGO5raeO7JId1UaJmWW0r4DR8TA=="],
 
-    "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.39", "", { "os": "linux", "cpu": "x64" }, "sha512-CeXVNa3hB7gTYKYoZAuMtxWMIXn2rPhmXLkHKpEvXvDRjODFDk8wN1AIVnT5tfncXbWNa5z35BhmqewpGkl4oQ=="],
+    "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251108-0c7899b1", "", { "os": "linux", "cpu": "x64" }, "sha512-i/AQWGyanpPRpk9NK7Ze1tn+d5bqzM9wZFKNB3rd9d2Vbt/ROgBJItG6igz8vzKPKgnlHK4Gw9b5iG5sbjpd+Q=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.39", "", { "os": "win32", "cpu": "arm64" }, "sha512-eeBrVOHz7B+JNZ+w7GH6QxXhXQVBxI6jHmw3B05czG905Je62P0skZNHxiol2BZRawDljo1J/nXQdO5XPeAk2A=="],
+    "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251108-0c7899b1", "", { "os": "win32", "cpu": "arm64" }, "sha512-C7JLWuNN3w2txiVx3demwNwogVi4DQB5ZNHy2b09++kd2m449/RwGPyLcKpuoTzU4s/usYOeY4TxKIAd8cKedQ=="],
 
-    "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.39", "", { "os": "win32", "cpu": "x64" }, "sha512-lLXeQUBg6Wlenauwd+xaBD+0HT4YIcONeZUTHA+Gyd/rqVhxId97rhhzFikp3bBTvNJlYAscJI3yIF2JvRiFNQ=="],
+    "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251108-0c7899b1", "", { "os": "win32", "cpu": "x64" }, "sha512-mpOryp37YaHlTsN70LhiSn9hJJBktbyhlH/eB3N2K7H1ANYQVrekgBJ3rDxlH1GDVtRz6vLS3IDlyK75qNX4pg=="],
 
-    "@opentui/solid": ["@opentui/solid@0.1.39", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.39", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-J34JpWh3HdiDbZajo06WUpd+9CLE/RotVjpVlBE4xtWs9tVMVSUrEZqjI7enoRS/IcCZaeNy3HEREuNA8ng7dw=="],
+    "@opentui/solid": ["@opentui/solid@0.0.0-20251108-0c7899b1", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251108-0c7899b1", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-tcsYnFGH/KBlQNG0IyZE2bisnm5NwN/w7theuWga3L1zoXqZqA5dQHutAVg4zkq5l/YKULeDI4jBlvz0lzH88A=="],
 
     "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
 

+ 2 - 2
packages/opencode/package.json

@@ -54,8 +54,8 @@
     "@opencode-ai/plugin": "workspace:*",
     "@opencode-ai/script": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",
-    "@opentui/core": "0.1.39",
-    "@opentui/solid": "0.1.39",
+    "@opentui/core": "0.0.0-20251108-0c7899b1",
+    "@opentui/solid": "0.0.0-20251108-0c7899b1",
     "@parcel/watcher": "2.5.1",
     "@pierre/precision-diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",

+ 75 - 48
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -1,13 +1,22 @@
 import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
 import { Clipboard } from "@tui/util/clipboard"
 import { TextAttributes } from "@opentui/core"
-import { RouteProvider, useRoute, type Route } from "@tui/context/route"
-import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal } from "solid-js"
+import { RouteProvider, useRoute } from "@tui/context/route"
+import {
+  Switch,
+  Match,
+  createEffect,
+  untrack,
+  ErrorBoundary,
+  createSignal,
+  onMount,
+  batch,
+} from "solid-js"
 import { Installation } from "@/installation"
 import { Global } from "@/global"
 import { DialogProvider, useDialog } from "@tui/ui/dialog"
 import { SDKProvider, useSDK } from "@tui/context/sdk"
-import { SyncProvider } from "@tui/context/sync"
+import { SyncProvider, useSync } from "@tui/context/sync"
 import { LocalProvider, useLocal } from "@tui/context/local"
 import { DialogModel } from "@tui/component/dialog-model"
 import { DialogStatus } from "@tui/component/dialog-status"
@@ -27,6 +36,8 @@ import { ExitProvider, useExit } from "./context/exit"
 import { Session as SessionApi } from "@/session"
 import { TuiEvent } from "./event"
 import { KVProvider, useKV } from "./context/kv"
+import { Provider } from "@/provider/provider"
+import { ArgsProvider, useArgs, type Args } from "./context/args"
 
 async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
   // can't set raw mode if not a TTY
@@ -88,25 +99,10 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
   })
 }
 
-export function tui(input: {
-  url: string
-  sessionID?: string
-  model?: string
-  agent?: string
-  prompt?: string
-  onExit?: () => Promise<void>
-}) {
+export function tui(input: { url: string; args: Args; onExit?: () => Promise<void> }) {
   // promise to prevent immediate exit
   return new Promise<void>(async (resolve) => {
     const mode = await getTerminalBackgroundColor()
-
-    const routeData: Route | undefined = input.sessionID
-      ? {
-          type: "session",
-          sessionID: input.sessionID,
-        }
-      : undefined
-
     const onExit = async () => {
       await input.onExit?.()
       resolve()
@@ -120,35 +116,33 @@ export function tui(input: {
               <ErrorComponent error={error} reset={reset} onExit={onExit} />
             )}
           >
-            <ExitProvider onExit={onExit}>
-              <KVProvider>
-                <ToastProvider>
-                  <RouteProvider data={routeData}>
-                    <SDKProvider url={input.url}>
-                      <SyncProvider>
-                        <ThemeProvider mode={mode}>
-                          <LocalProvider
-                            initialModel={input.model}
-                            initialAgent={input.agent}
-                            initialPrompt={input.prompt}
-                          >
-                            <KeybindProvider>
-                              <DialogProvider>
-                                <CommandProvider>
-                                  <PromptHistoryProvider>
-                                    <App />
-                                  </PromptHistoryProvider>
-                                </CommandProvider>
-                              </DialogProvider>
-                            </KeybindProvider>
-                          </LocalProvider>
-                        </ThemeProvider>
-                      </SyncProvider>
-                    </SDKProvider>
-                  </RouteProvider>
-                </ToastProvider>
-              </KVProvider>
-            </ExitProvider>
+            <ArgsProvider {...input.args}>
+              <ExitProvider onExit={onExit}>
+                <KVProvider>
+                  <ToastProvider>
+                    <RouteProvider>
+                      <SDKProvider url={input.url}>
+                        <SyncProvider>
+                          <ThemeProvider mode={mode}>
+                            <LocalProvider>
+                              <KeybindProvider>
+                                <DialogProvider>
+                                  <CommandProvider>
+                                    <PromptHistoryProvider>
+                                      <App />
+                                    </PromptHistoryProvider>
+                                  </CommandProvider>
+                                </DialogProvider>
+                              </KeybindProvider>
+                            </LocalProvider>
+                          </ThemeProvider>
+                        </SyncProvider>
+                      </SDKProvider>
+                    </RouteProvider>
+                  </ToastProvider>
+                </KVProvider>
+              </ExitProvider>
+            </ArgsProvider>
           </ErrorBoundary>
         )
       },
@@ -174,12 +168,45 @@ function App() {
   const { event } = useSDK()
   const toast = useToast()
   const { theme, mode, setMode } = useTheme()
+  const sync = useSync()
   const exit = useExit()
 
   createEffect(() => {
     console.log(JSON.stringify(route.data))
   })
 
+  const args = useArgs()
+  onMount(() => {
+    batch(() => {
+      if (args.agent) local.agent.set(args.agent)
+      if (args.model) {
+        const { providerID, modelID } = Provider.parseModel(args.model)
+        if (!providerID || !modelID)
+          return toast.show({
+            variant: "warning",
+            message: `Invalid model format: ${args.model}`,
+            duration: 3000,
+          })
+        local.model.set({ providerID, modelID }, { recent: true })
+      }
+      if (args.continue) {
+        const match = sync.data.session.at(-1)?.id
+        if (match) {
+          route.navigate({
+            type: "session",
+            sessionID: match,
+          })
+        }
+      }
+      if (args.sessionID) {
+        route.navigate({
+          type: "session",
+          sessionID: args.sessionID,
+        })
+      }
+    })
+  })
+
   command.register(() => [
     {
       title: "Switch session",

+ 4 - 1
packages/opencode/src/cli/cmd/tui/attach.ts

@@ -17,6 +17,9 @@ export const AttachCommand = cmd({
       }),
   handler: async (args) => {
     if (args.dir) process.chdir(args.dir)
-    await tui(args)
+    await tui({
+      url: args.url,
+      args: {},
+    })
   },
 })

+ 0 - 10
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -200,16 +200,6 @@ export function Prompt(props: PromptProps) {
     input.focus()
   })
 
-  local.setInitialPrompt.listen((initialPrompt) => {
-    batch(() => {
-      setStore("prompt", {
-        input: initialPrompt,
-        parts: [],
-      })
-      input.insertText(initialPrompt)
-    })
-  })
-
   onMount(() => {
     promptPartTypeId = input.extmarks.registerType("prompt-part")
   })

+ 16 - 0
packages/opencode/src/cli/cmd/tui/context/args.tsx

@@ -0,0 +1,16 @@
+import { createSimpleContext } from "./helper"
+
+export interface Args {
+  model?: string
+  agent?: string
+  prompt?: string
+  continue?: boolean
+  sessionID?: string
+}
+
+export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({
+  name: "Args",
+  init: (props: Args) => {
+    return props
+  },
+})

+ 6 - 33
packages/opencode/src/cli/cmd/tui/context/local.tsx

@@ -1,5 +1,5 @@
 import { createStore } from "solid-js/store"
-import { batch, createEffect, createMemo, createSignal, onMount } from "solid-js"
+import { batch, createEffect, createMemo } from "solid-js"
 import { useSync } from "@tui/context/sync"
 import { useTheme } from "@tui/context/theme"
 import { uniqueBy } from "remeda"
@@ -8,12 +8,12 @@ import { Global } from "@/global"
 import { iife } from "@/util/iife"
 import { createSimpleContext } from "./helper"
 import { useToast } from "../ui/toast"
-import { createEventBus } from "@solid-primitives/event-bus"
 import { Provider } from "@/provider/provider"
+import { useArgs } from "./args"
 
 export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
   name: "Local",
-  init: (props: { initialModel?: string; initialAgent?: string; initialPrompt?: string }) => {
+  init: () => {
     const sync = useSync()
     const toast = useToast()
 
@@ -32,25 +32,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       }
     }
 
-    // Set initial model if provided
-    onMount(() => {
-      batch(() => {
-        if (props.initialAgent) {
-          agent.set(props.initialAgent)
-        }
-        if (props.initialModel) {
-          const { providerID, modelID } = Provider.parseModel(props.initialModel)
-          if (!providerID || !modelID)
-            return toast.show({
-              variant: "warning",
-              message: `Invalid model format: ${props.initialModel}`,
-              duration: 3000,
-            })
-          model.set({ providerID, modelID }, { recent: true })
-        }
-      })
-    })
-
     // Automatically update model when agent changes
     createEffect(() => {
       const value = agent.current()
@@ -149,9 +130,10 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           setModelStore("ready", true)
         })
 
+      const args = useArgs()
       const fallbackModel = createMemo(() => {
-        if (props.initialModel) {
-          const { providerID, modelID } = Provider.parseModel(props.initialModel)
+        if (args.model) {
+          const { providerID, modelID } = Provider.parseModel(args.model)
           if (isModelValid({ providerID, modelID })) {
             return {
               providerID,
@@ -251,18 +233,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
       }
     })
 
-    const setInitialPrompt = createEventBus<string>()
-
-    onMount(() => {
-      if (props.initialPrompt) setInitialPrompt.emit(props.initialPrompt)
-    })
-
     const result = {
       model,
       agent,
-      get setInitialPrompt() {
-        return setInitialPrompt
-      },
     }
     return result
   },

+ 6 - 7
packages/opencode/src/cli/cmd/tui/context/route.tsx

@@ -14,14 +14,13 @@ export type Route = HomeRoute | SessionRoute
 
 export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
   name: "Route",
-  init: (props: { data?: Route }) => {
+  init: () => {
     const [store, setStore] = createStore<Route>(
-      props.data ??
-        (process.env["OPENCODE_ROUTE"]
-          ? JSON.parse(process.env["OPENCODE_ROUTE"])
-          : {
-              type: "home",
-            }),
+      process.env["OPENCODE_ROUTE"]
+        ? JSON.parse(process.env["OPENCODE_ROUTE"])
+        : {
+            type: "home",
+          },
     )
 
     return {

+ 5 - 1
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -225,12 +225,16 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
       }
     })
 
+    const now = Date.now()
     // blocking
     Promise.all([
       sdk.client.config.providers().then((x) => setStore("provider", x.data!.providers)),
       sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
       sdk.client.config.get().then((x) => setStore("config", x.data!)),
-    ]).then(() => setStore("ready", true))
+    ]).then(() => {
+      console.log("loaded in " + (Date.now() - now))
+      setStore("ready", true)
+    })
 
     // non-blocking
     Promise.all([

+ 17 - 3
packages/opencode/src/cli/cmd/tui/routes/home.tsx

@@ -1,5 +1,5 @@
-import { Prompt } from "@tui/component/prompt"
-import { createMemo, Match, Show, Switch, type ParentProps } from "solid-js"
+import { Prompt, type PromptRef } from "@tui/component/prompt"
+import { createMemo, Match, onMount, Show, Switch, type ParentProps } from "solid-js"
 import { useTheme } from "@tui/context/theme"
 import { useKeybind } from "../context/keybind"
 import type { KeybindsConfig } from "@opencode-ai/sdk"
@@ -7,6 +7,10 @@ import { Logo } from "../component/logo"
 import { Locale } from "@/util/locale"
 import { useSync } from "../context/sync"
 import { Toast } from "../ui/toast"
+import { useArgs } from "../context/args"
+
+// TODO: what is the best way to do this?
+let once = false
 
 export function Home() {
   const sync = useSync()
@@ -38,6 +42,16 @@ export function Home() {
     </Show>
   )
 
+  let prompt: PromptRef
+  const args = useArgs()
+  onMount(() => {
+    if (once) return
+    if (args.prompt) {
+      prompt.set({ input: args.prompt, parts: [] })
+      once = true
+    }
+  })
+
   return (
     <box
       flexGrow={1}
@@ -55,7 +69,7 @@ export function Home() {
         <HelpRow keybind="agent_cycle">Switch agent</HelpRow>
       </box>
       <box width="100%" maxWidth={75} zIndex={1000} paddingTop={1}>
-        <Prompt hint={Hint} />
+        <Prompt ref={(r) => (prompt = r)} hint={Hint} />
       </box>
       <Toast />
     </box>

+ 34 - 56
packages/opencode/src/cli/cmd/tui/thread.ts

@@ -2,10 +2,9 @@ import { cmd } from "@/cli/cmd/cmd"
 import { tui } from "./app"
 import { Rpc } from "@/util/rpc"
 import { type rpc } from "./worker"
-import { Session } from "@/session"
-import { bootstrap } from "@/cli/bootstrap"
 import path from "path"
 import { UI } from "@/cli/ui"
+import { iife } from "@/util/iife"
 
 declare global {
   const OPENCODE_WORKER_PATH: string
@@ -32,8 +31,8 @@ export const TuiThreadCommand = cmd({
       })
       .option("session", {
         alias: ["s"],
-        describe: "session id to continue",
         type: "string",
+        describe: "session id to continue",
       })
       .option("prompt", {
         alias: ["p"],
@@ -55,12 +54,6 @@ export const TuiThreadCommand = cmd({
         default: "127.0.0.1",
       }),
   handler: async (args) => {
-    const prompt = await (async () => {
-      const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
-      if (!args.prompt) return piped
-      return piped ? piped + "\n" + args.prompt : args.prompt
-    })()
-
     // Resolve relative paths against PWD to preserve behavior when using --cwd flag
     const baseCwd = process.env.PWD ?? process.cwd()
     const cwd = args.project ? path.resolve(baseCwd, args.project) : process.cwd()
@@ -76,56 +69,41 @@ export const TuiThreadCommand = cmd({
       return
     }
 
-    await bootstrap(cwd, async () => {
-      const sessionID = await (async () => {
-        if (args.continue) {
-          const it = Session.list()
-          try {
-            for await (const s of it) {
-              if (s.parentID === undefined) {
-                return s.id
-              }
-            }
-            return
-          } finally {
-            await it.return()
-          }
-        }
-        if (args.session) {
-          return args.session
-        }
-        return undefined
-      })()
-
-      const worker = new Worker(workerPath, {
-        env: Object.fromEntries(
-          Object.entries(process.env).filter(
-            (entry): entry is [string, string] => entry[1] !== undefined,
-          ),
+    const worker = new Worker(workerPath, {
+      env: Object.fromEntries(
+        Object.entries(process.env).filter(
+          (entry): entry is [string, string] => entry[1] !== undefined,
         ),
-      })
-      worker.onerror = console.error
-      const client = Rpc.client<typeof rpc>(worker)
-      process.on("uncaughtException", (e) => {
-        console.error(e)
-      })
-      process.on("unhandledRejection", (e) => {
-        console.error(e)
-      })
-      const server = await client.call("server", {
-        port: args.port,
-        hostname: args.hostname,
-      })
-      await tui({
-        url: server.url,
-        sessionID,
-        model: args.model,
+      ),
+    })
+    worker.onerror = console.error
+    const client = Rpc.client<typeof rpc>(worker)
+    process.on("uncaughtException", (e) => {
+      console.error(e)
+    })
+    process.on("unhandledRejection", (e) => {
+      console.error(e)
+    })
+    const server = await client.call("server", {
+      port: args.port,
+      hostname: args.hostname,
+    })
+    const prompt = await iife(async () => {
+      const piped = !process.stdin.isTTY ? await Bun.stdin.text() : undefined
+      if (!args.prompt) return piped
+      return piped ? piped + "\n" + args.prompt : args.prompt
+    })
+    await tui({
+      url: server.url,
+      args: {
+        continue: args.continue,
+        sessionID: args.session,
         agent: args.agent,
         prompt,
-        onExit: async () => {
-          await client.call("shutdown", undefined)
-        },
-      })
+      },
+      onExit: async () => {
+        await client.call("shutdown", undefined)
+      },
     })
   },
 })

+ 2 - 0
packages/opencode/src/project/bootstrap.ts

@@ -9,9 +9,11 @@ import { Project } from "./project"
 import { Bus } from "../bus"
 import { Command } from "../command"
 import { Instance } from "./instance"
+import { Log } from "@/util/log"
 
 export async function InstanceBootstrap() {
   if (Flag.OPENCODE_EXPERIMENTAL_NO_BOOTSTRAP) return
+  Log.Default.info("bootstrapping", { directory: Instance.directory })
   await Plugin.init()
   Share.init()
   Format.init()

+ 18 - 13
packages/opencode/src/project/instance.ts

@@ -2,6 +2,7 @@ import { Log } from "@/util/log"
 import { Context } from "../util/context"
 import { Project } from "./project"
 import { State } from "./state"
+import { iife } from "@/util/iife"
 
 interface Context {
   directory: string
@@ -9,7 +10,7 @@ interface Context {
   project: Project.Info
 }
 const context = Context.create<Context>("instance")
-const cache = new Map<string, Context>()
+const cache = new Map<string, Promise<Context>>()
 
 export const Instance = {
   async provide<R>(input: {
@@ -19,18 +20,22 @@ export const Instance = {
   }): Promise<R> {
     let existing = cache.get(input.directory)
     if (!existing) {
-      const project = await Project.fromDirectory(input.directory)
-      existing = {
-        directory: input.directory,
-        worktree: project.worktree,
-        project,
-      }
+      existing = iife(async () => {
+        const project = await Project.fromDirectory(input.directory)
+        const ctx = {
+          directory: input.directory,
+          worktree: project.worktree,
+          project,
+        }
+        await context.provide(ctx, async () => {
+          await input.init?.()
+        })
+        return ctx
+      })
+      cache.set(input.directory, existing)
     }
-    return context.provide(existing, async () => {
-      if (!cache.has(input.directory)) {
-        cache.set(input.directory, existing)
-        await input.init?.()
-      }
+    const ctx = await existing
+    return context.provide(ctx, async () => {
       return input.fn()
     })
   },
@@ -52,7 +57,7 @@ export const Instance = {
   },
   async disposeAll() {
     for (const [_key, value] of cache) {
-      await context.provide(value, async () => {
+      await context.provide(await value, async () => {
         await Instance.dispose()
       })
     }

+ 1 - 0
packages/opencode/src/provider/provider.ts

@@ -231,6 +231,7 @@ export namespace Provider {
   }
 
   const state = Instance.state(async () => {
+    using _ = log.time("state")
     const config = await Config.get()
     const database = await ModelsDev.get()
 

+ 7 - 8
packages/opencode/src/server/server.ts

@@ -114,12 +114,13 @@ export namespace Server {
             path: c.req.path,
           })
         }
-        const start = Date.now()
+        const timer = log.time("request", {
+          method: c.req.method,
+          path: c.req.path,
+        })
         await next()
         if (!skipLogging) {
-          log.info("response", {
-            duration: Date.now() - start,
-          })
+          timer.stop()
         }
       })
       .use(async (c, next) => {
@@ -1083,13 +1084,11 @@ export namespace Server {
           },
         }),
         async (c) => {
+          using _ = log.time("providers")
           const providers = await Provider.list().then((x) => mapValues(x, (item) => item.info))
           return c.json({
             providers: Object.values(providers),
-            default: mapValues(
-              providers,
-              (item) => Provider.sort(Object.values(item.models))[0].id,
-            ),
+            default: [],
           })
         },
       )