Skip to content

Commit 11bcdfe

Browse files
authored
desktop: new-session deeplink (anomalyco#15322)
1 parent d5d7ef4 commit 11bcdfe

4 files changed

Lines changed: 106 additions & 37 deletions

File tree

packages/app/src/pages/layout.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { playSound, soundSrc } from "@/utils/sound"
4444
import { createAim } from "@/utils/aim"
4545
import { setNavigate } from "@/utils/notification-click"
4646
import { Worktree as WorktreeState } from "@/utils/worktree"
47+
import { setSessionHandoff } from "@/pages/session/handoff"
4748

4849
import { useDialog } from "@opencode-ai/ui/context/dialog"
4950
import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
@@ -67,7 +68,12 @@ import {
6768
sortedRootSessions,
6869
workspaceKey,
6970
} from "./layout/helpers"
70-
import { collectOpenProjectDeepLinks, deepLinkEvent, drainPendingDeepLinks } from "./layout/deep-links"
71+
import {
72+
collectNewSessionDeepLinks,
73+
collectOpenProjectDeepLinks,
74+
deepLinkEvent,
75+
drainPendingDeepLinks,
76+
} from "./layout/deep-links"
7177
import { createInlineEditorController } from "./layout/inline-editor"
7278
import {
7379
LocalWorkspace,
@@ -1177,9 +1183,20 @@ export default function Layout(props: ParentProps) {
11771183

11781184
const handleDeepLinks = (urls: string[]) => {
11791185
if (!server.isLocal()) return
1186+
11801187
for (const directory of collectOpenProjectDeepLinks(urls)) {
11811188
openProject(directory)
11821189
}
1190+
1191+
for (const link of collectNewSessionDeepLinks(urls)) {
1192+
openProject(link.directory, false)
1193+
const slug = base64Encode(link.directory)
1194+
if (link.prompt) {
1195+
setSessionHandoff(slug, { prompt: link.prompt })
1196+
}
1197+
const href = link.prompt ? `/${slug}/session?prompt=${encodeURIComponent(link.prompt)}` : `/${slug}/session`
1198+
navigateWithSidebarReset(href)
1199+
}
11831200
}
11841201

11851202
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: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { UserMessage } from "@opencode-ai/sdk/v2"
2+
import { useDialog } from "@opencode-ai/ui/context/dialog"
13
import {
24
onCleanup,
35
Show,
@@ -9,7 +11,6 @@ import {
911
on,
1012
onMount,
1113
untrack,
12-
createSignal,
1314
} from "solid-js"
1415
import { createMediaQuery } from "@solid-primitives/media"
1516
import { createResizeObserver } from "@solid-primitives/resize-observer"
@@ -20,29 +21,26 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
2021
import { Select } from "@opencode-ai/ui/select"
2122
import { createAutoScroll } from "@opencode-ai/ui/hooks"
2223
import { Mark } from "@opencode-ai/ui/logo"
23-
24-
import { useSync } from "@/context/sync"
25-
import { useLayout } from "@/context/layout"
26-
import { checksum, base64Encode } from "@opencode-ai/util/encode"
27-
import { useDialog } from "@opencode-ai/ui/context/dialog"
24+
import { base64Encode, checksum } from "@opencode-ai/util/encode"
25+
import { useNavigate, useParams, useSearchParams } from "@solidjs/router"
26+
import { NewSessionView, SessionHeader } from "@/components/session"
27+
import { useComments } from "@/context/comments"
2828
import { useLanguage } from "@/context/language"
29-
import { useNavigate, useParams } from "@solidjs/router"
30-
import { UserMessage } from "@opencode-ai/sdk/v2"
31-
import { useSDK } from "@/context/sdk"
29+
import { useLayout } from "@/context/layout"
3230
import { usePrompt } from "@/context/prompt"
33-
import { useComments } from "@/context/comments"
34-
import { SessionHeader, NewSessionView } from "@/components/session"
35-
import { same } from "@/utils/same"
31+
import { useSDK } from "@/context/sdk"
32+
import { useSync } from "@/context/sync"
33+
import { createSessionComposerState, SessionComposerRegion } from "@/pages/session/composer"
3634
import { createOpenReviewFile } from "@/pages/session/helpers"
37-
import { createScrollSpy } from "@/pages/session/scroll-spy"
38-
import { SessionReviewTab, type DiffStyle, type SessionReviewTabProps } from "@/pages/session/review-tab"
39-
import { TerminalPanel } from "@/pages/session/terminal-panel"
4035
import { MessageTimeline } from "@/pages/session/message-timeline"
41-
import { useSessionCommands } from "@/pages/session/use-session-commands"
42-
import { SessionComposerRegion, createSessionComposerState } from "@/pages/session/composer"
36+
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
37+
import { createScrollSpy } from "@/pages/session/scroll-spy"
4338
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
4439
import { SessionSidePanel } from "@/pages/session/session-side-panel"
40+
import { TerminalPanel } from "@/pages/session/terminal-panel"
41+
import { useSessionCommands } from "@/pages/session/use-session-commands"
4542
import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll"
43+
import { same } from "@/utils/same"
4644

4745
const emptyUserMessages: UserMessage[] = []
4846

@@ -265,6 +263,19 @@ export default function Page() {
265263
const sdk = useSDK()
266264
const prompt = usePrompt()
267265
const comments = useComments()
266+
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
267+
268+
createEffect(() => {
269+
if (!untrack(() => prompt.ready())) return
270+
prompt.ready()
271+
untrack(() => {
272+
if (params.id || !prompt.ready()) return
273+
const text = searchParams.prompt
274+
if (!text) return
275+
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
276+
setSearchParams({ ...searchParams, prompt: undefined })
277+
})
278+
})
268279

269280
const [ui, setUi] = createStore({
270281
pendingMessage: undefined as string | undefined,
@@ -679,7 +690,11 @@ export default function Page() {
679690
on(
680691
sessionKey,
681692
() => {
682-
setTree({ reviewScroll: undefined, pendingDiff: undefined, activeDiff: undefined })
693+
setTree({
694+
reviewScroll: undefined,
695+
pendingDiff: undefined,
696+
activeDiff: undefined,
697+
})
683698
},
684699
{ defer: true },
685700
),

0 commit comments

Comments
 (0)