diff --git a/.github/last-synced-tag b/.github/last-synced-tag
index 19f5e1b57ed..a829bcbe438 100644
--- a/.github/last-synced-tag
+++ b/.github/last-synced-tag
@@ -1 +1 @@
-v1.1.12
+v1.1.13
diff --git a/.gitignore b/.gitignore
index 76ec47537d7..41e6625a036 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ docker/workspace
opencode-dev
logs/
.loop*
+*.bun-build
diff --git a/bun.lock b/bun.lock
index 8da6739c0d9..8c6317dcd28 100644
--- a/bun.lock
+++ b/bun.lock
@@ -22,7 +22,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -71,7 +71,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -100,7 +100,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -127,7 +127,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -151,7 +151,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -175,7 +175,7 @@
},
"packages/desktop": {
"name": "@shuvcode/desktop",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -204,7 +204,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -233,7 +233,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -249,7 +249,7 @@
},
"packages/opencode": {
"name": "opencode",
- "version": "1.1.12",
+ "version": "1.1.13",
"bin": {
"opencode": "./bin/opencode",
},
@@ -353,7 +353,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -373,7 +373,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
- "version": "1.1.12",
+ "version": "1.1.13",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -384,7 +384,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -397,7 +397,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -436,7 +436,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"zod": "catalog:",
},
@@ -447,7 +447,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
diff --git a/packages/app/package.json b/packages/app/package.json
index 53d87c132c0..4c6bc1d0a59 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
- "version": "1.1.12",
+ "version": "1.1.13",
"description": "",
"type": "module",
"exports": {
diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
index 7c13e40dee5..ef24e81f176 100644
--- a/packages/app/src/app.tsx
+++ b/packages/app/src/app.tsx
@@ -34,7 +34,7 @@ const Loading = () =>
localhost (same as upstream + shuv.ai)
+ // 2. Configured server URL (from desktop settings)
+ if (window.__OPENCODE__?.serverUrl) return window.__OPENCODE__.serverUrl
+
+ // 3. Known production hosts -> localhost (same as upstream + shuv.ai)
if (location.hostname.includes("opencode.ai") || location.hostname.includes("shuv.ai"))
return "http://localhost:4096"
- // 3. Desktop app (Tauri) with injected port
+ // 4. Desktop app (Tauri) with injected port
if (window.__SHUVCODE__?.port) return `http://127.0.0.1:${window.__SHUVCODE__.port}`
if (window.__OPENCODE__?.port) return `http://127.0.0.1:${window.__OPENCODE__.port}`
- // 4. Dev mode -> same-origin so Vite proxy handles LAN access + CORS
- if (import.meta.env.DEV) return window.location.origin
+ // 5. Dev mode -> same-origin so Vite proxy handles LAN access + CORS
+ if (import.meta.env.DEV) {
+ return `http://${import.meta.env.VITE_OPENCODE_SERVER_HOST ?? "localhost"}:${import.meta.env.VITE_OPENCODE_SERVER_PORT ?? "4096"}`
+ }
- // 5. Default -> same origin (production web command)
+ // 6. Default -> same origin (production web command)
return window.location.origin
})
diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx
new file mode 100644
index 00000000000..472a1994f13
--- /dev/null
+++ b/packages/app/src/components/dialog-fork.tsx
@@ -0,0 +1,99 @@
+import { Component, createMemo } from "solid-js"
+import { useNavigate, useParams } from "@solidjs/router"
+import { useSync } from "@/context/sync"
+import { useSDK } from "@/context/sdk"
+import { usePrompt } from "@/context/prompt"
+import { useDialog } from "@opencode-ai/ui/context/dialog"
+import { Dialog } from "@opencode-ai/ui/dialog"
+import { List } from "@opencode-ai/ui/list"
+import { extractPromptFromParts } from "@/utils/prompt"
+import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client"
+import { base64Encode } from "@opencode-ai/util/encode"
+
+interface ForkableMessage {
+ id: string
+ text: string
+ time: string
+}
+
+function formatTime(date: Date): string {
+ return date.toLocaleTimeString(undefined, { timeStyle: "short" })
+}
+
+export const DialogFork: Component = () => {
+ const params = useParams()
+ const navigate = useNavigate()
+ const sync = useSync()
+ const sdk = useSDK()
+ const prompt = usePrompt()
+ const dialog = useDialog()
+
+ const messages = createMemo((): ForkableMessage[] => {
+ const sessionID = params.id
+ if (!sessionID) return []
+
+ const msgs = sync.data.message[sessionID] ?? []
+ const result: ForkableMessage[] = []
+
+ for (const message of msgs) {
+ if (message.role !== "user") continue
+
+ const parts = sync.data.part[message.id] ?? []
+ const textPart = parts.find((x): x is SDKTextPart => x.type === "text" && !x.synthetic && !x.ignored)
+ if (!textPart) continue
+
+ result.push({
+ id: message.id,
+ text: textPart.text.replace(/\n/g, " ").slice(0, 200),
+ time: formatTime(new Date(message.time.created)),
+ })
+ }
+
+ return result.reverse()
+ })
+
+ const handleSelect = (item: ForkableMessage | undefined) => {
+ if (!item) return
+
+ const sessionID = params.id
+ if (!sessionID) return
+
+ const parts = sync.data.part[item.id] ?? []
+ const restored = extractPromptFromParts(parts, { directory: sdk.directory })
+
+ dialog.close()
+
+ sdk.client.session.fork({ sessionID, messageID: item.id }).then((forked) => {
+ if (!forked.data) return
+ navigate(`/${base64Encode(sdk.directory)}/session/${forked.data.id}`)
+ requestAnimationFrame(() => {
+ prompt.set(restored)
+ })
+ })
+ }
+
+ return (
+
+ )
+}
diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx
index 6d224c6c3f3..7e2bcc181ad 100644
--- a/packages/app/src/components/dialog-select-server.tsx
+++ b/packages/app/src/components/dialog-select-server.tsx
@@ -1,4 +1,4 @@
-import { createEffect, createMemo, onCleanup } from "solid-js"
+import { createResource, createEffect, createMemo, onCleanup, Show } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
@@ -35,6 +35,8 @@ export function DialogSelectServer() {
error: "",
status: {} as Record
,
})
+ const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.())
+ const isDesktop = platform.platform === "desktop"
const items = createMemo(() => {
const current = server.url
@@ -173,6 +175,53 @@ export function DialogSelectServer() {
+
+
+
+
+
Default server
+
+ Connect to this server on app launch instead of starting a local server. Requires restart.
+
+
+
+
No server selected}
+ >
+
+
+ }
+ >
+
+ {serverDisplayName(defaultUrl()!)}
+
+
+
+
+
+
)
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index c257a93d045..0788e0945b7 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -386,6 +386,7 @@ export const PromptInput: Component = (props) => {
const {
flat: atFlat,
active: atActive,
+ setActive: setAtActive,
onInput: atOnInput,
onKeyDown: atOnKeyDown,
} = useFilteredList({
@@ -463,6 +464,7 @@ export const PromptInput: Component = (props) => {
const {
flat: slashFlat,
active: slashActive,
+ setActive: setSlashActive,
onInput: slashOnInput,
onKeyDown: slashOnKeyDown,
refetch: slashRefetch,
@@ -1313,6 +1315,7 @@ export const PromptInput: Component = (props) => {
class="absolute inset-x-0 -top-3 -translate-y-full origin-bottom-left max-h-80 min-h-10
overflow-auto no-scrollbar flex flex-col p-2 rounded-md
border border-border-base bg-surface-raised-stronger-non-alpha shadow-md"
+ onMouseDown={(e) => e.preventDefault()}
>
@@ -1327,10 +1330,8 @@ export const PromptInput: Component = (props) => {
"w-full flex items-center gap-x-2 rounded-md px-2 py-0.5": true,
"bg-surface-raised-base-hover": atActive() === atKey(item),
}}
- onMouseDown={(e) => {
- e.preventDefault()
- handleAtSelect(item)
- }}
+ onClick={() => handleAtSelect(item)}
+ onMouseEnter={() => setAtActive(atKey(item))}
>
= (props) => {
"w-full flex items-center justify-between gap-4 rounded-md px-2 py-1": true,
"bg-surface-raised-base-hover": slashActive() === cmd.id,
}}
- onMouseDown={(e) => {
- e.preventDefault()
- handleSlashSelect(cmd)
- }}
+ onClick={() => handleSlashSelect(cmd)}
+ onMouseEnter={() => setSlashActive(cmd.id)}
>
/{cmd.trigger}
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx
index aba1e515c46..898dca3a523 100644
--- a/packages/app/src/context/platform.tsx
+++ b/packages/app/src/context/platform.tsx
@@ -47,6 +47,12 @@ export type Platform = {
/** Fetch override */
fetch?: typeof fetch
+
+ /** Get the configured default server URL (desktop only) */
+ getDefaultServerUrl?(): Promise
+
+ /** Set the default server URL to use on app startup (desktop only) */
+ setDefaultServerUrl?(url: string | null): Promise
}
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
index 06c37b59291..8945cd37e9e 100644
--- a/packages/app/src/context/server.tsx
+++ b/packages/app/src/context/server.tsx
@@ -16,10 +16,7 @@ export function normalizeServerUrl(input: string) {
export function serverDisplayName(url: string) {
if (!url) return ""
- return url
- .replace(/^https?:\/\//, "")
- .replace(/\/+$/, "")
- .split("/")[0]
+ return url.replace(/^https?:\/\//, "").replace(/\/+$/, "")
}
function projectsKey(url: string) {
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 873b8fc1a55..af286c229dd 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -31,6 +31,7 @@ import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectFile } from "@/components/dialog-select-file"
import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectMcp } from "@/components/dialog-select-mcp"
+import { DialogFork } from "@/components/dialog-fork"
import { useCommand } from "@/context/command"
import { useNavigate, useParams } from "@solidjs/router"
import { UserMessage } from "@opencode-ai/sdk/v2"
@@ -696,6 +697,15 @@ export default function Page() {
})
},
},
+ {
+ id: "session.fork",
+ title: "Fork from message",
+ description: "Create a new session from a previous message",
+ category: "Session",
+ slash: "fork",
+ disabled: !params.id || visibleUserMessages().length === 0,
+ onSelect: () => dialog.show(() => ),
+ },
])
const handleKeyDown = (event: KeyboardEvent) => {
diff --git a/packages/console/app/package.json b/packages/console/app/package.json
index 05fb0c8bffd..6bd1ba0771f 100644
--- a/packages/console/app/package.json
+++ b/packages/console/app/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
- "version": "1.1.12",
+ "version": "1.1.13",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/console/core/package.json b/packages/console/core/package.json
index 074932a91fd..5a30d55771c 100644
--- a/packages/console/core/package.json
+++ b/packages/console/core/package.json
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
- "version": "1.1.12",
+ "version": "1.1.13",
"private": true,
"type": "module",
"license": "MIT",
diff --git a/packages/console/core/script/lookup-user.ts b/packages/console/core/script/lookup-user.ts
index 15cbe80f8a7..b06fb5654c3 100644
--- a/packages/console/core/script/lookup-user.ts
+++ b/packages/console/core/script/lookup-user.ts
@@ -41,8 +41,8 @@ if (identifier.startsWith("wrk_")) {
subscribed: SubscriptionTable.timeCreated,
})
.from(UserTable)
- .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
- .innerJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
+ .rightJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
+ .leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
.where(eq(UserTable.accountID, accountID))
.then((rows) =>
rows.map((row) => ({
@@ -113,6 +113,8 @@ async function printWorkspace(workspaceID: string) {
.select({
balance: BillingTable.balance,
customerID: BillingTable.customerID,
+ subscriptionID: BillingTable.subscriptionID,
+ subscriptionCouponID: BillingTable.subscriptionCouponID,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, workspace.id))
@@ -149,6 +151,7 @@ async function printWorkspace(workspaceID: string) {
),
)
+ /*
await printTable("Usage", (tx) =>
tx
.select({
@@ -174,6 +177,7 @@ async function printWorkspace(workspaceID: string) {
})),
),
)
+ */
}
function formatMicroCents(value: number | null | undefined) {
diff --git a/packages/console/core/script/remove-black.ts b/packages/console/core/script/remove-black.ts
new file mode 100644
index 00000000000..0803c8f8330
--- /dev/null
+++ b/packages/console/core/script/remove-black.ts
@@ -0,0 +1,78 @@
+import { Billing } from "../src/billing.js"
+import { and, Database, eq } from "../src/drizzle/index.js"
+import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
+
+const workspaceID = process.argv[2]
+
+if (!workspaceID) {
+ console.error("Usage: bun remove-black.ts ")
+ process.exit(1)
+}
+
+console.log(`Removing subscription from workspace ${workspaceID}`)
+
+// Look up the workspace billing
+const billing = await Database.use((tx) =>
+ tx
+ .select({
+ customerID: BillingTable.customerID,
+ subscriptionID: BillingTable.subscriptionID,
+ })
+ .from(BillingTable)
+ .where(eq(BillingTable.workspaceID, workspaceID))
+ .then((rows) => rows[0]),
+)
+
+if (!billing) {
+ console.error(`Error: No billing record found for workspace ${workspaceID}`)
+ process.exit(1)
+}
+
+if (!billing.subscriptionID) {
+ console.error(`Error: Workspace ${workspaceID} does not have a subscription`)
+ process.exit(1)
+}
+
+console.log(` Customer ID: ${billing.customerID}`)
+console.log(` Subscription ID: ${billing.subscriptionID}`)
+
+// Clear workspaceID from Stripe customer metadata
+if (billing.customerID) {
+ //await Billing.stripe().customers.update(billing.customerID, {
+ // metadata: {
+ // workspaceID: "",
+ // },
+ //})
+ //console.log(`Cleared workspaceID from Stripe customer metadata`)
+}
+
+await Database.transaction(async (tx) => {
+ // Clear subscription-related fields from billing table
+ await tx
+ .update(BillingTable)
+ .set({
+ // customerID: null,
+ subscriptionID: null,
+ subscriptionCouponID: null,
+ // paymentMethodID: null,
+ // paymentMethodLast4: null,
+ // paymentMethodType: null,
+ })
+ .where(eq(BillingTable.workspaceID, workspaceID))
+
+ // Delete from subscription table
+ await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
+
+ // Delete from payments table
+ await tx
+ .delete(PaymentTable)
+ .where(
+ and(
+ eq(PaymentTable.workspaceID, workspaceID),
+ eq(PaymentTable.enrichment, { type: "subscription" }),
+ eq(PaymentTable.amount, 20000000000),
+ ),
+ )
+})
+
+console.log(`Successfully removed subscription from workspace ${workspaceID}`)
diff --git a/packages/console/function/package.json b/packages/console/function/package.json
index 33d1f5b3a1b..5e80e0670b4 100644
--- a/packages/console/function/package.json
+++ b/packages/console/function/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
- "version": "1.1.12",
+ "version": "1.1.13",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json
index ea7ac4f5671..dcc7c61b181 100644
--- a/packages/console/mail/package.json
+++ b/packages/console/mail/package.json
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
- "version": "1.1.12",
+ "version": "1.1.13",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
diff --git a/packages/desktop/package.json b/packages/desktop/package.json
index 1b151f183fe..62bb28d303c 100644
--- a/packages/desktop/package.json
+++ b/packages/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@shuvcode/desktop",
"private": true,
- "version": "1.1.12",
+ "version": "1.1.13",
"type": "module",
"license": "MIT",
"scripts": {
diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock
index c533bf9e95d..92953ea19ca 100644
--- a/packages/desktop/src-tauri/Cargo.lock
+++ b/packages/desktop/src-tauri/Cargo.lock
@@ -2795,6 +2795,7 @@ dependencies = [
"futures",
"gtk",
"listeners",
+ "reqwest",
"semver",
"serde",
"serde_json",
diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml
index 3c898319a68..746651d1886 100644
--- a/packages/desktop/src-tauri/Cargo.toml
+++ b/packages/desktop/src-tauri/Cargo.toml
@@ -38,6 +38,7 @@ listeners = "0.3"
tauri-plugin-os = "2"
futures = "0.3.31"
semver = "1.0.27"
+reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18.2"
diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs
index a8f357cfd95..9480c72ff24 100644
--- a/packages/desktop/src-tauri/src/lib.rs
+++ b/packages/desktop/src-tauri/src/lib.rs
@@ -13,12 +13,17 @@ use tauri::{
path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, State, WebviewUrl,
WebviewWindow,
};
+use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult};
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
+use tauri_plugin_store::StoreExt;
use tokio::net::TcpSocket;
use crate::window_customizer::PinchZoomDisablePlugin;
+const SETTINGS_STORE: &str = "opencode.settings.dat";
+const DEFAULT_SERVER_URL_KEY: &str = "defaultServerUrl";
+
#[derive(Clone)]
struct ServerState {
child: Arc>>,
@@ -88,6 +93,41 @@ async fn ensure_server_started(state: State<'_, ServerState>) -> Result<(), Stri
.map_err(|_| "Failed to get server status".to_string())?
}
+#[tauri::command]
+async fn get_default_server_url(app: AppHandle) -> Result