mirror of
https://github.com/ollama/ollama.git
synced 2026-09-21 13:38:14 -05:00
app: add Apps deep link (#18497)
This commit is contained in:
+18
-11
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
+42
-1
@@ -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
|
||||
|
||||
@@ -107,6 +107,10 @@ func openUI(path string) {
|
||||
wv.Run(path)
|
||||
}
|
||||
|
||||
func openAppsUI() {
|
||||
wv.Run("/connect")
|
||||
}
|
||||
|
||||
func (*appCallbacks) UITerminate() {
|
||||
wv.Terminate()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user