summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-17 03:47:44 -0600
committerAdam <[email protected]>2025-12-17 03:47:49 -0600
commit494e6fff019bb502fff88a07d6c519c063af9a02 (patch)
treecda9a1f68c013c7a542cbc4f82c7b7b63f9fa0a1 /packages
parent0c7a297b1d3d9cdf8a060c4ed75160152da2b981 (diff)
downloadopencode-494e6fff019bb502fff88a07d6c519c063af9a02.tar.gz
opencode-494e6fff019bb502fff88a07d6c519c063af9a02.zip
feat(desktop): share sessions
Diffstat (limited to 'packages')
-rw-r--r--packages/desktop/src/components/header.tsx36
-rw-r--r--packages/desktop/src/pages/layout.tsx26
-rw-r--r--packages/desktop/src/pages/session.tsx47
-rw-r--r--packages/desktop/src/utils/solid-dnd.tsx55
-rw-r--r--packages/ui/src/components/icon.tsx1
-rw-r--r--packages/ui/src/components/popover.css95
-rw-r--r--packages/ui/src/components/popover.tsx44
-rw-r--r--packages/ui/src/components/text-field.tsx10
-rw-r--r--packages/ui/src/styles/index.css1
9 files changed, 240 insertions, 75 deletions
diff --git a/packages/desktop/src/components/header.tsx b/packages/desktop/src/components/header.tsx
index cc4d01816..d70bfee24 100644
--- a/packages/desktop/src/components/header.tsx
+++ b/packages/desktop/src/components/header.tsx
@@ -1,27 +1,34 @@
import { useGlobalSync } from "@/context/global-sync"
+import { useGlobalSDK } from "@/context/global-sdk"
import { useLayout } from "@/context/layout"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Mark } from "@opencode-ai/ui/logo"
+import { Popover } from "@opencode-ai/ui/popover"
import { Select } from "@opencode-ai/ui/select"
+import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Decode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router"
-import { createMemo, Show } from "solid-js"
+import { createMemo, createResource, Show } from "solid-js"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { iife } from "@opencode-ai/util/iife"
export function Header(props: {
navigateToProject: (directory: string) => void
navigateToSession: (session: Session | undefined) => void
}) {
const globalSync = useGlobalSync()
+ const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const store = createMemo(() => globalSync.child(currentDirectory())[0])
const sessions = createMemo(() => store().session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
+ const shareEnabled = createMemo(() => store().config.share !== "disabled")
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
@@ -105,6 +112,33 @@ export function Header(props: {
</div>
</Button>
</Tooltip>
+ <Show when={shareEnabled() && currentSession()}>
+ <Popover
+ title="Share session"
+ trigger={
+ <Tooltip class="shrink-0" value="Share session">
+ <IconButton icon="share" variant="ghost" class="" />
+ </Tooltip>
+ }
+ >
+ {iife(() => {
+ const [url] = createResource(
+ () => currentSession(),
+ async (session) => {
+ if (!session) return
+ let shareURL = session.share?.url
+ if (!shareURL) {
+ shareURL = await globalSDK.client.session
+ .share({ sessionID: session.id, directory: currentDirectory() })
+ .then((r) => r.data?.share?.url)
+ }
+ return shareURL
+ },
+ )
+ return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
+ })}
+ </Popover>
+ </Show>
</div>
</Show>
</div>
diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx
index bed8950c7..618b84840 100644
--- a/packages/desktop/src/pages/layout.tsx
+++ b/packages/desktop/src/pages/layout.tsx
@@ -25,9 +25,8 @@ import {
SortableProvider,
closestCenter,
createSortable,
- useDragDropContext,
} from "@thisbeyond/solid-dnd"
-import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
+import type { DragEvent } from "@thisbeyond/solid-dnd"
import { useProviders } from "@/hooks/use-providers"
import { Toast } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
@@ -37,6 +36,7 @@ import { Header } from "@/components/header"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { useCommand } from "@/context/command"
+import { ConstrainDragXAxis } from "@/utils/solid-dnd"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
@@ -301,28 +301,6 @@ export default function Layout(props: ParentProps) {
setStore("activeDraggable", undefined)
}
- const ConstrainDragXAxis = (): JSX.Element => {
- const context = useDragDropContext()
- if (!context) return <></>
- const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
- const transformer: Transformer = {
- id: "constrain-x-axis",
- order: 100,
- callback: (transform) => ({ ...transform, x: 0 }),
- }
- onDragStart((event) => {
- const id = getDraggableId(event)
- if (!id) return
- addTransformer("draggables", id, transformer)
- })
- onDragEnd((event) => {
- const id = getDraggableId(event)
- if (!id) return
- removeTransformer("draggables", id, transformer.id)
- })
- return <></>
- }
-
const ProjectAvatar = (props: {
project: Project
class?: string
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index d024d5047..3415d0c4e 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -23,9 +23,8 @@ import {
SortableProvider,
closestCenter,
createSortable,
- useDragDropContext,
} from "@thisbeyond/solid-dnd"
-import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
+import type { DragEvent } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { useTerminal, type LocalPTY } from "@/context/terminal"
@@ -42,6 +41,7 @@ import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { extractPromptFromParts } from "@/utils/prompt"
+import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
export default function Page() {
const layout = useLayout()
@@ -324,19 +324,6 @@ export default function Page() {
if ((document.activeElement as HTMLElement)?.dataset?.component === "terminal") return
if (dialog.active) return
- if (event.key === "PageUp" || event.key === "PageDown") {
- const scrollContainer = document.querySelector('[data-slot="session-turn-content"]') as HTMLElement
- if (scrollContainer) {
- event.preventDefault()
- const scrollAmount = scrollContainer.clientHeight * 0.8
- scrollContainer.scrollBy({
- top: event.key === "PageUp" ? -scrollAmount : scrollAmount,
- behavior: "instant",
- })
- }
- return
- }
-
const focused = document.activeElement === inputRef
if (focused) {
if (event.key === "Escape") inputRef?.blur()
@@ -519,36 +506,6 @@ export default function Page() {
)
}
- const ConstrainDragYAxis = (): JSX.Element => {
- const context = useDragDropContext()
- if (!context) return <></>
- const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
- const transformer: Transformer = {
- id: "constrain-y-axis",
- order: 100,
- callback: (transform) => ({ ...transform, y: 0 }),
- }
- onDragStart((event) => {
- const id = getDraggableId(event)
- if (!id) return
- addTransformer("draggables", id, transformer)
- })
- onDragEnd((event) => {
- const id = getDraggableId(event)
- if (!id) return
- removeTransformer("draggables", id, transformer.id)
- })
- return <></>
- }
-
- const getDraggableId = (event: unknown): string | undefined => {
- if (typeof event !== "object" || event === null) return undefined
- if (!("draggable" in event)) return undefined
- const draggable = (event as { draggable?: { id?: unknown } }).draggable
- if (!draggable) return undefined
- return typeof draggable.id === "string" ? draggable.id : undefined
- }
-
const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length)
return (
diff --git a/packages/desktop/src/utils/solid-dnd.tsx b/packages/desktop/src/utils/solid-dnd.tsx
new file mode 100644
index 000000000..a634be4b4
--- /dev/null
+++ b/packages/desktop/src/utils/solid-dnd.tsx
@@ -0,0 +1,55 @@
+import { useDragDropContext } from "@thisbeyond/solid-dnd"
+import { JSXElement } from "solid-js"
+import type { Transformer } from "@thisbeyond/solid-dnd"
+
+export const getDraggableId = (event: unknown): string | undefined => {
+ if (typeof event !== "object" || event === null) return undefined
+ if (!("draggable" in event)) return undefined
+ const draggable = (event as { draggable?: { id?: unknown } }).draggable
+ if (!draggable) return undefined
+ return typeof draggable.id === "string" ? draggable.id : undefined
+}
+
+export const ConstrainDragXAxis = (): JSXElement => {
+ const context = useDragDropContext()
+ if (!context) return <></>
+ const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
+ const transformer: Transformer = {
+ id: "constrain-x-axis",
+ order: 100,
+ callback: (transform) => ({ ...transform, x: 0 }),
+ }
+ onDragStart((event) => {
+ const id = getDraggableId(event)
+ if (!id) return
+ addTransformer("draggables", id, transformer)
+ })
+ onDragEnd((event) => {
+ const id = getDraggableId(event)
+ if (!id) return
+ removeTransformer("draggables", id, transformer.id)
+ })
+ return <></>
+}
+
+export const ConstrainDragYAxis = (): JSXElement => {
+ const context = useDragDropContext()
+ if (!context) return <></>
+ const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
+ const transformer: Transformer = {
+ id: "constrain-y-axis",
+ order: 100,
+ callback: (transform) => ({ ...transform, y: 0 }),
+ }
+ onDragStart((event) => {
+ const id = getDraggableId(event)
+ if (!id) return
+ addTransformer("draggables", id, transformer)
+ })
+ onDragEnd((event) => {
+ const id = getDraggableId(event)
+ if (!id) return
+ removeTransformer("draggables", id, transformer.id)
+ })
+ return <></>
+}
diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx
index b8e8106e8..94d9544d6 100644
--- a/packages/ui/src/components/icon.tsx
+++ b/packages/ui/src/components/icon.tsx
@@ -52,6 +52,7 @@ const icons = {
copy: `<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>`,
check: `<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>`,
photo: `<path d="M16.6665 16.6666L11.6665 11.6666L9.99984 13.3333L6.6665 9.99996L3.08317 13.5833M2.9165 2.91663H17.0832V17.0833H2.9165V2.91663ZM13.3332 7.49996C13.3332 8.30537 12.6803 8.95829 11.8748 8.95829C11.0694 8.95829 10.4165 8.30537 10.4165 7.49996C10.4165 6.69454 11.0694 6.04163 11.8748 6.04163C12.6803 6.04163 13.3332 6.69454 13.3332 7.49996Z" stroke="currentColor" stroke-linecap="square"/>`,
+ share: `<path d="M10.0013 12.0846L10.0013 3.33464M13.7513 6.66797L10.0013 2.91797L6.2513 6.66797M17.0846 10.418V17.0846H2.91797V10.418" stroke="currentColor" stroke-linecap="square"/>`,
}
export interface IconProps extends ComponentProps<"svg"> {
diff --git a/packages/ui/src/components/popover.css b/packages/ui/src/components/popover.css
new file mode 100644
index 000000000..74d7f5a39
--- /dev/null
+++ b/packages/ui/src/components/popover.css
@@ -0,0 +1,95 @@
+[data-slot="popover-trigger"] {
+ display: inline-flex;
+}
+
+[data-component="popover-content"] {
+ z-index: 50;
+ min-width: 200px;
+ max-width: 320px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-weak-base);
+ background-color: var(--surface-raised-stronger-non-alpha);
+ box-shadow: var(--shadow-md);
+ transform-origin: var(--kb-popover-content-transform-origin);
+
+ &:focus-within {
+ outline: none;
+ }
+
+ &[data-closed] {
+ animation: popover-close 0.15s ease-out;
+ }
+
+ &[data-expanded] {
+ animation: popover-open 0.15s ease-out;
+ }
+
+ [data-slot="popover-header"] {
+ display: flex;
+ padding: 12px;
+ padding-bottom: 0;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+
+ [data-slot="popover-title"] {
+ flex: 1;
+ color: var(--text-strong);
+ margin: 0;
+
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-base);
+ font-style: normal;
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large);
+ letter-spacing: var(--letter-spacing-normal);
+ }
+
+ [data-slot="popover-close-button"] {
+ flex-shrink: 0;
+ }
+ }
+
+ [data-slot="popover-description"] {
+ padding: 0 12px;
+ margin: 0;
+ color: var(--text-base);
+
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-style: normal;
+ font-weight: var(--font-weight-regular);
+ line-height: var(--line-height-large);
+ letter-spacing: var(--letter-spacing-normal);
+ }
+
+ [data-slot="popover-body"] {
+ padding: 12px;
+ }
+
+ [data-slot="popover-arrow"] {
+ fill: var(--surface-raised-stronger-non-alpha);
+ }
+}
+
+@keyframes popover-open {
+ from {
+ opacity: 0;
+ transform: scale(0.96);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+@keyframes popover-close {
+ from {
+ opacity: 1;
+ transform: scale(1);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.96);
+ }
+}
diff --git a/packages/ui/src/components/popover.tsx b/packages/ui/src/components/popover.tsx
new file mode 100644
index 000000000..3262098e5
--- /dev/null
+++ b/packages/ui/src/components/popover.tsx
@@ -0,0 +1,44 @@
+import { Popover as Kobalte } from "@kobalte/core/popover"
+import { ComponentProps, JSXElement, ParentProps, Show, splitProps } from "solid-js"
+import { IconButton } from "./icon-button"
+
+export interface PopoverProps extends ParentProps, Omit<ComponentProps<typeof Kobalte>, "children"> {
+ trigger: JSXElement
+ title?: JSXElement
+ description?: JSXElement
+ class?: ComponentProps<"div">["class"]
+ classList?: ComponentProps<"div">["classList"]
+}
+
+export function Popover(props: PopoverProps) {
+ const [local, rest] = splitProps(props, ["trigger", "title", "description", "class", "classList", "children"])
+
+ return (
+ <Kobalte gutter={4} {...rest}>
+ <Kobalte.Trigger as="div" data-slot="popover-trigger">
+ {local.trigger}
+ </Kobalte.Trigger>
+ <Kobalte.Portal>
+ <Kobalte.Content
+ data-component="popover-content"
+ classList={{
+ ...(local.classList ?? {}),
+ [local.class ?? ""]: !!local.class,
+ }}
+ >
+ {/* <Kobalte.Arrow data-slot="popover-arrow" /> */}
+ <Show when={local.title}>
+ <div data-slot="popover-header">
+ <Kobalte.Title data-slot="popover-title">{local.title}</Kobalte.Title>
+ <Kobalte.CloseButton data-slot="popover-close-button" as={IconButton} icon="close" variant="ghost" />
+ </div>
+ </Show>
+ <Show when={local.description}>
+ <Kobalte.Description data-slot="popover-description">{local.description}</Kobalte.Description>
+ </Show>
+ <div data-slot="popover-body">{local.children}</div>
+ </Kobalte.Content>
+ </Kobalte.Portal>
+ </Kobalte>
+ )
+}
diff --git a/packages/ui/src/components/text-field.tsx b/packages/ui/src/components/text-field.tsx
index 63ffb2594..77f014b6b 100644
--- a/packages/ui/src/components/text-field.tsx
+++ b/packages/ui/src/components/text-field.tsx
@@ -56,6 +56,10 @@ export function TextField(props: TextFieldProps) {
setTimeout(() => setCopied(false), 2000)
}
+ function handleClick() {
+ if (local.copyable) handleCopy()
+ }
+
return (
<Kobalte
data-component="input"
@@ -65,6 +69,7 @@ export function TextField(props: TextFieldProps) {
value={local.value}
onChange={local.onChange}
onKeyDown={local.onKeyDown}
+ onClick={handleClick}
required={local.required}
disabled={local.disabled}
readOnly={local.readOnly}
@@ -96,8 +101,3 @@ export function TextField(props: TextFieldProps) {
</Kobalte>
)
}
-
-/** @deprecated Use TextField instead */
-export const Input = TextField
-/** @deprecated Use TextFieldProps instead */
-export type InputProps = TextFieldProps
diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css
index 3f8838a7a..c4302a4d3 100644
--- a/packages/ui/src/styles/index.css
+++ b/packages/ui/src/styles/index.css
@@ -27,6 +27,7 @@
@import "../components/markdown.css" layer(components);
@import "../components/message-part.css" layer(components);
@import "../components/message-nav.css" layer(components);
+@import "../components/popover.css" layer(components);
@import "../components/progress-circle.css" layer(components);
@import "../components/resize-handle.css" layer(components);
@import "../components/select.css" layer(components);