Skip to content

Commit 7e85717

Browse files
Apply PR #15322: desktop: new-session deeplink
2 parents eb05c81 + 14acf26 commit 7e85717

4 files changed

Lines changed: 113 additions & 43 deletions

File tree

packages/app/src/pages/layout.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { retry } from "@opencode-ai/util/retry"
4343
import { playSound, soundSrc } from "@/utils/sound"
4444
import { createAim } from "@/utils/aim"
4545
import { Worktree as WorktreeState } from "@/utils/worktree"
46+
import { setSessionHandoff } from "@/pages/session/handoff"
4647

4748
import { useDialog } from "@opencode-ai/ui/context/dialog"
4849
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
@@ -66,7 +67,12 @@ import {
6667
syncWorkspaceOrder,
6768
workspaceKey,
6869
} from "./layout/helpers"
69-
import { collectOpenProjectDeepLinks, deepLinkEvent, drainPendingDeepLinks } from "./layout/deep-links"
70+
import {
71+
collectNewSessionDeepLinks,
72+
collectOpenProjectDeepLinks,
73+
deepLinkEvent,
74+
drainPendingDeepLinks,
75+
} from "./layout/deep-links"
7076
import { createInlineEditorController } from "./layout/inline-editor"
7177
import {
7278
LocalWorkspace,
@@ -1157,9 +1163,20 @@ export default function Layout(props: ParentProps) {
11571163

11581164
const handleDeepLinks = (urls: string[]) => {
11591165
if (!server.isLocal()) return
1166+
11601167
for (const directory of collectOpenProjectDeepLinks(urls)) {
11611168
openProject(directory)
11621169
}
1170+
1171+
for (const link of collectNewSessionDeepLinks(urls)) {
1172+
openProject(link.directory, false)
1173+
const slug = base64Encode(link.directory)
1174+
if (link.prompt) {
1175+
setSessionHandoff(slug, { prompt: link.prompt })
1176+
}
1177+
const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session`
1178+
navigateWithSidebarReset(href)
1179+
}
11631180
}
11641181

11651182
onMount(() => {

packages/app/src/pages/layout/deep-links.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,41 @@
11
export const deepLinkEvent = "opencode:deep-link"
22

3-
export const parseDeepLink = (input: string) => {
3+
const parseUrl = (input: string) => {
44
if (!input.startsWith("opencode://")) return
55
if (typeof URL.canParse === "function" && !URL.canParse(input)) return
6-
const url = (() => {
7-
try {
8-
return new URL(input)
9-
} catch {
10-
return undefined
11-
}
12-
})()
6+
try {
7+
return new URL(input)
8+
} catch {
9+
return
10+
}
11+
}
12+
13+
export const parseDeepLink = (input: string) => {
14+
const url = parseUrl(input)
1315
if (!url) return
1416
if (url.hostname !== "open-project") return
1517
const directory = url.searchParams.get("directory")
1618
if (!directory) return
1719
return directory
1820
}
1921

22+
export const parseNewSessionDeepLink = (input: string) => {
23+
const url = parseUrl(input)
24+
if (!url) return
25+
if (url.hostname !== "new-session") return
26+
const directory = url.searchParams.get("directory")
27+
if (!directory) return
28+
const prompt = url.searchParams.get("prompt") || undefined
29+
if (!prompt) return { directory }
30+
return { directory, prompt }
31+
}
32+
2033
export const collectOpenProjectDeepLinks = (urls: string[]) =>
2134
urls.map(parseDeepLink).filter((directory): directory is string => !!directory)
2235

36+
export const collectNewSessionDeepLinks = (urls: string[]) =>
37+
urls.map(parseNewSessionDeepLink).filter((link): link is { directory: string; prompt?: string } => !!link)
38+
2339
type OpenCodeWindow = Window & {
2440
__OPENCODE__?: {
2541
deepLinks?: string[]

packages/app/src/pages/layout/helpers.test.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { describe, expect, test } from "bun:test"
2-
import { type Session } from "@opencode-ai/sdk/v2/client"
3-
import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links"
42
import {
5-
displayName,
6-
errorMessage,
7-
getDraggableId,
8-
hasProjectPermissions,
9-
latestRootSession,
10-
syncWorkspaceOrder,
11-
workspaceKey,
12-
} from "./helpers"
3+
collectNewSessionDeepLinks,
4+
collectOpenProjectDeepLinks,
5+
drainPendingDeepLinks,
6+
parseDeepLink,
7+
parseNewSessionDeepLink,
8+
} from "./deep-links"
9+
import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers"
10+
import { type Session } from "@opencode-ai/sdk/v2/client"
11+
import { hasProjectPermissions, latestRootSession } from "./helpers"
1312

1413
const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
1514
({
@@ -62,6 +61,28 @@ describe("layout deep links", () => {
6261
expect(result).toEqual(["/a", "/c"])
6362
})
6463

64+
test("parses new-session deep links with optional prompt", () => {
65+
expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo")).toEqual({ directory: "/tmp/demo" })
66+
expect(parseNewSessionDeepLink("opencode://new-session?directory=/tmp/demo&prompt=hello%20world")).toEqual({
67+
directory: "/tmp/demo",
68+
prompt: "hello world",
69+
})
70+
})
71+
72+
test("ignores new-session deep links without directory", () => {
73+
expect(parseNewSessionDeepLink("opencode://new-session")).toBeUndefined()
74+
expect(parseNewSessionDeepLink("opencode://new-session?directory=")).toBeUndefined()
75+
})
76+
77+
test("collects only valid new-session deep links", () => {
78+
const result = collectNewSessionDeepLinks([
79+
"opencode://new-session?directory=/a",
80+
"opencode://open-project?directory=/b",
81+
"opencode://new-session?directory=/c&prompt=ship%20it",
82+
])
83+
expect(result).toEqual([{ directory: "/a" }, { directory: "/c", prompt: "ship it" }])
84+
})
85+
6586
test("drains global deep links once", () => {
6687
const target = {
6788
__OPENCODE__: {

packages/app/src/pages/session.tsx

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,35 @@
1-
import { onCleanup, Show, Match, Switch, createMemo, createEffect, on, onMount } from "solid-js"
1+
import type { UserMessage } from "@opencode-ai/sdk/v2"
2+
import { useDialog } from "@opencode-ai/ui/context/dialog"
3+
import { createAutoScroll } from "@opencode-ai/ui/hooks"
4+
import { Mark } from "@opencode-ai/ui/logo"
5+
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
6+
import { Select } from "@opencode-ai/ui/select"
7+
import { base64Encode, checksum } from "@opencode-ai/util/encode"
28
import { createMediaQuery } from "@solid-primitives/media"
39
import { createResizeObserver } from "@solid-primitives/resize-observer"
4-
import { useLocal } from "@/context/local"
5-
import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file"
10+
import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
11+
import { createEffect, createMemo, Match, on, onCleanup, onMount, Show, Switch, untrack } from "solid-js"
612
import { createStore } from "solid-js/store"
7-
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
8-
import { Select } from "@opencode-ai/ui/select"
9-
import { createAutoScroll } from "@opencode-ai/ui/hooks"
10-
import { Mark } from "@opencode-ai/ui/logo"
11-
12-
import { useSync } from "@/context/sync"
13-
import { useLayout } from "@/context/layout"
14-
import { checksum, base64Encode } from "@opencode-ai/util/encode"
15-
import { useDialog } from "@opencode-ai/ui/context/dialog"
13+
import { NewSessionView, SessionHeader } from "@/components/session"
14+
import { useComments } from "@/context/comments"
15+
import { type FileSelection, type SelectedLineRange, selectionFromLines, useFile } from "@/context/file"
1616
import { useLanguage } from "@/context/language"
17-
import { useNavigate, useParams } from "@solidjs/router"
18-
import { UserMessage } from "@opencode-ai/sdk/v2"
19-
import { useSDK } from "@/context/sdk"
17+
import { useLayout } from "@/context/layout"
18+
import { useLocal } from "@/context/local"
2019
import { usePrompt } from "@/context/prompt"
21-
import { useComments } from "@/context/comments"
22-
import { SessionHeader, NewSessionView } from "@/components/session"
23-
import { same } from "@/utils/same"
20+
import { useSDK } from "@/context/sdk"
21+
import { useSync } from "@/context/sync"
22+
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
2423
import { createOpenReviewFile } from "@/pages/session/helpers"
25-
import { createScrollSpy } from "@/pages/session/scroll-spy"
26-
import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/pages/session/review-tab"
27-
import { TerminalPanel } from "@/pages/session/terminal-panel"
2824
import { MessageTimeline } from "@/pages/session/message-timeline"
29-
import { useSessionCommands } from "@/pages/session/use-session-commands"
30-
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
25+
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
26+
import { createScrollSpy } from "@/pages/session/scroll-spy"
3127
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
3228
import { SessionSidePanel } from "@/pages/session/session-side-panel"
29+
import { TerminalPanel } from "@/pages/session/terminal-panel"
30+
import { useSessionCommands } from "@/pages/session/use-session-commands"
3331
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
32+
import { same } from "@/utils/same"
3433

3534
export default function Page() {
3635
const layout = useLayout()
@@ -44,6 +43,19 @@ export default function Page() {
4443
const sdk = useSDK()
4544
const prompt = usePrompt()
4645
const comments = useComments()
46+
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
47+
48+
createEffect(() => {
49+
if (!untrack(() => prompt.ready())) return
50+
prompt.ready()
51+
untrack(() => {
52+
if (params.id || !prompt.ready()) return
53+
const text = searchParams.prompt
54+
if (!text) return
55+
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
56+
setSearchParams({ ...searchParams, prompt: undefined })
57+
})
58+
})
4759

4860
const [ui, setUi] = createStore({
4961
pendingMessage: undefined as string | undefined,
@@ -478,7 +490,11 @@ export default function Page() {
478490
on(
479491
sessionKey,
480492
() => {
481-
setTree({ reviewScroll: undefined, pendingDiff: undefined, activeDiff: undefined })
493+
setTree({
494+
reviewScroll: undefined,
495+
pendingDiff: undefined,
496+
activeDiff: undefined,
497+
})
482498
},
483499
{ defer: true },
484500
),

0 commit comments

Comments
 (0)