diff --git a/github/action.yml b/github/action.yml
index 3d983a160995..2ed8fd6f976e 100644
--- a/github/action.yml
+++ b/github/action.yml
@@ -14,7 +14,7 @@ inputs:
required: false
share:
- description: "Share the opencode session (defaults to true for public repos)"
+ description: "Deprecated and ignored: session sharing was discontinued"
required: false
prompt:
@@ -71,7 +71,6 @@ runs:
env:
MODEL: ${{ inputs.model }}
AGENT: ${{ inputs.agent }}
- SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
MENTIONS: ${{ inputs.mentions }}
diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx
index e0838825570e..e68e9a369e10 100644
--- a/packages/app/src/pages/session/timeline/message-timeline.tsx
+++ b/packages/app/src/pages/session/timeline/message-timeline.tsx
@@ -299,7 +299,8 @@ export function MessageTimeline(props: {
const titleValue = createMemo(() => info()?.title)
const titleLabel = createMemo(() => sessionTitle(titleValue()))
const shareUrl = createMemo(() => info()?.share?.url)
- const shareEnabled = createMemo(() => sync().data.config.share !== "disabled")
+ // Session sharing was discontinued; keep the share menu and popover hidden.
+ const shareEnabled = () => false
const parentID = createMemo(() => info()?.parentID)
const parent = createMemo(() => {
const id = parentID()
diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx
index 18d5b172ff17..81868961fbba 100644
--- a/packages/app/src/pages/session/use-session-commands.tsx
+++ b/packages/app/src/pages/session/use-session-commands.tsx
@@ -147,93 +147,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
if (sessionID) return permission.isAutoAccepting(sessionID, sdk().directory)
return permission.isAutoAcceptingDirectory(sdk().directory)
}
- const write = async (value: string) => {
- const body = typeof document === "undefined" ? undefined : document.body
- if (body) {
- const textarea = document.createElement("textarea")
- textarea.value = value
- textarea.setAttribute("readonly", "")
- textarea.style.position = "fixed"
- textarea.style.opacity = "0"
- textarea.style.pointerEvents = "none"
- body.appendChild(textarea)
- textarea.select()
- const copied = document.execCommand("copy")
- body.removeChild(textarea)
- if (copied) return true
- }
-
- const clipboard = typeof navigator === "undefined" ? undefined : navigator.clipboard
- if (!clipboard?.writeText) return false
- return clipboard.writeText(value).then(
- () => true,
- () => false,
- )
- }
-
- const copyShare = async (url: string, existing: boolean) => {
- if (!(await write(url))) {
- showToast({
- title: language.t("toast.session.share.copyFailed.title"),
- variant: "error",
- })
- return
- }
-
- showToast({
- title: existing ? language.t("session.share.copy.copied") : language.t("toast.session.share.success.title"),
- description: language.t("toast.session.share.success.description"),
- variant: "success",
- })
- }
-
- const share = async () => {
- const sessionID = params.id
- if (!sessionID) return
-
- const existing = info()?.share?.url
- if (existing) {
- await copyShare(existing, true)
- return
- }
-
- const url = await sdk()
- .client.session.share({ sessionID })
- .then((res) => res.data?.share?.url)
- .catch(() => undefined)
- if (!url) {
- showToast({
- title: language.t("toast.session.share.failed.title"),
- description: language.t("toast.session.share.failed.description"),
- variant: "error",
- })
- return
- }
-
- await copyShare(url, false)
- }
-
- const unshare = async () => {
- const sessionID = params.id
- if (!sessionID) return
-
- await sdk()
- .client.session.unshare({ sessionID })
- .then(() =>
- showToast({
- title: language.t("toast.session.unshare.success.title"),
- description: language.t("toast.session.unshare.success.description"),
- variant: "success",
- }),
- )
- .catch(() =>
- showToast({
- title: language.t("toast.session.unshare.failed.title"),
- description: language.t("toast.session.unshare.failed.description"),
- variant: "error",
- }),
- )
- }
const exportSession = async () => {
const sessionID = params.id
@@ -421,30 +334,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
)
}
- const shareCmds = () => {
- if (sync().data.config.share === "disabled") return []
- return [
- sessionCommand({
- id: "session.share",
- title: info()?.share?.url ? language.t("session.share.copy.copyLink") : language.t("command.session.share"),
- description: info()?.share?.url
- ? language.t("toast.session.share.success.description")
- : language.t("command.session.share.description"),
- slash: "share",
- disabled: !params.id,
- onSelect: share,
- }),
- sessionCommand({
- id: "session.unshare",
- title: language.t("command.session.unshare"),
- description: language.t("command.session.unshare.description"),
- slash: "unshare",
- disabled: !params.id || !info()?.share?.url,
- onSelect: unshare,
- }),
- ]
- }
-
const sessionCmds = () => [
sessionCommand({
id: "session.new",
@@ -645,7 +534,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
command.register("session", () => [
...sessionCmds(),
- ...shareCmds(),
...fileCmds(),
...contextCmds(),
...viewCmds(),
diff --git a/packages/opencode/src/cli/cmd/github.handler.ts b/packages/opencode/src/cli/cmd/github.handler.ts
index fcf44279ce7f..e990a3c1fe33 100644
--- a/packages/opencode/src/cli/cmd/github.handler.ts
+++ b/packages/opencode/src/cli/cmd/github.handler.ts
@@ -19,7 +19,6 @@ import type {
import { UI } from "../ui"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { InstanceRef } from "@/effect/instance-ref"
-import { SessionShare } from "@/share/session"
import { Session } from "@/session/session"
import type { SessionID } from "../../session/schema"
import { MessageID, PartID } from "../../session/schema"
@@ -380,7 +379,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
if (!ctx) return yield* Effect.die("InstanceRef not provided")
const gitSvc = yield* Git.Service
const sessionSvc = yield* Session.Service
- const sessionShare = yield* SessionShare.Service
const sessionPrompt = yield* SessionPrompt.Service
const events = yield* EventV2Bridge.Service
const runLocalEffect = (effect: Effect.Effect) =>
@@ -407,7 +405,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
const { providerID, modelID } = normalizeModel()
const variant = process.env["VARIANT"] || undefined
const runId = normalizeRunId()
- const share = normalizeShare()
const oidcBaseUrl = normalizeOidcBaseUrl()
const { owner, repo } = context.repo
// For repo events (schedule, workflow_dispatch), payload has no issue/comment data
@@ -428,14 +425,12 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
? (payload as IssueCommentEvent | IssuesEvent).issue.number
: (payload as PullRequestEvent | PullRequestReviewCommentEvent).pull_request.number
const runUrl = `/${owner}/${repo}/actions/runs/${runId}`
- const shareBaseUrl = isMock ? "https://dev.opencode.ai" : "https://opencode.ai"
let appToken: string
let octoRest: Octokit
let octoGraph: typeof graphql
let gitConfig: string
let session: { id: SessionID; title: string; version: string }
- let shareId: string | undefined
let exitCode = 0
let githubClientReady = false
type PromptFiles = Awaited>["promptFiles"]
@@ -512,12 +507,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
}),
)
await subscribeSessionEvents()
- shareId = await (async () => {
- if (share === false) return
- if (!share && repoData.data.private) return
- await runLocalEffect(sessionShare.share(session.id))
- return session.id.slice(-8)
- })()
console.log("opencode session", session.id)
// Handle event types:
@@ -547,7 +536,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
repoData.data.default_branch,
branch,
summary,
- `${response}\n\nTriggered by ${triggerType}${footer({ image: true })}`,
+ `${response}\n\nTriggered by ${triggerType}${footer()}`,
)
if (pr) {
console.log(`Created PR #${pr}`)
@@ -576,8 +565,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
const summary = await summarize(response)
await pushToLocalBranch(summary, uncommittedChanges)
}
- const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
- await createComment(`${response}${footer({ image: !hasShared })}`)
+ await createComment(`${response}${footer()}`)
await removeReaction(commentType)
}
// Fork PR
@@ -594,8 +582,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
const summary = await summarize(response)
await pushToForkBranch(summary, prData, uncommittedChanges)
}
- const hasShared = prData.comments.nodes.some((c) => c.body.includes(`${shareBaseUrl}/s/${shareId}`))
- await createComment(`${response}${footer({ image: !hasShared })}`)
+ await createComment(`${response}${footer()}`)
await removeReaction(commentType)
}
}
@@ -610,7 +597,7 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
if (switched) {
// Agent switched branches (likely created its own branch/PR).
// Don't push the stale infrastructure branch — just comment.
- await createComment(`${response}${footer({ image: true })}`)
+ await createComment(`${response}${footer()}`)
await removeReaction(commentType)
} else if (dirty) {
const summary = await summarize(response)
@@ -619,16 +606,16 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
repoData.data.default_branch,
branch,
summary,
- `${response}\n\nCloses #${issueId}${footer({ image: true })}`,
+ `${response}\n\nCloses #${issueId}${footer()}`,
)
if (pr) {
- await createComment(`Created PR #${pr}${footer({ image: true })}`)
+ await createComment(`Created PR #${pr}${footer()}`)
} else {
- await createComment(`${response}${footer({ image: true })}`)
+ await createComment(`${response}${footer()}`)
}
await removeReaction(commentType)
} else {
- await createComment(`${response}${footer({ image: true })}`)
+ await createComment(`${response}${footer()}`)
await removeReaction(commentType)
}
}
@@ -677,14 +664,6 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
return value
}
- function normalizeShare() {
- const value = process.env["SHARE"]
- if (!value) return undefined
- if (value === "true") return true
- if (value === "false") return false
- throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
- }
-
function normalizeUseGithubToken() {
const value = process.env["USE_GITHUB_TOKEN"]
if (!value) return false
@@ -1351,18 +1330,8 @@ export const githubRun = Effect.fn("Cli.github.run")(function* (args: { event?:
}
}
- function footer(opts?: { image?: boolean }) {
- const image = (() => {
- if (!shareId) return ""
- if (!opts?.image) return ""
-
- const titleAlt = encodeURIComponent(session.title.substring(0, 50))
- const title64 = Buffer.from(session.title.substring(0, 700), "utf8").toString("base64")
-
- return `
\n`
- })()
- const shareUrl = shareId ? `[opencode session](${shareBaseUrl}/s/${shareId}) | ` : ""
- return `\n\n${image}${shareUrl}[github run](${runUrl})`
+ function footer() {
+ return `\n\n[github run](${runUrl})`
}
async function fetchRepo() {
diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts
index 1b7350f74478..ace560368dd1 100644
--- a/packages/opencode/src/cli/cmd/import.ts
+++ b/packages/opencode/src/cli/cmd/import.ts
@@ -2,11 +2,10 @@ import type { Session as SDKSession, Message, Part } from "@opencode-ai/sdk/v2"
import { SessionV1 } from "@opencode-ai/core/v1/session"
import { Session } from "@/session/session"
import { MessageV2 } from "../../session/message-v2"
-import { CliError, effectCmd } from "../effect-cmd"
+import { CliError, effectCmd, fail } from "../effect-cmd"
import { Database } from "@opencode-ai/core/database/database"
import { SessionTable, MessageTable, PartTable } from "@opencode-ai/core/session/sql"
import { InstanceRef } from "@/effect/instance-ref"
-import { ShareNext } from "@/share/share-next"
import { EOL } from "os"
import path from "path"
import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -16,28 +15,6 @@ import type { InstanceContext } from "@/project/instance-context"
const decodeMessageInfo = Schema.decodeUnknownSync(SessionV1.Info)
const decodePart = Schema.decodeUnknownSync(SessionV1.Part)
-/** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
-export type ShareData =
- | { type: "session"; data: SDKSession }
- | { type: "message"; data: Message }
- | { type: "part"; data: Part }
- | { type: "session_diff"; data: unknown }
- | { type: "model"; data: unknown }
-
-/** Extract share ID from a share URL like https://opncd.ai/share/abc123 */
-export function parseShareUrl(url: string): string | null {
- const match = url.match(/^https?:\/\/[^/]+\/share\/([a-zA-Z0-9_-]+)$/)
- return match ? match[1] : null
-}
-
-export function shouldAttachShareAuthHeaders(shareUrl: string, accountBaseUrl: string): boolean {
- try {
- return new URL(shareUrl).origin === new URL(accountBaseUrl).origin
- } catch {
- return false
- }
-}
-
export function formatImportFileError(file: string, error: FSUtil.Error) {
if (error._tag === "PlatformError") {
if (error.reason._tag === "NotFound") return `File not found: ${file}`
@@ -49,54 +26,14 @@ export function formatImportFileError(file: string, error: FSUtil.Error) {
return `Invalid JSON in ${file}: ${detail}`
}
-/**
- * Transform ShareNext API response (flat array) into the nested structure for local file storage.
- *
- * The API returns a flat array: [session, message, message, part, part, ...]
- * Local storage expects: { info: session, messages: [{ info: message, parts: [part, ...] }, ...] }
- *
- * This groups parts by their messageID to reconstruct the hierarchy before writing to disk.
- */
-export function transformShareData(shareData: ShareData[]): {
- info: SDKSession
- messages: Array<{ info: Message; parts: Part[] }>
-} | null {
- const sessionItem = shareData.find((d) => d.type === "session")
- if (!sessionItem) return null
-
- const messageMap = new Map()
- const partMap = new Map()
-
- for (const item of shareData) {
- if (item.type === "message") {
- messageMap.set(item.data.id, item.data)
- } else if (item.type === "part") {
- if (!partMap.has(item.data.messageID)) {
- partMap.set(item.data.messageID, [])
- }
- partMap.get(item.data.messageID)!.push(item.data)
- }
- }
-
- if (messageMap.size === 0) return null
-
- return {
- info: sessionItem.data,
- messages: Array.from(messageMap.values()).map((msg) => ({
- info: msg,
- parts: partMap.get(msg.id) ?? [],
- })),
- }
-}
-
type ExportData = { info: SDKSession; messages: Array<{ info: Message; parts: Part[] }> }
export const ImportCommand = effectCmd({
command: "import ",
- describe: "import session data from JSON file or URL",
+ describe: "import session data from JSON file",
builder: (yargs) =>
yargs.positional("file", {
- describe: "path to JSON file or share URL",
+ describe: "path to JSON file",
type: "string",
demandOption: true,
}),
@@ -108,67 +45,15 @@ export const ImportCommand = effectCmd({
})
const runImport = Effect.fn("Cli.import.body")(function* (file: string, ctx: InstanceContext) {
- const share = yield* ShareNext.Service
const fs = yield* FSUtil.Service
const { db } = yield* Database.Service
- let exportData: ExportData | undefined
-
- const isUrl = file.startsWith("http://") || file.startsWith("https://")
+ if (file.startsWith("http://") || file.startsWith("https://"))
+ return yield* fail("Importing from share URLs is no longer supported: session sharing has been discontinued")
- if (isUrl) {
- const slug = parseShareUrl(file)
- if (!slug) {
- const baseUrl = yield* Effect.orDie(share.url())
- process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/`)
- process.stdout.write(EOL)
- return
- }
-
- const baseUrl = new URL(file).origin
- const req = yield* Effect.orDie(share.request())
- const headers = shouldAttachShareAuthHeaders(file, req.baseUrl) ? req.headers : {}
-
- const tryFetch = (url: string) =>
- Effect.tryPromise({
- try: () => fetch(url, { headers }),
- catch: (e) =>
- new CliError({
- message: `Failed to fetch share data: ${e instanceof Error ? e.message : String(e)}`,
- }),
- })
-
- const dataPath = req.api.data(slug)
- let response = yield* tryFetch(`${baseUrl}${dataPath}`)
-
- if (!response.ok && dataPath !== `/api/share/${slug}/data`) {
- response = yield* tryFetch(`${baseUrl}/api/share/${slug}/data`)
- }
-
- if (!response.ok) {
- process.stdout.write(`Failed to fetch share data: ${response.statusText}`)
- process.stdout.write(EOL)
- return
- }
-
- const shareData = yield* Effect.tryPromise({
- try: () => response.json() as Promise,
- catch: () => new CliError({ message: "Share data was not valid JSON" }),
- })
- const transformed = transformShareData(shareData)
-
- if (!transformed) {
- process.stdout.write(`Share not found or empty: ${slug}`)
- process.stdout.write(EOL)
- return
- }
-
- exportData = transformed
- } else {
- exportData = (yield* fs
- .readJson(file)
- .pipe(Effect.mapError((error) => new CliError({ message: formatImportFileError(file, error) })))) as ExportData
- }
+ const exportData = (yield* fs
+ .readJson(file)
+ .pipe(Effect.mapError((error) => new CliError({ message: formatImportFileError(file, error) })))) as ExportData
if (!exportData) {
process.stdout.write(`Failed to read session data`)
diff --git a/packages/opencode/src/cli/cmd/pr.ts b/packages/opencode/src/cli/cmd/pr.ts
index 420972235746..6bab3286c5b9 100644
--- a/packages/opencode/src/cli/cmd/pr.ts
+++ b/packages/opencode/src/cli/cmd/pr.ts
@@ -43,14 +43,12 @@ export const PrCommand = effectCmd({
"view",
`${prNumber}`,
"--json",
- "headRepository,headRepositoryOwner,isCrossRepository,headRefName,body",
+ "headRepository,headRepositoryOwner,isCrossRepository,headRefName",
],
{ nothrow: true },
),
)
- let sessionId: string | undefined
-
if (prInfoResult.code === 0 && prInfoResult.text.trim()) {
const prInfo = JSON.parse(prInfoResult.text)
@@ -71,26 +69,6 @@ export const PrCommand = effectCmd({
cwd: worktree,
})
}
-
- if (prInfo?.body) {
- const sessionMatch = prInfo.body.match(/https:\/\/opncd\.ai\/s\/([a-zA-Z0-9_-]+)/)
- if (sessionMatch) {
- const sessionUrl = sessionMatch[0]
- UI.println(`Found opencode session: ${sessionUrl}`)
- UI.println(`Importing session...`)
-
- const importResult = yield* Effect.promise(() =>
- Process.text(["opencode", "import", sessionUrl], { nothrow: true }),
- )
- if (importResult.code === 0) {
- const sessionIdMatch = importResult.text.trim().match(/Imported session: ([a-zA-Z0-9_-]+)/)
- if (sessionIdMatch) {
- sessionId = sessionIdMatch[1]
- UI.println(`Session imported: ${sessionId}`)
- }
- }
- }
- }
}
UI.println(`Successfully checked out PR #${prNumber} as branch '${localBranchName}'`)
@@ -98,10 +76,9 @@ export const PrCommand = effectCmd({
UI.println("Starting opencode...")
UI.println()
- const opencodeArgs = sessionId ? ["-s", sessionId] : []
const code = yield* Effect.promise(
() =>
- Process.spawn(["opencode", ...opencodeArgs], {
+ Process.spawn(["opencode"], {
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index cccc2550179f..944aec17abd7 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -160,7 +160,8 @@ export const RunCommand = effectCmd({
})
.option("share", {
type: "boolean",
- describe: "share the session",
+ describe: "deprecated: session sharing was discontinued",
+ hidden: true,
})
.option("model", {
type: "string",
@@ -532,19 +533,8 @@ export const RunCommand = effectCmd({
}
}
- async function share(sdk: OpencodeClient, sessionID: string) {
- const cfg = await sdk.config.get()
- if (!cfg.data) return
- if (cfg.data.share !== "auto" && !flags.autoShare && !args.share) return
- const res = await sdk.session.share({ sessionID }).catch((error) => {
- if (error instanceof Error && error.message.includes("disabled")) {
- UI.println(UI.Style.TEXT_DANGER_BOLD + "! " + error.message)
- }
- return { error }
- })
- if (!res.error && "data" in res && res.data?.share?.url) {
- UI.println(UI.Style.TEXT_INFO_BOLD + "~ " + res.data.share.url)
- }
+ async function share(_sdk: OpencodeClient, _sessionID: string) {
+ if (args.share) UI.println(UI.Style.TEXT_DANGER_BOLD + "! Session sharing has been discontinued")
}
async function createFreshSession(
diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts
index d17326966f92..d010eb8039ca 100644
--- a/packages/opencode/src/effect/app-runtime.ts
+++ b/packages/opencode/src/effect/app-runtime.ts
@@ -44,7 +44,6 @@ import { Vcs } from "@/project/vcs"
import { Workspace } from "@/control-plane/workspace"
import { Worktree } from "@/worktree"
import { Installation } from "@/installation"
-import { ShareNext } from "@/share/share-next"
import { SessionShare } from "@/share/session"
import { Npm } from "@opencode-ai/core/npm"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
@@ -103,7 +102,6 @@ export const AppLayer = AppNodeBuilderV1.build(
Workspace.node,
Worktree.node,
Installation.node,
- ShareNext.node,
SessionShare.node,
]),
).pipe(Layer.provideMerge(AppNodeBuilderV1.build(Ripgrep.node)), Layer.provideMerge(Observability.layer))
diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts
index 57fe43b46ee3..f9902bc025e4 100644
--- a/packages/opencode/src/effect/bootstrap-runtime.ts
+++ b/packages/opencode/src/effect/bootstrap-runtime.ts
@@ -5,7 +5,6 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Plugin } from "@/plugin"
import { LSP } from "@/lsp/lsp"
import { Format } from "@/format"
-import { ShareNext } from "@/share/share-next"
import { Vcs } from "@/project/vcs"
import { Snapshot } from "@/snapshot"
import { Config } from "@/config/config"
@@ -13,7 +12,7 @@ import * as Observability from "@opencode-ai/core/observability"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
export const BootstrapLayer = AppNodeBuilder.build(
- LayerNode.group([Config.node, Plugin.node, ShareNext.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
+ LayerNode.group([Config.node, Plugin.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
).pipe(Layer.provide(Observability.layer))
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })
diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts
index acf0caed3cdf..d11e4e8e6204 100644
--- a/packages/opencode/src/project/bootstrap.ts
+++ b/packages/opencode/src/project/bootstrap.ts
@@ -6,7 +6,6 @@ import { Snapshot } from "../snapshot"
import * as Project from "./project"
import * as Vcs from "./vcs"
import { InstanceState } from "@/effect/instance-state"
-import { ShareNext } from "@/share/share-next"
import { Effect, Layer } from "effect"
import { Config } from "@/config/config"
import { Service } from "./bootstrap-service"
@@ -25,7 +24,6 @@ const layer = Layer.effect(
const lsp = yield* LSP.Service
const plugin = yield* Plugin.Service
const project = yield* Project.Service
- const shareNext = yield* ShareNext.Service
const snapshot = yield* Snapshot.Service
const vcs = yield* Vcs.Service
@@ -39,7 +37,7 @@ const layer = Layer.effect(
// Each service self-manages its own slow work via Effect.forkScoped against
// its per-instance state scope. We just await materialization here.
yield* Effect.forEach(
- [lsp, shareNext, format, vcs, snapshot, project],
+ [lsp, format, vcs, snapshot, project],
(s) => s.init().pipe(Effect.catchCause((cause) => Effect.logWarning("init failed", { cause }))),
{ concurrency: "unbounded", discard: true },
).pipe(Effect.withSpan("InstanceBootstrap.init"))
@@ -52,7 +50,7 @@ const layer = Layer.effect(
export const node = makeGlobalNode({
service: Service,
layer: layer,
- deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, ShareNext.node, Snapshot.node, Vcs.node],
+ deps: [Config.node, Format.node, LSP.node, Plugin.node, Project.node, Snapshot.node, Vcs.node],
})
export * as InstanceBootstrap from "./bootstrap"
diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts
index fb9d2db65621..94555ca27806 100644
--- a/packages/opencode/src/server/routes/instance/httpapi/server.ts
+++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts
@@ -40,7 +40,6 @@ import { SessionStatus } from "@/session/status"
import { SessionSummary } from "@/session/summary"
import { Todo } from "@/session/todo"
import { SessionShare } from "@/share/session"
-import { ShareNext } from "@/share/share-next"
import { Skill } from "@/skill"
import { Discovery } from "@/skill/discovery"
import { Snapshot } from "@/snapshot"
@@ -258,7 +257,6 @@ const app = LayerNode.group([
Workspace.node,
Worktree.node,
Installation.node,
- ShareNext.node,
SessionShare.node,
InstanceStore.node,
httpClient,
diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts
index a4ef77fbfd2f..7634aea18e43 100644
--- a/packages/opencode/src/share/session.ts
+++ b/packages/opencode/src/share/session.ts
@@ -1,10 +1,7 @@
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Session } from "@/session/session"
import { SessionID } from "@/session/schema"
-import { Effect, Layer, Scope, Context } from "effect"
-import { Config } from "@/config/config"
-import { RuntimeFlags } from "@/effect/runtime-flags"
-import { ShareNext } from "./share-next"
+import { Effect, Layer, Context } from "effect"
export interface Interface {
readonly create: (input?: Session.CreateInput) => Effect.Effect
@@ -17,32 +14,20 @@ export class Service extends Context.Service()("@opencode/Se
const layer = Layer.effect(
Service,
Effect.gen(function* () {
- const cfg = yield* Config.Service
const session = yield* Session.Service
- const shareNext = yield* ShareNext.Service
- const scope = yield* Scope.Scope
- const flags = yield* RuntimeFlags.Service
-
- const share = Effect.fn("SessionShare.share")(function* (sessionID: SessionID) {
- const conf = yield* cfg.get()
- if (conf.share === "disabled") throw new Error("Sharing is disabled in configuration")
- const result = yield* shareNext.create(sessionID)
- yield* session.setShare({ sessionID, share: { url: result.url } })
- return result
+
+ // Session sharing was discontinued. The share endpoints stay for API compatibility: share fails and unshare only
+ // clears the local link.
+ const share = Effect.fn("SessionShare.share")(function* (_sessionID: SessionID) {
+ return yield* Effect.fail(new Error("Session sharing has been discontinued"))
})
const unshare = Effect.fn("SessionShare.unshare")(function* (sessionID: SessionID) {
- yield* shareNext.remove(sessionID)
yield* session.setShare({ sessionID, share: undefined })
})
const create = Effect.fn("SessionShare.create")(function* (input?: Session.CreateInput) {
- const result = yield* session.create(input)
- if (result.parentID) return result
- const conf = yield* cfg.get()
- if (!(flags.autoShare || conf.share === "auto")) return result
- yield* share(result.id).pipe(Effect.ignore, Effect.forkIn(scope))
- return result
+ return yield* session.create(input)
})
return Service.of({ create, share, unshare })
@@ -52,7 +37,7 @@ const layer = Layer.effect(
export const node = LayerNode.make({
service: Service,
layer: layer,
- deps: [Config.node, Session.node, ShareNext.node, RuntimeFlags.node],
+ deps: [Session.node],
})
export * as SessionShare from "./session"
diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts
deleted file mode 100644
index 60112e10d964..000000000000
--- a/packages/opencode/src/share/share-next.ts
+++ /dev/null
@@ -1,371 +0,0 @@
-import { LayerNode } from "@opencode-ai/core/effect/layer-node"
-import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
-import type * as SDK from "@opencode-ai/sdk/v2"
-import { serviceUse } from "@opencode-ai/core/effect/service-use"
-import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
-import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
-import { Account } from "@/account/account"
-import { EventV2Bridge } from "@/event-v2-bridge"
-import { InstanceState } from "@/effect/instance-state"
-import { Provider } from "@/provider/provider"
-
-import { Session } from "@/session/session"
-import { MessageV2 } from "@/session/message-v2"
-import type { SessionID } from "@/session/schema"
-import { Database } from "@opencode-ai/core/database/database"
-import { eq } from "drizzle-orm"
-import { Config } from "@/config/config"
-import { SessionShareTable } from "@opencode-ai/core/share/sql"
-import { ProviderV2 } from "@opencode-ai/core/provider"
-import { ModelV2 } from "@opencode-ai/core/model"
-import { EventV2 } from "@opencode-ai/core/event"
-
-const disabled = process.env["OPENCODE_DISABLE_SHARE"] === "true" || process.env["OPENCODE_DISABLE_SHARE"] === "1"
-
-export type Api = {
- create: string
- sync: (shareID: string) => string
- remove: (shareID: string) => string
- data: (shareID: string) => string
-}
-
-export type Req = {
- headers: Record
- api: Api
- baseUrl: string
-}
-
-const ShareSchema = Schema.Struct({
- id: Schema.String,
- url: Schema.String,
- secret: Schema.String,
-})
-export type Share = typeof ShareSchema.Type
-
-type State = {
- queue: Map>
- scope: Scope.Closeable
- shared: Map
-}
-
-type Data =
- | {
- type: "session"
- data: SDK.Session
- }
- | {
- type: "message"
- data: SDK.Message
- }
- | {
- type: "part"
- data: SDK.Part
- }
- | {
- type: "session_diff"
- data: SDK.SnapshotFileDiff[]
- }
- | {
- type: "model"
- data: SDK.Model[]
- }
-
-export interface Interface {
- readonly init: () => Effect.Effect
- readonly url: () => Effect.Effect
- readonly request: () => Effect.Effect
- readonly create: (sessionID: SessionID) => Effect.Effect
- readonly remove: (sessionID: SessionID) => Effect.Effect
-}
-
-export class Service extends Context.Service()("@opencode/ShareNext") {}
-
-export const use = serviceUse(Service)
-
-function api(resource: string): Api {
- return {
- create: `/api/${resource}`,
- sync: (shareID) => `/api/${resource}/${shareID}/sync`,
- remove: (shareID) => `/api/${resource}/${shareID}`,
- data: (shareID) => `/api/${resource}/${shareID}/data`,
- }
-}
-
-const legacyApi = api("share")
-const consoleApi = api("shares")
-
-function key(item: Data) {
- switch (item.type) {
- case "session":
- return "session"
- case "message":
- return `message/${item.data.id}`
- case "part":
- return `part/${item.data.messageID}/${item.data.id}`
- case "session_diff":
- return "session_diff"
- case "model":
- return "model"
- }
-}
-
-const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const account = yield* Account.Service
- const events = yield* EventV2Bridge.Service
- const cfg = yield* Config.Service
- const { db } = yield* Database.Service
- const http = yield* HttpClient.HttpClient
- const httpOk = HttpClient.filterStatusOk(http)
- const provider = yield* Provider.Service
- const session = yield* Session.Service
-
- function sync(sessionID: SessionID, data: Data[]) {
- return Effect.gen(function* () {
- if (disabled) return
- const share = yield* getCached(sessionID)
- if (!share) return
-
- const s = yield* InstanceState.get(state)
- const existing = s.queue.get(sessionID)
- if (existing) {
- for (const item of data) {
- existing.set(key(item), item)
- }
- return
- }
-
- const next = new Map(data.map((item) => [key(item), item]))
- s.queue.set(sessionID, next)
- yield* flush(sessionID).pipe(
- Effect.delay(1000),
- Effect.catchCause((cause) => Effect.logError("share flush failed", { sessionID: sessionID, cause: cause })),
- Effect.forkIn(s.scope),
- )
- })
- }
-
- const state: InstanceState.InstanceState = yield* InstanceState.make(
- Effect.fn("ShareNext.state")(function* (_ctx) {
- const cache: State = { queue: new Map(), scope: yield* Scope.make(), shared: new Map() }
-
- yield* Effect.addFinalizer(() =>
- Scope.close(cache.scope, Exit.void).pipe(
- Effect.andThen(
- Effect.sync(() => {
- cache.queue.clear()
- cache.shared.clear()
- }),
- ),
- ),
- )
-
- if (disabled) return cache
-
- const watch = (
- def: D,
- fn: (data: EventV2.Data) => Effect.Effect,
- ) =>
- events.listen((event) => {
- if (event.type !== def.type || event.location?.directory !== _ctx.directory) return Effect.void
- return fn(event.data as EventV2.Data).pipe(
- Effect.catchCause((cause) =>
- Effect.logError("share subscriber failed", { type: def.type, cause: cause }),
- ),
- )
- })
-
- yield* watch(Session.Event.Updated, (data) =>
- Effect.gen(function* () {
- const info = data.info
- yield* sync(info.id, [{ type: "session", data: structuredClone(info) as SDK.Session }])
- }),
- )
- yield* watch(MessageV2.Event.Updated, (data) =>
- Effect.gen(function* () {
- const info = data.info
- yield* sync(info.sessionID, [{ type: "message", data: structuredClone(info) as SDK.Message }])
- if (info.role !== "user") return
- const model = yield* provider.getModel(info.model.providerID, info.model.modelID)
- yield* sync(info.sessionID, [{ type: "model", data: [model] }])
- }),
- )
- yield* watch(MessageV2.Event.PartUpdated, (data) =>
- sync(data.part.sessionID, [{ type: "part", data: structuredClone(data.part) as SDK.Part }]),
- )
- yield* watch(Session.Event.Diff, (data) =>
- sync(data.sessionID, [{ type: "session_diff", data: structuredClone(data.diff) as SDK.SnapshotFileDiff[] }]),
- )
- yield* watch(Session.Event.Deleted, (data) => remove(data.sessionID))
-
- return cache
- }),
- )
-
- const request = Effect.fn("ShareNext.request")(function* () {
- const headers: Record = {}
- const active = yield* account.active()
- if (Option.isNone(active) || !active.value.active_org_id) {
- const baseUrl = (yield* cfg.get()).enterprise?.url ?? "https://opncd.ai"
- return { headers, api: legacyApi, baseUrl } satisfies Req
- }
-
- const token = yield* account.token(active.value.id)
- if (Option.isNone(token)) {
- throw new Error("No active account token available for sharing")
- }
-
- headers.authorization = `Bearer ${token.value}`
- headers["x-org-id"] = active.value.active_org_id
- return { headers, api: consoleApi, baseUrl: active.value.url } satisfies Req
- })
-
- const get = Effect.fnUntraced(function* (sessionID: SessionID) {
- const row = yield* db
- .select()
- .from(SessionShareTable)
- .where(eq(SessionShareTable.session_id, sessionID))
- .get()
- .pipe(Effect.orDie)
- if (!row) return
- return { id: row.id, secret: row.secret, url: row.url } satisfies Share
- })
-
- const getCached = Effect.fnUntraced(function* (sessionID: SessionID) {
- const s = yield* InstanceState.get(state)
- if (s.shared.has(sessionID)) {
- const cached = s.shared.get(sessionID)
- return cached === null ? undefined : cached
- }
-
- const share = yield* get(sessionID)
- s.shared.set(sessionID, share ?? null)
- return share
- })
-
- const flush = Effect.fn("ShareNext.flush")(function* (sessionID: SessionID) {
- if (disabled) return
- const s = yield* InstanceState.get(state)
- const queued = s.queue.get(sessionID)
- if (!queued) return
-
- s.queue.delete(sessionID)
-
- const share = yield* getCached(sessionID)
- if (!share) return
-
- const req = yield* request()
- const res = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.sync(share.id)}`).pipe(
- HttpClientRequest.setHeaders(req.headers),
- HttpClientRequest.bodyJson({ secret: share.secret, data: Array.from(queued.values()) }),
- Effect.flatMap((r) => http.execute(r)),
- )
-
- if (res.status >= 400) {
- yield* Effect.logWarning("failed to sync share", {
- sessionID: sessionID,
- shareID: share.id,
- status: res.status,
- })
- }
- })
-
- const full = Effect.fn("ShareNext.full")(function* (sessionID: SessionID) {
- yield* Effect.logInfo("full sync", { sessionID: sessionID })
- const info = yield* session.get(sessionID)
- const diffs = yield* session.diff(sessionID)
- const messages = yield* session.messages({ sessionID })
- const models = yield* Effect.forEach(
- Array.from(
- new Map(
- messages
- .filter((msg) => msg.info.role === "user")
- .map((msg) => (msg.info as SDK.UserMessage).model)
- .map((item) => [`${item.providerID}/${item.modelID}`, item] as const),
- ).values(),
- ),
- (item) => provider.getModel(ProviderV2.ID.make(item.providerID), ModelV2.ID.make(item.modelID)),
- { concurrency: 8 },
- )
-
- yield* sync(sessionID, [
- { type: "session", data: info },
- ...messages.map((item) => ({ type: "message" as const, data: item.info })),
- ...messages.flatMap((item) => item.parts.map((part) => ({ type: "part" as const, data: part }))),
- { type: "session_diff", data: diffs },
- { type: "model", data: models },
- ])
- })
-
- const init = Effect.fn("ShareNext.init")(function* () {
- if (disabled) return
- yield* InstanceState.get(state)
- })
-
- const url = Effect.fn("ShareNext.url")(function* () {
- return (yield* request()).baseUrl
- })
-
- const create = Effect.fn("ShareNext.create")(function* (sessionID: SessionID) {
- if (disabled) return { id: "", url: "", secret: "" }
- yield* Effect.logInfo("creating share", { sessionID: sessionID })
- const req = yield* request()
- const result = yield* HttpClientRequest.post(`${req.baseUrl}${req.api.create}`).pipe(
- HttpClientRequest.setHeaders(req.headers),
- HttpClientRequest.bodyJson({ sessionID }),
- Effect.flatMap((r) => httpOk.execute(r)),
- Effect.flatMap(HttpClientResponse.schemaBodyJson(ShareSchema)),
- )
- yield* db
- .insert(SessionShareTable)
- .values({ session_id: sessionID, id: result.id, secret: result.secret, url: result.url })
- .onConflictDoUpdate({
- target: SessionShareTable.session_id,
- set: { id: result.id, secret: result.secret, url: result.url },
- })
- .run()
- .pipe(Effect.orDie)
- const s = yield* InstanceState.get(state)
- s.shared.set(sessionID, result)
- yield* full(sessionID).pipe(
- Effect.catchCause((cause) => Effect.logError("share full sync failed", { sessionID: sessionID, cause: cause })),
- Effect.forkIn(s.scope),
- )
- return result
- })
-
- const remove = Effect.fn("ShareNext.remove")(function* (sessionID: SessionID) {
- if (disabled) return
- yield* Effect.logInfo("removing share", { sessionID: sessionID })
- const s = yield* InstanceState.get(state)
- const share = yield* getCached(sessionID)
- if (!share) {
- s.shared.delete(sessionID)
- s.queue.delete(sessionID)
- return
- }
-
- const req = yield* request()
- yield* HttpClientRequest.delete(`${req.baseUrl}${req.api.remove(share.id)}`).pipe(
- HttpClientRequest.setHeaders(req.headers),
- HttpClientRequest.bodyJson({ secret: share.secret }),
- Effect.flatMap((r) => httpOk.execute(r)),
- )
-
- yield* db.delete(SessionShareTable).where(eq(SessionShareTable.session_id, sessionID)).run().pipe(Effect.orDie)
- s.shared.delete(sessionID)
- s.queue.delete(sessionID)
- })
-
- return Service.of({ init, url, request, create, remove })
- }),
-)
-
-export const node = LayerNode.make({
- service: Service,
- layer: layer,
- deps: [Account.node, EventV2Bridge.node, Config.node, Database.node, httpClient, Provider.node, Session.node],
-})
-
-export * as ShareNext from "./share-next"
diff --git a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap
index d22ded5af10f..f9d9ea70ba68 100644
--- a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap
+++ b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap
@@ -85,7 +85,6 @@ Options:
-c, --continue continue the last session [boolean]
-s, --session session id to continue [string]
--fork fork the session before continuing (requires --continue or --session) [boolean]
- --share share the session [boolean]
-m, --model model to use in the format of provider/model [string]
--agent agent to use [string]
--format format: default (formatted) or json (raw JSON events)
@@ -302,10 +301,10 @@ Options:
exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode import --help 1`] = `
"opencode import
-import session data from JSON file or URL
+import session data from JSON file
Positionals:
- file path to JSON file or share URL [string] [required]
+ file path to JSON file [string] [required]
Options:
-h, --help show help [boolean]
diff --git a/packages/opencode/test/cli/import.test.ts b/packages/opencode/test/cli/import.test.ts
index 59824c3efcb9..f3abdd5618a9 100644
--- a/packages/opencode/test/cli/import.test.ts
+++ b/packages/opencode/test/cli/import.test.ts
@@ -1,11 +1,5 @@
import { test, expect } from "bun:test"
-import {
- formatImportFileError,
- parseShareUrl,
- shouldAttachShareAuthHeaders,
- transformShareData,
- type ShareData,
-} from "../../src/cli/cmd/import"
+import { formatImportFileError } from "../../src/cli/cmd/import"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { PlatformError } from "effect"
@@ -41,50 +35,3 @@ test("formats import file errors", () => {
),
).toBe("Invalid JSON in test.json: Unexpected token")
})
-
-// parseShareUrl tests
-test("parses valid share URLs", () => {
- expect(parseShareUrl("https://opncd.ai/share/Jsj3hNIW")).toBe("Jsj3hNIW")
- expect(parseShareUrl("https://custom.example.com/share/abc123")).toBe("abc123")
- expect(parseShareUrl("http://localhost:3000/share/test_id-123")).toBe("test_id-123")
-})
-
-test("rejects invalid URLs", () => {
- expect(parseShareUrl("https://opncd.ai/s/Jsj3hNIW")).toBeNull() // legacy format
- expect(parseShareUrl("https://opncd.ai/share/")).toBeNull()
- expect(parseShareUrl("https://opncd.ai/share/id/extra")).toBeNull()
- expect(parseShareUrl("not-a-url")).toBeNull()
-})
-
-test("only attaches share auth headers for same-origin URLs", () => {
- expect(shouldAttachShareAuthHeaders("https://control.example.com/share/abc", "https://control.example.com")).toBe(
- true,
- )
- expect(shouldAttachShareAuthHeaders("https://other.example.com/share/abc", "https://control.example.com")).toBe(false)
- expect(shouldAttachShareAuthHeaders("https://control.example.com:443/share/abc", "https://control.example.com")).toBe(
- true,
- )
- expect(shouldAttachShareAuthHeaders("not-a-url", "https://control.example.com")).toBe(false)
-})
-
-// transformShareData tests
-test("transforms share data to storage format", () => {
- const data: ShareData[] = [
- { type: "session", data: { id: "sess-1", title: "Test" } as any },
- { type: "message", data: { id: "msg-1", sessionID: "sess-1" } as any },
- { type: "part", data: { id: "part-1", messageID: "msg-1" } as any },
- { type: "part", data: { id: "part-2", messageID: "msg-1" } as any },
- ]
-
- const result = transformShareData(data)!
-
- expect(result.info.id).toBe("sess-1")
- expect(result.messages).toHaveLength(1)
- expect(result.messages[0].parts).toHaveLength(2)
-})
-
-test("returns null for invalid share data", () => {
- expect(transformShareData([])).toBeNull()
- expect(transformShareData([{ type: "message", data: {} as any }])).toBeNull()
- expect(transformShareData([{ type: "session", data: { id: "s" } as any }])).toBeNull() // no messages
-})
diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts
index c8e11ea8451b..77641cec3290 100644
--- a/packages/opencode/test/server/httpapi-exercise/index.ts
+++ b/packages/opencode/test/server/httpapi-exercise/index.ts
@@ -1656,14 +1656,7 @@ const scenarios: Scenario[] = [
.mutating()
.seeded((ctx) => ctx.session({ title: "Share session" }))
.at((ctx) => ({ path: route("/session/{sessionID}/share", { sessionID: ctx.state.id }), headers: ctx.headers() }))
- .json(
- 200,
- (body, ctx) => {
- object(body)
- check(body.id === ctx.state.id, "share should return the session")
- },
- "status",
- ),
+ .json(500, object, "status"),
http.protected
.delete("/session/{sessionID}/share", "session.unshare")
.mutating()
diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts
deleted file mode 100644
index fe036be8c959..000000000000
--- a/packages/opencode/test/share/share-next.test.ts
+++ /dev/null
@@ -1,324 +0,0 @@
-import { beforeEach, describe, expect } from "bun:test"
-import { Effect, Exit, Layer, Option } from "effect"
-import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
-import { LayerNode } from "@opencode-ai/core/effect/layer-node"
-import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
-import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
-import { SessionProjector } from "@opencode-ai/core/session/projector"
-
-import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema"
-import { AccountRepo } from "../../src/account/repo"
-import { EventV2Bridge } from "../../src/event-v2-bridge"
-import { Session } from "@/session/session"
-import type { SessionID } from "../../src/session/schema"
-import { ShareNext } from "@/share/share-next"
-import { SessionShareTable } from "@opencode-ai/core/share/sql"
-import { Database } from "@opencode-ai/core/database/database"
-import { eq } from "drizzle-orm"
-import { provideTmpdirInstance } from "../fixture/fixture"
-import { resetDatabase } from "../fixture/db"
-import { pollWithTimeout, testEffect } from "../lib/effect"
-
-const env = LayerNode.compile(LayerNode.group([CrossSpawnSpawner.node]))
-const it = testEffect(env)
-
-const json = (req: Parameters[0], body: unknown, status = 200) =>
- HttpClientResponse.fromWeb(
- req,
- new Response(JSON.stringify(body), {
- status,
- headers: { "content-type": "application/json" },
- }),
- )
-
-const none = HttpClient.make(() => Effect.die("unexpected http call"))
-
-function requestLayer(client: HttpClient.HttpClient) {
- const replacement = [httpClient, Layer.succeed(HttpClient.HttpClient, client)] as const
- return LayerNode.compile(LayerNode.group([ShareNext.node, AccountRepo.node]), [replacement])
-}
-
-function integrationLayer(client: HttpClient.HttpClient) {
- const replacement = [httpClient, Layer.succeed(HttpClient.HttpClient, client)] as const
- return LayerNode.compile(
- LayerNode.group([
- ShareNext.node,
- EventV2Bridge.node,
- Session.node,
- SessionProjector.node,
- AccountRepo.node,
- Database.node,
- ]),
- [replacement],
- )
-}
-
-const share = (id: SessionID) =>
- Effect.gen(function* () {
- const { db } = yield* Database.Service
- return yield* db
- .select()
- .from(SessionShareTable)
- .where(eq(SessionShareTable.session_id, id))
- .get()
- .pipe(Effect.orDie)
- })
-
-const seed = (url: string, org?: string) =>
- AccountRepo.Service.use((repo) =>
- repo.persistAccount({
- id: AccountID.make("account-1"),
- email: "user@example.com",
- url,
- accessToken: AccessToken.make("st_test_token"),
- refreshToken: RefreshToken.make("rt_test_token"),
- expiry: Date.now() + 10 * 60_000,
- orgID: org ? Option.some(OrgID.make(org)) : Option.none(),
- }),
- )
-
-beforeEach(async () => {
- await resetDatabase()
-})
-
-describe("ShareNext", () => {
- it.live("request uses legacy share API without active org account", () =>
- provideTmpdirInstance(
- () =>
- ShareNext.Service.use((svc) =>
- Effect.gen(function* () {
- const req = yield* svc.request()
-
- expect(req.api.create).toBe("/api/share")
- expect(req.api.sync("shr_123")).toBe("/api/share/shr_123/sync")
- expect(req.api.remove("shr_123")).toBe("/api/share/shr_123")
- expect(req.api.data("shr_123")).toBe("/api/share/shr_123/data")
- expect(req.baseUrl).toBe("https://legacy-share.example.com")
- expect(req.headers).toEqual({})
- }),
- ).pipe(Effect.provide(requestLayer(none))),
- { config: { enterprise: { url: "https://legacy-share.example.com" } } },
- ),
- )
-
- it.live("request uses default URL when no enterprise config", () =>
- provideTmpdirInstance(() =>
- ShareNext.Service.use((svc) =>
- Effect.gen(function* () {
- const req = yield* svc.request()
-
- expect(req.baseUrl).toBe("https://opncd.ai")
- expect(req.api.create).toBe("/api/share")
- expect(req.headers).toEqual({})
- }),
- ).pipe(Effect.provide(requestLayer(none))),
- ),
- )
-
- it.live("request uses org share API with auth headers when account is active", () =>
- provideTmpdirInstance(() =>
- Effect.gen(function* () {
- yield* seed("https://control.example.com", "org-1")
-
- const req = yield* ShareNext.use.request()
-
- expect(req.api.create).toBe("/api/shares")
- expect(req.api.sync("shr_123")).toBe("/api/shares/shr_123/sync")
- expect(req.api.remove("shr_123")).toBe("/api/shares/shr_123")
- expect(req.api.data("shr_123")).toBe("/api/shares/shr_123/data")
- expect(req.baseUrl).toBe("https://control.example.com")
- expect(req.headers).toEqual({
- authorization: "Bearer st_test_token",
- "x-org-id": "org-1",
- })
- }).pipe(Effect.provide(requestLayer(none))),
- ),
- )
-
- it.live("create posts share, persists it, and returns the result", () =>
- provideTmpdirInstance(
- () => {
- const createRequests: HttpClientRequest.HttpClientRequest[] = []
- const client = HttpClient.make((req) => {
- if (req.url.endsWith("/api/share")) {
- createRequests.push(req)
- return Effect.succeed(
- json(req, {
- id: "shr_abc",
- url: "https://legacy-share.example.com/share/abc",
- secret: "sec_123",
- }),
- )
- }
- return Effect.succeed(json(req, { ok: true }))
- })
- return Effect.gen(function* () {
- const session = yield* (yield* Session.Service).create({ title: "test" })
-
- const result = yield* (yield* ShareNext.Service).create(session.id)
-
- expect(result.id).toBe("shr_abc")
- expect(result.url).toBe("https://legacy-share.example.com/share/abc")
- expect(result.secret).toBe("sec_123")
-
- const row = yield* share(session.id)
- expect(row?.id).toBe("shr_abc")
- expect(row?.url).toBe("https://legacy-share.example.com/share/abc")
- expect(row?.secret).toBe("sec_123")
-
- expect(createRequests).toHaveLength(1)
- expect(createRequests[0].method).toBe("POST")
- expect(createRequests[0].url).toBe("https://legacy-share.example.com/api/share")
- }).pipe(Effect.provide(integrationLayer(client)))
- },
- { config: { enterprise: { url: "https://legacy-share.example.com" } } },
- ),
- )
-
- it.live("remove deletes the persisted share and calls the delete endpoint", () =>
- provideTmpdirInstance(
- () => {
- const seen: HttpClientRequest.HttpClientRequest[] = []
- const client = HttpClient.make((req) => {
- seen.push(req)
- if (req.method === "POST") {
- return Effect.succeed(
- json(req, {
- id: "shr_abc",
- url: "https://legacy-share.example.com/share/abc",
- secret: "sec_123",
- }),
- )
- }
- return Effect.succeed(HttpClientResponse.fromWeb(req, new Response(null, { status: 200 })))
- })
- return Effect.gen(function* () {
- const session = yield* (yield* Session.Service).create({ title: "test" })
- const service = yield* ShareNext.Service
-
- yield* service.create(session.id)
- yield* service.remove(session.id)
-
- expect(yield* share(session.id)).toBeUndefined()
- expect(seen.map((req) => [req.method, req.url])).toEqual([
- ["POST", "https://legacy-share.example.com/api/share"],
- ["DELETE", "https://legacy-share.example.com/api/share/shr_abc"],
- ])
- }).pipe(Effect.provide(integrationLayer(client)))
- },
- { config: { enterprise: { url: "https://legacy-share.example.com" } } },
- ),
- )
-
- it.live("create fails on a non-ok response and does not persist a share", () =>
- provideTmpdirInstance(() => {
- const client = HttpClient.make((req) => Effect.succeed(json(req, { error: "bad" }, 500)))
- return Effect.gen(function* () {
- const session = yield* (yield* Session.Service).create({ title: "test" })
-
- const exit = yield* ShareNext.Service.use((svc) => Effect.exit(svc.create(session.id)))
-
- expect(Exit.isFailure(exit)).toBe(true)
- expect(yield* share(session.id)).toBeUndefined()
- }).pipe(Effect.provide(integrationLayer(client)))
- }),
- )
-
- it.live("ShareNext coalesces rapid diff events into one delayed sync with latest data", () =>
- provideTmpdirInstance(
- () => {
- const seen: Array<{ url: string; body: string }> = []
- const client = HttpClient.make((req) => {
- if (req.url.endsWith("/sync") && req.body._tag === "Uint8Array") {
- seen.push({ url: req.url, body: new TextDecoder().decode(req.body.body) })
- }
- return Effect.succeed(json(req, { ok: true }))
- })
-
- return Effect.gen(function* () {
- const events = yield* EventV2Bridge.Service
- const share = yield* ShareNext.Service
- const session = yield* Session.Service
-
- const info = yield* session.create({ title: "first" })
- yield* share.init()
- yield* Effect.sleep(50)
- const { db } = yield* Database.Service
- yield* db
- .insert(SessionShareTable)
- .values({
- session_id: info.id,
- id: "shr_abc",
- url: "https://legacy-share.example.com/share/abc",
- secret: "sec_123",
- })
- .run()
- .pipe(Effect.orDie)
-
- yield* events.publish(Session.Event.Diff, {
- sessionID: info.id,
- diff: [
- {
- file: "a.ts",
- patch:
- "Index: a.ts\n===================================================================\n--- a.ts\t\n+++ a.ts\t\n@@ -1,1 +1,1 @@\n-one\n\\ No newline at end of file\n+two\n\\ No newline at end of file\n",
- additions: 1,
- deletions: 1,
- status: "modified",
- },
- ],
- })
- yield* events.publish(Session.Event.Diff, {
- sessionID: info.id,
- diff: [
- {
- file: "b.ts",
- patch:
- "Index: b.ts\n===================================================================\n--- b.ts\t\n+++ b.ts\t\n@@ -1,1 +1,1 @@\n-old\n\\ No newline at end of file\n+new\n\\ No newline at end of file\n",
- additions: 2,
- deletions: 0,
- status: "modified",
- },
- ],
- })
- yield* pollWithTimeout(
- Effect.sync(() => (seen.length === 1 ? true : undefined)),
- "timed out waiting for share sync",
- "5 seconds",
- )
-
- expect(seen).toHaveLength(1)
- expect(seen[0].url).toBe("https://legacy-share.example.com/api/share/shr_abc/sync")
-
- const body = JSON.parse(seen[0].body) as {
- secret: string
- data: Array<{
- type: string
- data: Array<{
- file: string
- patch: string
- additions: number
- deletions: number
- status?: string
- }>
- }>
- }
- expect(body.secret).toBe("sec_123")
- expect(body.data).toHaveLength(1)
- expect(body.data[0].type).toBe("session_diff")
- expect(body.data[0].data).toEqual([
- {
- file: "b.ts",
- patch:
- "Index: b.ts\n===================================================================\n--- b.ts\t\n+++ b.ts\t\n@@ -1,1 +1,1 @@\n-old\n\\ No newline at end of file\n+new\n\\ No newline at end of file\n",
- additions: 2,
- deletions: 0,
- status: "modified",
- },
- ])
- }).pipe(Effect.provide(integrationLayer(client)))
- },
- { config: { enterprise: { url: "https://legacy-share.example.com" } } },
- ),
- )
-})
diff --git a/packages/tui/src/feature-plugins/home/tips-view.tsx b/packages/tui/src/feature-plugins/home/tips-view.tsx
index d9ef81e40cca..c7ef5c8865f8 100644
--- a/packages/tui/src/feature-plugins/home/tips-view.tsx
+++ b/packages/tui/src/feature-plugins/home/tips-view.tsx
@@ -167,7 +167,6 @@ const TIPS: Tip[] = [
(shortcuts) => press(shortcuts.agentCycle(), "to cycle between Build and Plan agents"),
"Use {highlight}/undo{/highlight} to revert the last message and file changes",
"Use {highlight}/redo{/highlight} to restore previously undone messages and file changes",
- "Run {highlight}/share{/highlight} to create a public opencode.ai link",
"Drag and drop images or PDFs into the terminal as context",
(shortcuts) => press(shortcuts.inputPaste(), "to paste images from your clipboard into the prompt"),
(shortcuts) => `Use ${commandText("/editor", shortcuts.editorOpen())} to compose messages in your external editor`,
@@ -259,9 +258,6 @@ const TIPS: Tip[] = [
'Set {highlight}"tools": {"bash": false}{/highlight} to disable specific tools',
'Set {highlight}"mcp_*": false{/highlight} to disable all tools from an MCP server',
"Override global tool settings per agent configuration",
- 'Set {highlight}"share": "auto"{/highlight} to automatically share all sessions',
- 'Set {highlight}"share": "disabled"{/highlight} to prevent any session sharing',
- "Run {highlight}/unshare{/highlight} to remove a session from public access",
"Permission {highlight}doom_loop{/highlight} prevents infinite tool call loops",
"Permission {highlight}external_directory{/highlight} protects files outside project",
"Run {highlight}opencode debug config{/highlight} to troubleshoot configuration",
diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx
index 2b54f21671b3..e1f554f483a0 100644
--- a/packages/tui/src/routes/session/index.tsx
+++ b/packages/tui/src/routes/session/index.tsx
@@ -113,12 +113,10 @@ function goUpsellKeys(action: RetryAction) {
}
const sessionBindingCommands = [
- "session.share",
"session.rename",
"session.timeline",
"session.fork",
"session.compact",
- "session.unshare",
"session.undo",
"session.redo",
"session.sidebar.toggle",
@@ -463,46 +461,6 @@ export function Session() {
}
const sessionCommandList = createMemo(() => [
- {
- title: session()?.share?.url ? "Copy share link" : "Share session",
- value: "session.share",
- suggested: route.type === "session",
- category: "Session",
- enabled: sync.data.config.share !== "disabled",
- slash: {
- name: "share",
- },
- run: async () => {
- const copy = (url: string) =>
- clipboard
- .write?.(url)
- .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" }))
- .catch(() => toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }))
- const url = session()?.share?.url
- if (url) {
- await copy(url)
- dialog.clear()
- return
- }
- if (!kv.get("share_consent", false)) {
- const ok = await DialogConfirm.show(dialog, "Share Session", "Are you sure you want to share it?")
- if (ok !== true) return
- kv.set("share_consent", true)
- }
- await sdk.client.session
- .share({
- sessionID: route.sessionID,
- })
- .then((res) => copy(res.data!.share!.url))
- .catch((error) => {
- toast.show({
- message: error instanceof Error ? error.message : "Failed to share session",
- variant: "error",
- })
- })
- dialog.clear()
- },
- },
{
title: "Rename session",
value: "session.rename",
@@ -584,29 +542,6 @@ export function Session() {
dialog.clear()
},
},
- {
- title: "Unshare session",
- value: "session.unshare",
- category: "Session",
- enabled: !!session()?.share?.url,
- slash: {
- name: "unshare",
- },
- run: async () => {
- await sdk.client.session
- .unshare({
- sessionID: route.sessionID,
- })
- .then(() => toast.show({ message: "Session unshared successfully", variant: "success" }))
- .catch((error) => {
- toast.show({
- message: error instanceof Error ? error.message : "Failed to unshare session",
- variant: "error",
- })
- })
- dialog.clear()
- },
- },
{
title: "Undo previous message",
value: "session.undo",
diff --git a/packages/tui/src/routes/session/sidebar.tsx b/packages/tui/src/routes/session/sidebar.tsx
index 0c5d2b313967..6103e2253200 100644
--- a/packages/tui/src/routes/session/sidebar.tsx
+++ b/packages/tui/src/routes/session/sidebar.tsx
@@ -77,9 +77,6 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
-
- {session()!.share!.url}
-