From 2679e412b374d6c96887445eaa364812ae6acdb4 Mon Sep 17 00:00:00 2001 From: zhw445611 Date: Sat, 24 Jan 2026 10:02:40 +0800 Subject: [PATCH 1/2] add(index.mdx):zhw445 --- packages/docs/index.mdx | 58 ++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 39 deletions(-) diff --git a/packages/docs/index.mdx b/packages/docs/index.mdx index 19a09f890cd..e138987046c 100644 --- a/packages/docs/index.mdx +++ b/packages/docs/index.mdx @@ -1,56 +1,36 @@ --- title: "Introduction" -description: "Welcome to the new home for your documentation" +description: "OpenCode is the open source AI coding agent for the terminal." --- -## Setting up +## What is OpenCode -Get your documentation site up and running in minutes. +OpenCode is an open source AI coding agent focused on the terminal experience. +It runs locally, supports multiple model providers, and offers both a TUI and API-based workflows. +You can use it to explore codebases, make changes, run tests, and automate common engineering tasks. - - Follow our three step quickstart guide. - - -## Make it yours +## Quick start -Design a docs site that looks great and empowers your users. + + One command installation for macOS, Linux, and Windows. + - - Edit your docs locally and preview them in real time. + + Launch OpenCode and start a session in minutes. - - Customize the design and colors of your site to match your brand. - - - Organize your docs to help users find what they need and succeed with your product. - - - Auto-generate API documentation from OpenAPI specifications. + + Preview docs locally and learn the basics. -## Create beautiful pages - -Everything you need to create world-class documentation. +## Community & source - - Use MDX to style your docs pages. + + Issues, roadmap, and pull requests live here. - - Add sample code to demonstrate how to use your product. + + Ask questions, share tips, and get help from the community. - - Display images and other media. - - - Write once and reuse across your docs. - - - -## Need inspiration? - - - Browse our showcase of exceptional documentation sites. - + \ No newline at end of file From 501754931b3245629b08d960b392d9f3cb57f6dc Mon Sep 17 00:00:00 2001 From: zhw445611 Date: Sat, 24 Jan 2026 12:34:20 +0800 Subject: [PATCH 2/2] feat(opencode): add linux clipboard toast warnings --- packages/opencode/src/cli/cmd/tui/app.tsx | 42 ++++++-- .../cli/cmd/tui/component/dialog-provider.tsx | 13 ++- .../cmd/tui/routes/session/dialog-message.tsx | 20 +++- .../src/cli/cmd/tui/routes/session/index.tsx | 101 +++++++++++++----- .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 21 +++- .../src/cli/cmd/tui/util/clipboard.ts | 54 +++++++--- 6 files changed, 198 insertions(+), 53 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4b177e292cf..28d0ef4e231 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -200,9 +200,24 @@ function App() { renderer.console.onCopySelection = async (text: string) => { if (!text || text.length === 0) return - await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) + const result = await Clipboard.copy(text).catch((error) => { + toast.error(error) + }) + if (!result) { + renderer.clearSelection() + return + } + if (result.warning) { + toast.show({ message: result.warning, variant: "warning" }) + renderer.clearSelection() + return + } + if (result.notice) { + toast.show({ message: result.notice, variant: "info" }) + renderer.clearSelection() + return + } + toast.show({ message: "Copied to clipboard", variant: "info" }) renderer.clearSelection() } const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true)) @@ -659,9 +674,24 @@ function App() { } const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { - await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) + const result = await Clipboard.copy(text).catch((error) => { + toast.error(error) + }) + if (!result) { + renderer.clearSelection() + return + } + if (result.warning) { + toast.show({ message: result.warning, variant: "warning" }) + renderer.clearSelection() + return + } + if (result.notice) { + toast.show({ message: result.notice, variant: "info" }) + renderer.clearSelection() + return + } + toast.show({ message: "Copied to clipboard", variant: "info" }) renderer.clearSelection() } }} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx index 4e1171a4201..435efce4c34 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx @@ -129,7 +129,18 @@ function AutoMethod(props: AutoMethodProps) { if (evt.name === "c" && !evt.ctrl && !evt.meta) { const code = props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4}/)?.[0] ?? props.authorization.url Clipboard.copy(code) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) + .then((result) => { + if (!result) return + if (result.warning) { + toast.show({ message: result.warning, variant: "warning" }) + return + } + if (result.notice) { + toast.show({ message: result.notice, variant: "info" }) + return + } + toast.show({ message: "Copied to clipboard", variant: "info" }) + }) .catch(toast.error) } }) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx index ff17b5567eb..3847bddd1e4 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx @@ -4,6 +4,7 @@ import { DialogSelect } from "@tui/ui/dialog-select" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" import { Clipboard } from "@tui/util/clipboard" +import { useToast } from "@tui/ui/toast" import type { PromptInfo } from "@tui/component/prompt/history" export function DialogMessage(props: { @@ -15,6 +16,7 @@ export function DialogMessage(props: { const sdk = useSDK() const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID)) const route = useRoute() + const toast = useToast() return ( { + toast.error(error) + }) + if (!result) { + dialog.clear() + return + } + if (result.warning) { + toast.show({ message: result.warning, variant: "warning" }) + dialog.clear() + return + } + if (result.notice) { + toast.show({ message: result.notice, variant: "info" }) + dialog.clear() + return + } dialog.clear() }, }, diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 1294ab849e9..54e886bbe98 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -306,17 +306,35 @@ export function Session() { name: "share", }, onSelect: async (dialog) => { - await sdk.client.session + const response = await sdk.client.session .share({ sessionID: route.sessionID, }) - .then((res) => - Clipboard.copy(res.data!.share!.url).catch(() => - toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }), - ), - ) - .then(() => toast.show({ message: "Share URL copied to clipboard!", variant: "success" })) - .catch(() => toast.show({ message: "Failed to share session", variant: "error" })) + .catch(() => { + toast.show({ message: "Failed to share session", variant: "error" }) + }) + if (!response) { + dialog.clear() + return + } + const result = await Clipboard.copy(response.data!.share!.url).catch(() => { + toast.show({ message: "Failed to copy URL to clipboard", variant: "error" }) + }) + if (!result) { + dialog.clear() + return + } + if (result.warning) { + toast.show({ message: result.warning, variant: "warning" }) + dialog.clear() + return + } + if (result.notice) { + toast.show({ message: result.notice, variant: "info" }) + dialog.clear() + return + } + toast.show({ message: "Share URL copied to clipboard!", variant: "success" }) dialog.clear() }, }, @@ -720,7 +738,7 @@ export function Session() { value: "messages.copy", keybind: "messages_copy", category: "Session", - onSelect: (dialog) => { + onSelect: async (dialog) => { const revertID = session()?.revert?.messageID const lastAssistantMessage = messages().findLast( (msg) => msg.role === "assistant" && (!revertID || msg.id < revertID), @@ -752,9 +770,24 @@ export function Session() { return } - Clipboard.copy(text) - .then(() => toast.show({ message: "Message copied to clipboard!", variant: "success" })) - .catch(() => toast.show({ message: "Failed to copy to clipboard", variant: "error" })) + const result = await Clipboard.copy(text).catch(() => { + toast.show({ message: "Failed to copy to clipboard", variant: "error" }) + }) + if (!result) { + dialog.clear() + return + } + if (result.warning) { + toast.show({ message: result.warning, variant: "warning" }) + dialog.clear() + return + } + if (result.notice) { + toast.show({ message: result.notice, variant: "info" }) + dialog.clear() + return + } + toast.show({ message: "Message copied to clipboard!", variant: "success" }) dialog.clear() }, }, @@ -766,24 +799,36 @@ export function Session() { name: "copy", }, onSelect: async (dialog) => { - try { - const sessionData = session() - if (!sessionData) return - const sessionMessages = messages() - const transcript = formatTranscript( - sessionData, - sessionMessages.map((msg) => ({ info: msg, parts: sync.data.part[msg.id] ?? [] })), - { - thinking: showThinking(), - toolDetails: showDetails(), - assistantMetadata: showAssistantMetadata(), - }, - ) - await Clipboard.copy(transcript) - toast.show({ message: "Session transcript copied to clipboard!", variant: "success" }) - } catch (error) { + const sessionData = session() + if (!sessionData) return + const sessionMessages = messages() + const transcript = formatTranscript( + sessionData, + sessionMessages.map((msg) => ({ info: msg, parts: sync.data.part[msg.id] ?? [] })), + { + thinking: showThinking(), + toolDetails: showDetails(), + assistantMetadata: showAssistantMetadata(), + }, + ) + const result = await Clipboard.copy(transcript).catch(() => { toast.show({ message: "Failed to copy session transcript", variant: "error" }) + }) + if (!result) { + dialog.clear() + return + } + if (result.warning) { + toast.show({ message: result.warning, variant: "warning" }) + dialog.clear() + return + } + if (result.notice) { + toast.show({ message: result.notice, variant: "info" }) + dialog.clear() + return } + toast.show({ message: "Session transcript copied to clipboard!", variant: "success" }) dialog.clear() }, }, diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 57375ba09db..939abcb7526 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -141,9 +141,24 @@ export function DialogProvider(props: ParentProps) { onMouseUp={async () => { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { - await Clipboard.copy(text) - .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) - .catch(toast.error) + const result = await Clipboard.copy(text).catch((error) => { + toast.error(error) + }) + if (!result) { + renderer.clearSelection() + return + } + if (result.warning) { + toast.show({ message: result.warning, variant: "warning" }) + renderer.clearSelection() + return + } + if (result.notice) { + toast.show({ message: result.notice, variant: "info" }) + renderer.clearSelection() + return + } + toast.show({ message: "Copied to clipboard", variant: "info" }) renderer.clearSelection() } }} diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 0e287fbc41a..892b076d256 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -21,6 +21,10 @@ function writeOsc52(text: string): void { } export namespace Clipboard { + export interface CopyResult { + notice?: string + warning?: string + } export interface Content { data: string mime: string @@ -73,30 +77,41 @@ export namespace Clipboard { } } + const noticeState = { + notice: false, + } + const getCopyMethod = lazy(() => { const os = platform() + const withNotice = (name: string, copy: (text: string) => Promise) => { + return { + name, + copy, + } + } + if (os === "darwin" && Bun.which("osascript")) { console.log("clipboard: using osascript") - return async (text: string) => { + return withNotice("osascript", async (text: string) => { const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') await $`osascript -e 'set the clipboard to "${escaped}"'`.nothrow().quiet() - } + }) } if (os === "linux") { if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) { console.log("clipboard: using wl-copy") - return async (text: string) => { + return withNotice("wl-copy", async (text: string) => { const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" }) proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) - } + }) } if (Bun.which("xclip")) { console.log("clipboard: using xclip") - return async (text: string) => { + return withNotice("xclip", async (text: string) => { const proc = Bun.spawn(["xclip", "-selection", "clipboard"], { stdin: "pipe", stdout: "ignore", @@ -105,11 +120,11 @@ export namespace Clipboard { proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) - } + }) } if (Bun.which("xsel")) { console.log("clipboard: using xsel") - return async (text: string) => { + return withNotice("xsel", async (text: string) => { const proc = Bun.spawn(["xsel", "--clipboard", "--input"], { stdin: "pipe", stdout: "ignore", @@ -118,13 +133,13 @@ export namespace Clipboard { proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) - } + }) } } if (os === "win32") { console.log("clipboard: using powershell") - return async (text: string) => { + return withNotice("powershell", async (text: string) => { // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.) const proc = Bun.spawn( [ @@ -144,17 +159,28 @@ export namespace Clipboard { proc.stdin.write(text) proc.stdin.end() await proc.exited.catch(() => {}) - } + }) } console.log("clipboard: no native support") - return async (text: string) => { + return withNotice("clipboardy", async (text: string) => { await clipboardy.write(text).catch(() => {}) - } + }) }) - export async function copy(text: string): Promise { + export async function copy(text: string): Promise { writeOsc52(text) - await getCopyMethod()(text) + const method = getCopyMethod() + await method.copy(text) + const os = platform() + if (os === "linux") { + if (method.name === "wl-copy" || method.name === "xclip" || method.name === "xsel") { + if (noticeState.notice) return {} + noticeState.notice = true + return { notice: `Clipboard: using ${method.name}` } + } + return { warning: "Clipboard: install wl-clipboard, xclip, or xsel for system copy" } + } + return {} } }