summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-11-08 20:17:46 -0500
committerDax Raad <[email protected]>2025-11-08 20:18:36 -0500
commit4bb7ea91271e85621a1394c70867ddc26be45631 (patch)
tree505f03aa7aff81270df6a4f18b9b901576e51041
parent7af338045563e0b06ebf150972ebe123bd9dee27 (diff)
downloadopencode-4bb7ea91271e85621a1394c70867ddc26be45631.tar.gz
opencode-4bb7ea91271e85621a1394c70867ddc26be45631.zip
improve startup speed
-rw-r--r--bun.lock20
-rw-r--r--packages/opencode/package.json4
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx123
-rw-r--r--packages/opencode/src/cli/cmd/tui/attach.ts5
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx10
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/args.tsx16
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/local.tsx39
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/route.tsx13
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/sync.tsx6
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/home.tsx20
-rw-r--r--packages/opencode/src/cli/cmd/tui/thread.ts90
-rw-r--r--packages/opencode/src/project/bootstrap.ts2
-rw-r--r--packages/opencode/src/project/instance.ts31
-rw-r--r--packages/opencode/src/provider/provider.ts1
-rw-r--r--packages/opencode/src/server/server.ts15
15 files changed, 203 insertions, 192 deletions
diff --git a/bun.lock b/bun.lock
index 9bed68120..5e9dea3ad 100644
--- a/bun.lock
+++ b/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/[email protected]", "", { "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/[email protected]", "", { "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/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tDUdNdzGeylkDWTiDIy/CalM/9nIeDwMZGN0Q6FLqABnAplwBhdIH2w/gInAcMaTyagm7Qk88p398Wbnxa9uyg=="],
+ "@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DS9CmFmZZjwe6PIhz6zhZAsDx11DtyMFDxn8V3On2b8G892aBG6rHYtBBnsM28/1GGEJBTeDQ/jUXPVd6FNJ/g=="],
- "@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-dWXXNUpdi3ndd+6WotQezsO7g54MLSc/6DmYcl0p7fZrQFct8fX0c9ny/S0xAusNHgBGVS5j5FWE75Mx79301Q=="],
+ "@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-K4XwdmT6FTShn7EG8AKliPzO5H59R0XUlZi9+kfRVW59IIJtna5wxbu69SkA28dFoWj5i4yDumwoBI+tI7T6vg=="],
- "@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-ookQbxLjsg51iwGb6/KTxCfiVRtE9lSE2OVFLLYork8iVzxg81jX29Uoxe1knZ8FjOJ0+VqTzex2IqQH6mjJlw=="],
+ "@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-3JUmxZeSvxV5yU7NEXSecy5Z1/LcVUMy1oWyusZgp96X0CTYAXMrolZt9IJDGO5raeO7JId1UaJmWW0r4DR8TA=="],
- "@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-CeXVNa3hB7gTYKYoZAuMtxWMIXn2rPhmXLkHKpEvXvDRjODFDk8wN1AIVnT5tfncXbWNa5z35BhmqewpGkl4oQ=="],
+ "@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-i/AQWGyanpPRpk9NK7Ze1tn+d5bqzM9wZFKNB3rd9d2Vbt/ROgBJItG6igz8vzKPKgnlHK4Gw9b5iG5sbjpd+Q=="],
- "@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-eeBrVOHz7B+JNZ+w7GH6QxXhXQVBxI6jHmw3B05czG905Je62P0skZNHxiol2BZRawDljo1J/nXQdO5XPeAk2A=="],
+ "@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-C7JLWuNN3w2txiVx3demwNwogVi4DQB5ZNHy2b09++kd2m449/RwGPyLcKpuoTzU4s/usYOeY4TxKIAd8cKedQ=="],
- "@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-lLXeQUBg6Wlenauwd+xaBD+0HT4YIcONeZUTHA+Gyd/rqVhxId97rhhzFikp3bBTvNJlYAscJI3yIF2JvRiFNQ=="],
+ "@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-mpOryp37YaHlTsN70LhiSn9hJJBktbyhlH/eB3N2K7H1ANYQVrekgBJ3rDxlH1GDVtRz6vLS3IDlyK75qNX4pg=="],
- "@opentui/solid": ["@opentui/[email protected]", "", { "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/[email protected]", "", { "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=="],
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index c6b03e65e..b1268a101 100644
--- a/packages/opencode/package.json
+++ b/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",
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 9d30ed6d8..3dfbd376c 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/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",
diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts
index 38f1b6719..7da6507ea 100644
--- a/packages/opencode/src/cli/cmd/tui/attach.ts
+++ b/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: {},
+ })
},
})
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index b9e406598..ade7190fb 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/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")
})
diff --git a/packages/opencode/src/cli/cmd/tui/context/args.tsx b/packages/opencode/src/cli/cmd/tui/context/args.tsx
new file mode 100644
index 000000000..055c7af41
--- /dev/null
+++ b/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
+ },
+})
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index ef26ee656..6f91d6a3f 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/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
},
diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx
index dd8ede156..b906de99b 100644
--- a/packages/opencode/src/cli/cmd/tui/context/route.tsx
+++ b/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 {
diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx
index 60758aeb1..8f6d118ed 100644
--- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx
+++ b/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([
diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
index a01bfa6a2..57aff585c 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx
+++ b/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>
diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts
index c530c8884..90c31e6fd 100644
--- a/packages/opencode/src/cli/cmd/tui/thread.ts
+++ b/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)
+ },
})
},
})
diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts
index 27451d225..e801d3f78 100644
--- a/packages/opencode/src/project/bootstrap.ts
+++ b/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()
diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts
index ce9a57d17..3809758ee 100644
--- a/packages/opencode/src/project/instance.ts
+++ b/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()
})
}
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 2ed072e51..f5b4da158 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/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()
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index bfe804ae1..460a99ff4 100644
--- a/packages/opencode/src/server/server.ts
+++ b/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: [],
})
},
)