From 5ed8dde3a7a5bf4c9047a8a91906b4f6788f60b9 Mon Sep 17 00:00:00 2001 From: Eva H <63033505+hoyyeva@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:18:29 -0400 Subject: [PATCH] app: add Apps deep link (#18497) --- app/cmd/app/app.go | 29 +++++++++++++++---------- app/cmd/app/app_darwin.go | 6 ++++++ app/cmd/app/app_darwin.m | 2 ++ app/cmd/app/app_test.go | 43 +++++++++++++++++++++++++++++++++++++- app/cmd/app/app_windows.go | 4 ++++ 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/app/cmd/app/app.go b/app/cmd/app/app.go index 71714ba29..a4a01ae6e 100644 --- a/app/cmd/app/app.go +++ b/app/cmd/app/app.go @@ -505,44 +505,51 @@ func openInBrowser(url string) { } // parseURLScheme parses an ollama:// URL and validates it -// Supports: ollama:// (open app) and ollama://connect (OAuth) -func parseURLScheme(urlSchemeRequest string) (isConnect bool, err error) { +// Supports: ollama:// (open app), ollama://apps, and ollama://connect (OAuth). +func parseURLScheme(urlSchemeRequest string) (action string, err error) { parsedURL, err := url.Parse(urlSchemeRequest) if err != nil { - return false, fmt.Errorf("invalid URL: %w", err) + return "", fmt.Errorf("invalid URL: %w", err) } // Check if this is a connect URL if parsedURL.Host == "connect" || strings.TrimPrefix(parsedURL.Path, "/") == "connect" { - return true, nil + return "connect", nil + } + + if parsedURL.Host == "apps" || strings.TrimPrefix(parsedURL.Path, "/") == "apps" { + return "apps", nil } // Allow bare ollama:// or ollama:/// to open the app if (parsedURL.Host == "" && parsedURL.Path == "") || parsedURL.Path == "/" { - return false, nil + return "", nil } - return false, fmt.Errorf("unsupported ollama:// URL path: %s", urlSchemeRequest) + return "", fmt.Errorf("unsupported ollama:// URL path: %s", urlSchemeRequest) } // handleURLSchemeInCurrentInstance processes URL scheme requests in the current instance func handleURLSchemeInCurrentInstance(urlSchemeRequest string) { err := dispatchURLSchemeRequest(urlSchemeRequest, handleConnectURLScheme, func() { openUI("/") - }) + }, openAppsUI) if err != nil { slog.Error("failed to parse URL scheme request", "url", urlSchemeRequest, "error", err) } } -func dispatchURLSchemeRequest(urlSchemeRequest string, connect, open func()) error { - isConnect, err := parseURLScheme(urlSchemeRequest) +func dispatchURLSchemeRequest(urlSchemeRequest string, connect, open, apps func()) error { + action, err := parseURLScheme(urlSchemeRequest) if err != nil { return err } - if isConnect { + switch action { + case "connect": connect() - } else { + case "apps": + apps() + default: open() } return nil diff --git a/app/cmd/app/app_darwin.go b/app/cmd/app/app_darwin.go index cd9250372..7beb49113 100644 --- a/app/cmd/app/app_darwin.go +++ b/app/cmd/app/app_darwin.go @@ -152,6 +152,12 @@ func openUI(path string) { StartUI(p) } +func openAppsUI() { + p := C.CString("/connect") + defer C.free(unsafe.Pointer(p)) + C.uiRequest(p) +} + //export StopUI func StopUI() { wv.Terminate() diff --git a/app/cmd/app/app_darwin.m b/app/cmd/app/app_darwin.m index cf376b93c..18c43c58d 100644 --- a/app/cmd/app/app_darwin.m +++ b/app/cmd/app/app_darwin.m @@ -441,6 +441,8 @@ static NSImage *ollamaApplicationIcon(void) { if (path && ([path isEqualToString:@"/connect"] || [url.host isEqualToString:@"connect"])) { // Special case: handle connect by opening browser instead of app handleConnectURL(); + } else if (path && ([path isEqualToString:@"/apps"] || [url.host isEqualToString:@"apps"])) { + [self appsUI]; } else { [self openUI]; } diff --git a/app/cmd/app/app_test.go b/app/cmd/app/app_test.go index 8404688c3..a79c6f448 100644 --- a/app/cmd/app/app_test.go +++ b/app/cmd/app/app_test.go @@ -48,21 +48,30 @@ func TestDispatchURLSchemeRequest(t *testing.T) { request string wantConnect bool wantOpen bool + wantApps bool wantErr bool }{ {name: "bare URL opens app", request: "ollama://", wantOpen: true}, + {name: "root URL opens app", request: "ollama:///", wantOpen: true}, + {name: "apps URL opens Apps", request: "ollama://apps", wantApps: true}, + {name: "apps path opens Apps", request: "ollama:///apps", wantApps: true}, + {name: "apps trailing slash opens Apps", request: "ollama://apps/", wantApps: true}, {name: "connect URL starts connection", request: "ollama://connect", wantConnect: true}, + {name: "connect path starts connection", request: "ollama:///connect", wantConnect: true}, {name: "unsupported URL", request: "ollama://unsupported", wantErr: true}, + {name: "invalid URL", request: "ollama://%", wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { connected := false opened := false + openedApps := false err := dispatchURLSchemeRequest( tt.request, func() { connected = true }, func() { opened = true }, + func() { openedApps = true }, ) if (err != nil) != tt.wantErr { t.Fatalf("dispatchURLSchemeRequest() error = %v, wantErr %v", err, tt.wantErr) @@ -73,6 +82,9 @@ func TestDispatchURLSchemeRequest(t *testing.T) { if opened != tt.wantOpen { t.Errorf("open called = %v, want %v", opened, tt.wantOpen) } + if openedApps != tt.wantApps { + t.Errorf("open Apps called = %v, want %v", openedApps, tt.wantApps) + } }) } } @@ -90,7 +102,12 @@ func TestRunInitialWindowsUIWithBareURL(t *testing.T) { func() { hiddenCalls++ }, func(request string) { urlCalls++ - if err := dispatchURLSchemeRequest(request, func() {}, func() { openCalls++ }); err != nil { + err := dispatchURLSchemeRequest(request, + func() { t.Fatal("unexpected sign-in") }, + func() { openCalls++ }, + func() { t.Fatal("unexpected Apps navigation") }, + ) + if err != nil { t.Fatalf("dispatchURLSchemeRequest() error = %v", err) } }, @@ -113,6 +130,30 @@ func TestRunInitialWindowsUIWithBareURL(t *testing.T) { } } +func TestRunInitialWindowsUIWithAppsURL(t *testing.T) { + appsCalls := 0 + runInitialWindowsUI( + false, + true, + "ollama://apps", + func() { t.Fatal("unexpected hidden startup") }, + func(request string) { + err := dispatchURLSchemeRequest(request, + func() { t.Fatal("unexpected sign-in") }, + func() { t.Fatal("unexpected home navigation") }, + func() { appsCalls++ }, + ) + if err != nil { + t.Fatalf("dispatchURLSchemeRequest() error = %v", err) + } + }, + func(string) { t.Fatal("unexpected onboarding") }, + ) + if appsCalls != 1 { + t.Fatalf("Apps opened %d times, want 1", appsCalls) + } +} + func TestRunInitialWindowsUIRoutesInteractiveLaunch(t *testing.T) { for _, tt := range []struct { name string diff --git a/app/cmd/app/app_windows.go b/app/cmd/app/app_windows.go index 81923a26d..037f687b1 100644 --- a/app/cmd/app/app_windows.go +++ b/app/cmd/app/app_windows.go @@ -107,6 +107,10 @@ func openUI(path string) { wv.Run(path) } +func openAppsUI() { + wv.Run("/connect") +} + func (*appCallbacks) UITerminate() { wv.Terminate() }